main()
,所有简单的程序都可以定义其他额外的函数。.h
头文件 。int *func(int x, int y)
。int (*funcp)(int x)
。int i;
int *a = &i; //这里a是一个指针,它指向变量i
int &b = i; //这里b是一个引用,它是变量i的引用(别名)
int * &c = a; //这里c是一个引用,它是指针a的引用
int & *d; //这里d是一个指针,它指向引用,但引用不是实体,所以这是错误的
复制代码
在分析上面代码时,可以从变量标识符开始从右往左看,最靠近标识符的是变量的本质类型,而再往左即为对变量类型的进一步修饰。
例如:int * & a
标识符a的左边紧邻的是 &,证明 a 是一个引用变量,而再往左是 * ,可见 a 是一个指针的引用,再往左是 int,可见 a 是一个指向int类型的指针的引用。
.
和 ->
struct Data
{
int a,b,c;
}; /*定义结构体类型*/
struct Data * p; /* 定义结构体指针 */
struct Data A = {1,2,3}; / * 声明结构体变量A,A即结构体名 */
int x; /* 声明一个变量x */
p = &A ; /* 地址赋值,让p指向A */
x = p->a; /* 取出p所指向的结构体中包含的数据项a赋值给x */
/* 此时由于p指向A,因而 p->a == A.a,也就是1 */
复制代码
因为此处
p
是一个指针,所以不能使用.号访问内部成员(即不能p.a
),而要使用->
。但是A.a
是可以的,因为A
不是指针,是结构体名。
一般情况下用 “.”
只需要声明一个结构体。格式是:结构体类型名+结构体名
。然后用 结构体名加“.”加成员名
就可以引用成员了。因为自动分配了结构体的内存。如同 int a;
一样。 用 “->”
,则要声明一个结构体指针,还要手动开辟一个该结构体的内存(上面的代码则是建了一个结构体实例,自动分配了内存,下面的例子则会讲到手动动态开辟内存),然后把返回的地址赋给声明的结构体指针,才能用“->”
正确引用。否则内存中只分配了指针的内存,没有分配结构体的内存,导致想要的结构体实际上是不存在。这时候用 “->”
引用自然出错了,因为没有结构体,自然没有结构体的域了。 此外,(*p).a
等价于 p->a
。
::
::
是作用域符,是运算符中等级最高的,它分为三种:
::name
class::name
namespace::name
他们都是左关联,他们的作用都是为了更明确的调用你想要的变量:
a
,那么就写成 ::a
;(也可以是全局函数)class A
中的成员变量 a
,那么就写成 A::a
;namespace std
中的 cout
成员,你就写成 std::cout
(相当于 using namespace std;cout
)意思是在这里我想用 cout
对象是命名空间 std
中的 cout
(即就是标准库里边的cout
);
- 表示“域操作符”:声明了一个类
A
,类A
里声明了一个成员函数void f()
,但没有在类的声明里给出f
的定义,那么在类外定义f时, 就要写成void A::f()
,表示这个f()
函数是类A
的成员函数。- 直接用在全局函数前,表示是全局函数:在 VC 里,你可以在调用 API 函数里,在 API 函数名前加
::
- 表示引用成员函数及变量,作用域成员运算符:
System::Math::Sqrt()
相当于System.Math.Sqrt()
;
int arr[] = {1,2,3};
int* p = arr
,指针 p 指向数组 arr 的首地址;*p = 6;
将 arr 数组的第一个元素赋值为 6;*(p+1) = 10;
将 arr 数组第二个元素赋值为 10;int* p[3];
for(int i = 0; i<3; i++){
p[i] = &arr[i];
}
复制代码
int (*p)[n]
优先级高,首先说明 p 是一个指针,指向一个整型的一维数组,这个一维数组的长度是 n,也可以说是 p 的步长。执行 p+1 时,p 要跨过 n 个整型数据的长度。
int a[3][4]
; int (*p)[4];
//该语句是定义一个数组指针,指向含 4 个元素的一维数组 p = a;
//将该二维数组的首地址赋给 p,也就是 a[0] 或 &a[0][0] p++;
//该语句执行后,也就是 p = p+1;
p 跨过行 a[0][] 指向了行 a[1][]
struct Person
{
char c;
int i;
char ch;
};
int main()
{
struct Person person;
person.c = 8;
person.i = 9;
}
复制代码
存储变量时地址要求对齐,编译器在编译程序时会遵循两个原则:
(1)结构体变量中成员的偏移量必须是成员大小的整数倍 (2)结构体大小必须是所有成员大小的整数倍,也即所有成员大小的公倍数
union Data
{
int i;
float f;
char str[20];
}data;
int main()
{
union Data data;
data.i = 11;
}
复制代码
char *pa, *pb;//传统写法
复制代码
typedef char* PCHAR; // 使用typedef 写法 一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
复制代码
struct
。以前的代码中,声明 struct
新对象时,必须要带上 struct
,即形式为: struct 结构名 对象名
:struct tagPOINT1
{
int x;
int y;
};
struct tagPOINT1 p1;
复制代码
//使用 typedef
typedef struct tagPOINT
{
int x;
int y;
}POINT;
POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候
复制代码
typedef
来定义与平台无关的类型:#if __ANDROID__
typedef double SUM;
#else
typedef float SUM ;
#endif
int test() {
SUM a;
return 0;
}
复制代码
//原声明:
int *(*a[5])(int, char*);
//变量名为a,直接用一个新别名pFun替换a就可以了:
typedef int *(*pFun)(int, char*);
//原声明的最简化版:
pFun a[5];
复制代码
.h
这里一般写类的声明(包括类里面的成员和方法的声明)、函数原型、#define常数等,但一般来说不写出具体的实现。写头文件时,为了防止重复编译,我们在开头和结尾处必须按照如下样式加上预编译语句:#ifndef CIRCLE_H
#define CIRCLE_H
class Circle
{
private:
double r;
public:
Circle();//构造函数
Circle(double R);//构造函数
double Area();
};
#endif
复制代码
至于
CIRCLE_H
这个名字实际上是无所谓的,你叫什么都行,只要符合规范都行。原则上来说,非常建议把它写成这种形式,因为比较容易和头文件的名字对应。
.cpp
源文件主要写实现头文件中已经声明的那些函数的具体代码。需要注意的是,开头必须 #include
一下实现的头文件,以及要用到的头文件。#include "Circle.h"
Circle::Circle()
{
this->r=5.0;
}
Circle::Circle(double R)
{
this->r=R;
}
double Circle:: Area()
{
return 3.14*r*r;
}
复制代码
main.cpp
来测试我们写的 Circle 类#include
#include "Circle.h"
using namespace std;
int main()
{
Circle c(3);
cout<<"Area="<
friend
。friend class 类名
(friend和class是关键字,类名必须是程序中的一个已定义过的类)。class INTEGER
{
private:
int num;
public:
friend void Print(const INTEGER& obj);//声明友元函数
};
void Print(const INTEGER& obj) //不使用friend和类::
{
//函数体
}
void main()
{
INTEGER obj;
Print(obj);//直接调用
}
复制代码
#include
using namespace std;
class girl
{
private:
char *name;
int age;
friend class boy; //声明类boy是类girl的友元
public:
girl(char *n,int age):name(n),age(age){};
};
class boy
{
private:
char *name;
int age;
public:
boy(char *n,int age):name(n),age(age){};
void disp(girl &);
};
void boy::disp(girl &x) // 该函数必须在girl类定义的后面定义,否则girl类中的私有变量还是未知的
{
cout<<"boy's name is:"<
operator
和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。#include
using namespace std;
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <
打印结果:
Volume of Box1 : 210 Volume of Box2 : 1560 Volume of Box3 : 5400
class derived-class: access-specifier base-class
;access-specifier
是 public
、protected
或 private
其中的一个,base-class
是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier
,则默认为 private
。private
。基类的构造函数、析构函数和拷贝构造函数。
基类的重载运算符。 基类的友元函数。
当一个类派生自基类,该基类可以被继承为 public
、protected
或 private
几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier
来指定的。
我们几乎不使用 protected
或 private
继承,通常使用 public
继承。当使用不同类型的继承时,遵循以下几个规则:
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
#include
using namespace std;
// 基类
class Shape
{
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
int main(void)
{
Rectangle Rect;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total area: " << Rect.getArea() << endl;
return 0;
}
复制代码
打印结果:
Total area: 35
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。 定义一个函数为纯虚函数,才代表函数没有被实现。 定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}
复制代码
virtual void funtion()=0
模板函数定义的一般形式如下所示:
template ret-type func-name(parameter list)
{
// 函数的主体
}
复制代码
#include
#include
using namespace std;
template
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
复制代码
打印结果:
Max(i, j): 39 Max(f1, f2): 20.7 Max(s1, s2): World
类模板,泛型类声明的一般形式如下所示:
template class class-name {
.
.
.
}
复制代码
#include
#include
#include
#include
#include
using namespace std;
template
class Stack {
private:
vector elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template
void Stack::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template
void Stack::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template
T Stack::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack intStack; // int 类型的栈
Stack stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <
打印结果:
7 hello Exception: Stack<>::pop(): empty stack
特点:
vector
头部与中间插入和删除效率较低,在尾部插入和删除效率高,支持随机访问。deque
是在头部和尾部插入和删除效率较高,支持随机访问,但效率没有 vector
高。list
在任意位置的插入和删除效率都较高,但不支持随机访问。set
由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复,且插入和删除效率比用其他序列容器高。map
可以自动建立 Key - value 的对应,key 和 value 可以是任意你需要的类型,根据 key 快速查找记录。选择:
vector
。list
。deque
。map
,一对多的情况使用 multimap
。set
,不唯一存在的情况使用 multiset
。时间复杂度:
vector
在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);deque
在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);list
在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);set
和 map
都是通过红黑树实现,因此插入、删除和查找操作的时间复杂度都是 O(log N)。namespace namespace_name {
// 代码声明
}
复制代码
namespace {
// 代码声明
}
复制代码
命名空间名::成员名 ……
定义方式,为命名空间添加新成员,而必须先在命名空间的定义中添加新成员的声明。using namespace
指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。#include
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();
return 0;
}
复制代码
using namespace
)外,还可以使用 using声明
来简化对命名空间中的名称的使用:using 命名空间名::[命名空间名::……]成员名;
。注意,关键字 using
后面并没有跟关键字 namespace
,而且最后必须为命名空间的成员名(而在 using
编译指令的最后,必须为命名空间名)。
using指令
使用后,可以一劳永逸,对整个命名空间的所有成员都有效,非常方便。而using声明
,则必须对命名空间的不同成员名称,一个一个地去声明。但是,一般来说,使用using声明
会更安全。因为,using声明
只导入指定的名称,如果该名称与局部名称发生冲突,编译器会报错。而using指令
导入整个命名空间中的所有成员的名称,包括那些可能根本用不到的名称,如果其中有名称与局部名称发生冲突,则编译器并不会发出任何警告信息,而只是用局部名去自动覆盖命名空间中的同名成员。特别是命名空间的开放性,使得一个命名空间的成员,可能分散在多个地方,程序员难以准确知道,别人到底为该命名空间添加了哪些名称。
.so
库;//MainActivity.java
static {
System.loadLibrary("native-lib");
}
复制代码
//MainActivity.java
public native String stringFromJNI();
复制代码
//native-lib.cpp
#include
#include
//函数名的构成:Java 加上包名、方法名并用下划线连接(Java_packageName_methodName)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cppdemo_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
复制代码
# CMakeLists.txt
# 设置构建本地库所需的最小版本的cbuild。
cmake_minimum_required(VERSION 3.4.1)
# 创建并命名一个库,将其设置为静态
# 或者共享,并提供其源代码的相对路径。
# 您可以定义多个库,而cbuild为您构建它们。
# Gradle自动将共享库与你的APK打包。
add_library( native-lib #设置库的名称。即SO文件的名称,生产的so文件为“libnative-lib.so”, 在加载的时候“System.loadLibrary("native-lib");”
SHARED # 将库设置为共享库。
native-lib.cpp # 提供一个源文件的相对路径
helloJni.cpp # 提供同一个SO文件中的另一个源文件的相对路径
)
# 搜索指定的预构建库,并将该路径存储为一个变量。因为cbuild默认包含了搜索路径中的系统库,所以您只需要指定您想要添加的公共NDK库的名称。cbuild在完成构建之前验证这个库是否存在。
find_library(log-lib # 设置path变量的名称。
log # 指定NDK库的名称 你想让CMake来定位。
)
#指定库的库应该链接到你的目标库。您可以链接多个库,比如在这个构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries( native-lib # 指定目标库中。与 add_library的库名称一定要相同
${log-lib} # 将目标库链接到日志库包含在NDK。
)
#如果需要生产多个SO文件的话,写法如下
add_library( natave-lib # 设置库的名称。另一个so文件的名称
SHARED # 将库设置为共享库。
nataveJni.cpp # 提供一个源文件的相对路径
)
target_link_libraries( natave-lib #指定目标库中。与 add_library的库名称一定要相同
${log-lib} # 将目标库链接到日志库包含在NDK。
)
复制代码
// build.gradle(:app)
android {
compileSdkVersion 29
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "com.example.cppdemo"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
复制代码
JavaVM
是 Java 虚拟机在 JNI 层的代表, JNI 全局只有一个;JNIEnv
是指向可用 JNI 函数表的接口指针,第二个参数 jobject
是 Java 函数所在类的实例的 Java 对象引用;JNIEnv
是 JavaVM
在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv
,同时 JNIEnv
具有线程相关性,也就是 B 线程无法使用 A 线程的 JNIEnv
;JNIEnv
类型实际上代表了 Java 环境,通过这个 JNIEnv*
指针,就可以对 Java 端的代码进行操作:调用 Java 函数; 操作 Java 对象;
JNIEnv
的本质是一个与线程相关的代表 JNI 环境的结构体,里面存放了大量的 JNI 函数指针;JNIEnv
内部结构如下:JavaVM
的结构如下:Signature格式 | Java | Native | Description |
---|---|---|---|
B | byte | jbyte | signed 8 bits |
C | char | jchar | unsigned 16 bits |
D | double | jdouble | 64 bits |
F | float | jfloat | 32 bits |
I | int | jint | signed 32 bits |
S | short | jshort | signed 16 bits |
J | long | jlong | signed 64 bits |
Z | boolean | jboolean | unsigned 8 bits |
V | void | void | N/A |
数组简称:在前面添加 [
Signature格式 | Java | Native |
---|---|---|
[B | byte[] | jbyteArray |
[C | char[] | jcharArray |
[D | double[] | jdoubleArray |
[F | float[] | jfloatArray |
[I | int[] | jintArray |
[S | short[] | jshortArray |
[J | long[] | jlongArray |
[Z | boolean[] | jbooleanArray |
对象类型简称:L+classname +;
Signature格式 | Java | Native |
---|---|---|
Ljava/lang/String; | String | jstring |
L+classname +; | 所有对象 | jobject |
[L+classname +; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
(输入参数...)返回值参数
Signature格式 | Java函数 |
---|---|
()V | void func() |
(I)F | float func(int i) |
([I)J | long func(int[] i) |
(Ljava/lang/Class;)D | double func(Class c) |
([ILjava/lang/String;)Z | boolean func(int[] i,String s) |
(I)Ljava/lang/String; | String func(int i) |
jclass thisclazz = env->GetObjectClass(thiz);//使用GetObjectClass方法获取thiz对应的jclass。
jclass thisclazz = env->FindClass("com/xxx/xxx/abc");//直接搜索类名
复制代码
/**
* thisclazz -->上一步获取的 jclass
* "onCallback"-->要调用的方法名
* "(I)Ljava/lang/String;"-->方法的 Signature, 签名参照前面的第 3.2 小节表格。
*/
jmethodID mid_callback = env->GetMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");
jmethodID mid_callback = env->GetStaticMethodID(thisclazz , "onCallback", "(Ljava/lang/String;)I");//获取静态方法的ID
复制代码
jint result = env->CallIntMethod(thisclazz , mid_callback , jstrParams);
jint result = env->CallStaticIntMethod(thisclazz , mid_callback , jstrParams);//调用静态方法
复制代码
贴一下JNI 常用接口文档,有需要可以在这里查询。
NewLocalRef
和各种JNI接口创建(FindClass
、NewObject
、GetObjectClass
和NewCharArray
等)。GetStringUTFChars
之后,调用 ReleaseStringUTFChars
释放;对于手动创建的 jclass
,jobject
等对象使用 DeleteLocalRef
方法进行释放。NewGlobalRef
基于局部引用创建。DeleteGlobalRef
来手动删除全局引用调用。NewWeakGlobalRef
基于局部引用或全局引用创建。DeleteWeakGlobalRef
手动释放。