这是NDK系列的第三章,将会学习C++语言的基础知识。C++是在C语言的基础上演变而来的一种语言,可以理解为C语言的基础上新增了一些优化内容。因此在C++项目中可以调用C语言的API,但在C语言中无法调用C++的API。
一、C++语言学习之面向对象
1.1 命名空间
所谓命名空间可以理解为Java 中的内部类,里面含有其自己特有的API,我们要使用其中的API可以有以下两种方式
#include <iostream> // C++标准支持 C++的与众不同
using namespace std; // 命名空间 C++ 的特性 (Java语言的内部类)
int main() {
// C++语言面向对象 + 标准特性
// C语言面向过程,函数+结构体
// C++里面可以运行C语言,可以调用C语言,反之 就不行C语言无法运行C++
// 以后我们85%以上 都是 用C++去写功能
// 小故事:谣言 C++-- 在C语言上增加好的内容
// C++里面可以运行C语言,可以调用C语言,反之 就不行C语言无法运行C++
printf("降龙十八掌(C版)\n");
// std::cout << "C++语言的学习" << std::endl;
cout << "C++语言的学习" << endl; // 因为你前面引入了命名空间,省略std::
std::cout << "不省略命名空间" << endl;
// endl == \n 都是换行的含义一样
// Kotlin也有操作符重载, Kotlin就是各个语言的精华所在
// << 不是属性里面的运算,操作符重载,后面会讲
cout << "擒龙功" << endl;
cout << "铁头功\n"
<< "金刚腿\n"
<< "铁布衫\n";
return 0;
}
运行结果:
命名空间
- 当在全局使用了命名空间的话,即可在程序任何位置使用这个命名空间中的相关API,同时省略命名空间的前缀。
- 也可以在函数里局部使用命名空间,格式为:XX :: XX(XX为相应的API)。
- 在C++项目中可以调用C语言相关的API(printf.....C语言项目但却无法调用C++相关API)。
- 同时需要注意的是引用库也发生了改变,但其实引用库的内容是一样的。
- 在 std 这个命名空间里,endl 的作用跟 \n 作用一样都是换行。
1.2 C 的常量与 C++的常量
在C语言与C++中都有常量的概念,然而C语言中的常量是假的,具体原因看以下这个demo:
1.2.1 C 的常量
#include <stdio.h>
// C语言的常量,其实是个 假常量(伪命题)
int main() {
const int number = 100;
// number = 200;
int * numP = &number;
*numP = 10000;
printf("%d\n", number);
return 0;
}
运行结果为:
1.2.2 C++的常量
在C++项目中的常量则是真正意义上的常量,编译器无法通过地址获取到常量的地址因此也就无法通过地址修改这个常量的值。
C++中的常量
1.3 引用的原理与常量引用
1.3.1 引用的原理
我们在之前的文章介绍过在C语言中通过指针获取地址交换两个值:
#include <iostream>
using namespace std;
// 互换两个数
#include <iostream>
using namespace std;
// 互换两个数
// 指针取地址 互换 C语言第一节课的内容
// 接收number1/number2的地址,取改地址的值,来完成的互换
void numberChange(int * number1, int * number2) {
int temp = 0;
temp = *number1;
*number1 = *number2;
*number2 = temp;
}
int main() {
int number1 = 10;
int number2 = 20;
numberChange(&number1, &number2);
cout << "n1:" << number1 << " , n2:" << number2 << endl;
return 0;
}
运行结果:
#include <iostream>
using namespace std;
// 互换两个数
#include <iostream>
using namespace std;
// 互换两个数
// C++提倡的引用
void numberChange2(int & number1, int & number2) {
// 如果不采用引用,main numberChange2 内存地址是不一样的
// 如果采用引用,main numberChange2 内存地址是一样的,为什么呢?
cout << "numberChange2 " << "n1地址:" << &number1 << " , n2地址:" << &number2 <<endl;
int temp = 0;
temp = number1;
number1 = number2;
number2 = temp;
}
int main() {
int number1 = 10;
int number2 = 20;
cout << "main " << "n1地址:" << &number1 << " , n2地址:" << &number2 <<endl;
numberChange2(&number1, &number2);
cout << "n1:" << number1 << " , n2:" << number2 << endl;
return 0;
}
运行结果:
可以看到使用了引用,传递到numberChange2函数中两个值的地址和在 main 函数中两个值的地址是一样的,所以这才导致在numberChange2函数中可以直接操作其两个值。而不需要通过指针获取其地址再去交换。
我们可以再看一个更明显的例子:
#include <iostream>
using namespace std;
int main() {
// 第一部分,不采用 &
int n1 = 999;
int n2 = n1;
cout << &n1 << "---" << &n2 << endl;
// 第二部分,采用&
int n3 = 999;
int & n4 = n3;
int & n9 = n3;
n3 = 777;
n9 = 9527;
cout << "地址:" << &n3 << "---" << &n4 << endl;
cout << "值:" << n3 << "---" << n9 << endl;
return 0;
}
运行结果:
引用demo
当使用了引用后,每个值的地址都是一样的。
这是因为当使用了引用后可以理解为是对某个对象取个别名而已。指向的还是原来那个对象。
1.3.2 常量引用
上面介绍了C++ 的常量是真正意义上的常量,不可以通过指针获取地址修改值,那么结合刚才的引用就是在外层传入的对象不可以被修改。我们假定插入数据库的场景,此时对于传入的数据是不允许被修改的,就可以用到 常量引用:
// 常量引用。
#include <iostream>
#include <string.h>
using namespace std;
// 代码的统一性
typedef struct {
char name[20];
int age;
}Student;
// 常量引用:Student不准你改 == const Student &
// 插入数据库,Student的信息给插入数据库
void insertStudent(const Student & student) {
// 内鬼 卧底
// strcpy(student.name, "李元霸"); 不能这样修改
//Student student2 = {"刘奋", 43};
// student = student2; 不能这样修改
// 只读的了,可以安心插入数据库了
cout << student.name << "," << student.age << endl;
}
int main() {
// 用户提交的Student数据
Student student = {"张无忌", 30};
insertStudent(student);
return 0;
}
运行结果:
1.4 函数重载与默认行参,无形参名的特殊写法
1.4.1 函数重载与默认行参
// C++语言的函数重载 是支持的
#include <iostream>
using namespace std;
// Java构造函数 50个字段,我真正使用的,只有6个
// 建造者设计模式 去解决此问题
int add(int number1) {
return number1;
}
int add(int number1, int number2) {
return number1 + number2;
}
// C++重载 == Java重载
int add(int number1, int number2, int number3) {
return number1 + number2 + number3;
}
// 函数重载 二义性
// 自己做实验
// 默认行参赋值, // KT也有默认行参赋值 优先寻找默认行参赋值的函数, 跟顺序无关
int add(double n1 = 100, int n2 = 200, int n3 = 300, int n4 = 400, bool isOK = 0) {
return 0;
}
int main() {
add(999);
add(999, 777);
add(100, 200, 888);
add(1,2,5,8);
return 0;
}
- 在Java中存在重载,在C++同样也存在重载,只要函数名一致,参数类型,参数顺序,参数数目不一致就属于重载。
- 同时类似于 Kotlin,C++同样提供了默认形参,不同于Javhzha ,当构造函数中含有很多字段时,需要每个都进行赋值,容易造成赋值错误,因此提供了建造者设计模式来解决这一问题,而C++则是通过 默认行参赋值来解决这一问题。
- C++ 中也是有布尔类型的,与C语言相同,C++中的布尔类型也是 非0 即 true,记住这一准则即可。
1.4.2 无形参名的特殊写法
// 系统源码里面大量的写法 [特殊写法,意义何在]
#include <iostream>
using namespace std;
// 前期先抽象出现,我后面再升级
// 后面你可以扩展
void JNIMethod(double, double, int, int) {
}
// 上层日志
// 我前期的时候,没有考虑好,为了防止扩展功能,你必须传递一个int类型参数
// 0:服务器同步, 1:友萌服务器同步 2:服务器同步+友萌服务器同步
void uploadLogToEngine(char * logText, int mode) {
// 普通上传日志
// 半年过后,我再来补这个功能
if (mode) {
} else if (mode == 1) {
}
}
int main(void) {
// 前面一个月开发功能的时候
uploadLogToEngine("xxxxx", 0); // 300出地方调用
uploadLogToEngine("xxxxx", 2); // 600出地方调用
uploadLogToEngine("xxxxx", 1); // 400出地方调用
JNIMethod(9.0 ,9.9, 1, 2);
return 0;
}
这个demo中的 JNIMethod 函数里每个形参都没有形参名,这是C++中抽象的写法。举一个栗子,当在前期开发时,先预留一个形参,前期用不到这个形参先不定义参数名,后期开发需要时再进行补全。
1.4 C++中的面向对象
C++中的面向对象思想主要涉及几个专业名词,我们先来解释一下:
- 头文件(.h) 类似于Java 中的接口
- 实现文件(.cpp)类似于Java 中实现接口的实现类
- 可执行文件(.cpp)函数的运行的入口
下面是一个完整的demo
//.h头文件
#include <iostream>
using namespace std;
// Student.h 头文件 只写声明,不写实现
class Student {
private: // 下面的代码(成员和函数),都是私有
char * name;
int age;
public: // 下面的代码(成员和函数),都是公开
// set get
void setAge(int age); // 声明函数
void setName(char * age); // 声明函数
int getAge(); // 声明函数
char * getName(); // 声明函数
};
我们之前demo都有引入iostream这个库,如果我们在头文件引入了的话,那么在实现文件,可执行文件都不需要再引入了。
//实现文件(.cpp)
#include "Student.h"
// 根据 Student.h 头文件 ,写实现
// 和实现头文件那个函数,没有任何关系,相当于另外一个函数
/*void setAge(int age) {
}*/
void Student::setAge(int age) { // 实现函数
// C++对象指向的是一个指针
// -> 调用一级指针的成员
this->age = age;
}
void Student::setName(char * name) { // 实现函数
this->name = name;
}
int Student::getAge() { // 实现函数
return this->age;
}
char * Student:: getName() { // 实现函数
return this->name;
}
因为是实现Student.h这个头文件,因此首先要跟引入库文件一样引入Student.h头文件,然后实现对应的方法,实现方式就像上面讲过一样的匿名空间一样,格式为:XX: :XX
最后是可执行文件:
// .cpp(可执行文件)
#include "Student.h"
int main() {
// 规范写法:要有 头文件.h .hpp -- 实现文件 .c cpp
// TODO ======= 下面是栈空间
Student student1; // 栈区开辟空间的
// 赋值
student1.setAge(99);
student1.setName("李连杰");
cout << "name:" << student1.getName() << " ,age:" << student1.getAge() << endl;
// TODO ======= 下面是堆空间
Student * student2 = new Student(); // new/delete
// 赋值
student2->setAge(88);
student2->setName("李元霸");
cout << "name:" << student2->getName() << " ,age:" << student2->getAge() << endl;
if (student2)
delete student2; // 必须手动释放堆空间的对象student2
student2 = NULL; // 指向NULL的地址区域
// free(student2); // 不能这样写,不规范,会被鄙视的
return 0;
} // main函数弹栈后,会释放栈成员 student1
运行结果:
c++面向对象 跟实现文件,也要导入头文件(Student.h)与C语言不同的是,C++中在堆中创建对象使用了 new关键字,释放内存使用 delete 关键字,同时也需要将指针指向 NULL 地址。
二、C++语言类中各个重要函数原理
第一节讲解了C++面向对象的基础知识,在这一节将介绍C++语言类中各个重要函数原理。
2.1 自定义命名空间
在第一节时介绍了命名空间的概念,其中 std 命名空间是 iostream 这个库里面的。上面自定义了C++的头文件,同样的C++也支持自定义命名空间。
// 命名空间
#include <iostream>
// 声明std,我们的main函数就可以直接使用里面的成员,不需要使用 std::
using namespace std; // C++自己的命名空间 (C# .net 命名空间)
// 自定义命名空间
namespace derry1 {
int age = 33;
char * name = "Derry猛男1";
void show() {
cout << "name:" << name << ", age:" << age << endl;
}
void action() {
cout << "derry1 action" << endl;
}
}
// TODO ------ 命名空间里面重复的函数
// 自定义命名空间
namespace derry2 {
void action() {
cout << "derry2 action" << endl;
}
}
// TODO ------ 小概率会遇到的情况,命名空间的嵌套
// 自定义命名空间
namespace derry3 {
namespace derry3Inner {
namespace derry3Inner1 {
namespace derry3Inner2 {
namespace derry3Inner3 {
void out() {
cout << "爱恨情仇人消瘦,悲欢起落人寂寞" << endl;
}
}
}
}
}
}
// 声明各个写的 命名空间
// using namespace derry1;
int main() {
cout << "自定义命名空间" << endl;
// 声明各个写的 命名空间
using namespace derry1;
int ageValue = derry1::age; // 方式1 使用 刚刚声明的命名空间
derry1::show(); // 使用 刚刚声明的命名空间
ageValue = age; // 方式2 直接去引出来 ::
show(); // 直接去引出来 ::
// TODO ------ 命名空间里面重复的函数
using namespace derry2;
// action(); 很尴尬
derry1::action();
derry2::action();
// TODO ------ 小概率会遇到的情况,命名空间的嵌套
// 第一种方式 先声明命名空间 再使用
using namespace derry3::derry3Inner::derry3Inner1::derry3Inner2::derry3Inner3;
// 再使用
out();
// 第二种方式 直接使用
derry3::derry3Inner::derry3Inner1::derry3Inner2::derry3Inner3::out();
return 0;
}
运行结果:
自定义命名空间 上面demo列举出了如何自定义命名空间。注意:当两个命名空间内出现函数名相同的情况时,需要带上指定命名空间一起使用。
2.2 构造函数详解
与Java类似,在C++中每个类中也有自己的构造函数,通过下面这个demo来介绍一下:
// 1.构造函数详讲, 2.析构函数, 3.Java/KT的所谓的析构函数。
#include <iostream>
#include <string.h>
using namespace std;
// 构造函数
class Student {
public:
// 空参数构造函数
Student() {
cout << "空参数构造函数" << endl;
}
// 一个参数的构造函数
Student(char *name) : name(name) {
cout << "一个参数的构造函数" << endl;
}
//两个参数的构造函数
Student(char *name,int age){
this->name = name;
this->age = age;
cout << "两个参数的构造函数" << endl;
}
// 私有属性
private:
char *name;
int age;
public:
int getAge() {
return this->age;
}
char *getName() {
return this->name;
}
void setAge(int age) {
this->age = age;
}
void setName(char *name) {
this->name = name;
}
};
int main() {
// =========== 下面是栈区 开辟空间的
Student stu; // 调用 空参数构造函数
return 0;
}
运行结果:
空参构造函数
此时只是执行了空参构造函数,并没有对象一说,下面接着执行set 函数,然后再通过 get 函数 打印出结果:
Student stu; // 调用 空参数构造函数
stu.setAge(34);
stu.setName("李元霸");
cout << "name:" << stu.getName() << ", age:" << stu.getAge() << endl;
运行结果:
执行set函数
除了这种之外还有两个参数的构造函数:
Student1 stu("雄霸", 30);
cout << "name:" << stu.getName() << ", age:" << stu.getAge() << endl;
运行结果:
两个参数构造函数
在上面的demo中看到类似于Java的this 方法赋值之外,C++还提供了额外一种语法糖,快捷的设置this 函数:
// 一个参数的构造函数
Student1(char *name) : name(name) {
cout << "一个参数的构造函数" << endl;
}
通过这种方式也可以实现跟 this 函数一样的效果。
同样,C++也有跟Java一样的重载函数:
// 一个参数的构造函数
Student1(char *name) : Student(name, 87) {
cout << "一个参数的构造函数" << endl;
}
当我们这样写时,当调用一个参数构造函数时,会先调用两个参数构造函数,执行完成之后再调用两个参数构造函数。
Student stu("李连杰");
cout << "name:" << stu.getName() << ", age:" << stu.getAge() << endl;
运行结果:
函数重载以上的这几种调用方式都是在栈区开辟空间,当需要在堆区开辟空间时就需要使用 new 关键字了:
// new/delete
// C++中,必须使用 new/delete 一套
Student1 *stu = new Student1("杜子腾", 26);
cout << "name:" << stu->getName() << ", age:" << stu->getAge() << endl;
delete stu;
运行结果:
堆区开辟空间 需要注意的是,在堆区开辟空间的对象必须手动释放,否则内存占用越来多。而于C不同的是,C++ 新建一个对象使用的是 new 关键字,释放使用的 delete。2.3 析构函数
在C++中的析构函数则类似于Java 中的 finalize 方法。作为函数临终遗言,当对象被回收了,做一些释放工作。
//两个参数的构造函数
Student(char *name, int age) {
this->name = (char *) (malloc(sizeof(char *) * 10));
strcpy(this->name,name);
this->age = age;
cout << "两个参数的构造函数" << endl;
}
~Student1() {
cout << "析构函数" << endl;
// 必须释放 堆区开辟的成员
if (this->name) {
free(this->name);
this->name = NULL;
// 执行NULL的地址,避免出现悬空指针
}
}
int main {
Student *stu = new Student1("杜子腾", 26);
cout << "name:" << stu->getName() << ", age:" << stu->getAge() << endl;
delete stu;
}
运行结果:
析构函数 首先在堆区中开辟出 name 空间,然后在执行 delete 函数时,执行析构函数,在析构函数中对 name 对象进行释放操作。有一点需要注意的,不能用 C语言中的 free进行释放,因为执行 free 时并不会执行 析构函数。完整demo:
// 1.构造函数详讲, 2.析构函数, 3.Java/KT的所谓的析构函数。
#include <iostream>
#include <string.h>
using namespace std;
// 构造函数
class Student1 {
public:
// 空参数构造函数
Student1() {
cout << "空参数构造函数" << endl;
}
// 一个参数的构造函数
Student1(char *name) : Student1(name, 87) {
cout << "一个参数的构造函数" << endl;
}
//两个参数的构造函数
Student1(char *name, int age) {
this->name = (char *) (malloc(sizeof(char *) * 10));
strcpy(this->name,name);
this->age = age;
cout << "两个参数的构造函数" << endl;
}
//析构函数 Student对象的,临终遗言,Student对象被回收了,你做一些释放工作
// delete stu 的时候,我们的析构函数一定执行
// free不会执行析构函数,也意味着,你没法在析构函数里面,做释放工作, malloc也不会调用构造函数
~Student1() {
cout << "析构函数" << endl;
// 必须释放 堆区开辟的成员
if (this->name) {
free(this->name);
this->name = NULL;
// 执行NULL的地址,避免出现悬空指针
}
}
// 私有属性
private:
char *name;
int age;
public:
int getAge() {
return this->age;
}
char *getName() {
return this->name;
}
void setAge(int age) {
this->age = age;
}
void setName(char *name) {
this->name = name;
}
};
int main() {
// TODO =========== 下面是栈区 开辟空间的
/* Student1 stu; // 调用 空参数构造函数
stu.setAge(34);
stu.setName("李元霸");
cout << "name:" << stu.getName() << ", age:" << stu.getAge() << endl;*/
// Student1 stu("雄霸", 30);
// Student1 stu("李连杰");
// cout << "name:" << stu.getName() << ", age:" << stu.getAge() << endl;
// TODO =========== 下面是堆区 开辟空间的 堆区必须手动释放,否则内存占用越来
// 系统源码中,会看到,很多使用 new 关键字
// *stu ->:调用一级指针的成员
// new/delete
// C++中,必须使用 new/delete 一套
Student1 *stu = new Student1("杜子腾", 26);
cout << "name:" << stu->getName() << ", age:" << stu->getAge() << endl;
delete stu;
// free(stu); 这样写是不规范的,不按人家规则来
// 还有人这样写 (C工程师,搞了六年,改不了自己的习惯, malloc)
// malloc 你的构造函数都没有调用,这个不行的
/*Student *stu2 = (Student*) malloc(sizeof(Student));
free(stu2);*/
return 0;
}
2.4 拷贝构造函数
我们首先来看一个demo:
struct Person {
int age;
char *name;
};
// TODO = 号的意义 隐士代码,引出 拷贝构造函数
int main() {
Person person1 = {100, "张三丰"};
// = 你看起来,没有什么特殊,隐士的代码:你看不到 C/C++编译器 会把p1的成员值赋值给p2成员
Person person2 = person1;
cout << &person1 << endl;
cout << &person2 << endl;
cout << person2.name << ", " << person2.age << endl;
// 思考:对象 对象1=对象2 默认的 拷贝构造函数
return 0;
}
我们可以先思考一下运行结果是什么,为什么是这样?运行结果:
拷贝构造函数 可以看到,两个对象的地址不一样,但是值是一样的,只是执行了一个赋值操作,也就是 =,我们这一节要讨论的就是 = 里面做了什么操作。在每个类里面都有一个默认的拷贝构造函数,隐式存在的,我们看不到,当我们执行赋值操作时,就会调用到这个函数,同时给对象赋值。
#include <iostream>
#include <string.h>
using namespace std;
class Student {
public:
Student() { cout << "空参数构造函数" << endl; }
// 两个参数的构造函数
Student(char *name, int age) : name(name), age(age) {
cout << "两个参数构造函数" << endl;
}
// 析构函数
// ~Student(char * name) { } 这样写,就不是析构函数了,如果你这样写,C/C++编译器对你很无语
~Student() {
cout << "析构函数" << endl;
}
// 以前是默认有一个拷贝构造函数,stu2 = stu1 默认赋值 【隐士 你看不到】
// 拷贝构造函数,它默认有,我们看不到,一旦我们写拷贝构造函数,会覆盖她
// 对象1 = 对象2
// 覆盖拷贝构造函数
Student(const Student & student) { // 常量引用:只读的,不让你修改
cout << "拷贝构造函数" << endl;
// 我们自己赋值
// 为什么要自己赋值,自己来控制,就可以 例如:-10
this->name = student.name;
this->age = student.age - 10;
}
// 私有属性
private:
char *name;
int age;
// 公开的 set get 函数
public:
int getAge() {
return this->age;
}
char *getName() {
return this->name;
}
void setAge(int age) {
this->age = age;
}
void setName(char *name) {
this->name = name;
}
};
// TODO 拷贝构造函数
int main() {
Student stu1("李鬼", 34);
Student stu2 = stu1;
cout << stu2.getName() << " , " << stu2.getAge() << endl;
return 0;
}
运行结果:
自定义拷贝构造函数
至于我们为什么要自定义拷贝构造函数,我们可以在这里面做一下我们想要的操作,就像上面的demo,我们可以在赋值的过程中对一些值进行操作,有点类似于代理模式中,在代理层做一些自定义的操作。
以下是一些注意的点:
Student stu1("李鬼", 34);
Student stu2;
stu2 = stu1;
运行结果:
调用方式注意点-1
当使用这种方式执行时,是不会调用到拷贝构造函数的,但会执行默认的赋值操作。
2.5 指针常量 常量指针 常量指针常量
这几个概念我们通常都会被网络上许多文章中的介绍给讲得头脑发热,我们可以采用大道至简的方式自己学习一下:const 代表常量, * 代表指针,那么连一起就是常量指针,同理,反过来即是指针常量
// 指针常量 常量指针 常量指针常量
#include <iostream>
#include <string.h>
#include <string.h>
using namespace std;
int main() {
// *strcpy (char *__restrict, const char *__restrict);
// strcpy()
int number = 9;
int number2 = 8;
// 大道至简 一分钟搞定
// 常量指针
const int * numberP1 = &number;
// *numberP1 = 100; // 报错,不允许去修改【常量指针】存放地址所对应的值
// numberP1 = &number2; // OK,允许重新指向【常量指针】存放的地址
// 指针常量
int* const numberP2 = &number;
*numberP2 = 100; // OK,允许去修改【指针常量】存放地址所对应的值
// numberP2 = &number2; // 报错,不允许重新指向【指针常量】存放的地址
// 常量指针常量
const int * const numberP3 = &number;
// *numberP3 = 100; // 报错,不允许去修改【常量指针常量】存放地址所对应的值
// numberP3 = &number2; // 报错,不允许重新指向【常量指针常量】存放的地址
return 0;
}
- 常量指针:只读,不允许修改值,允许重新指向地址
- 指针常量:只写,允许修改值,不允许重新指向地址
- 常量指针常量:不允许修改值,也不允许重新指向地址
网友评论