0x03 C++
0. C++的诞生
- Bjarne Stroustrup 1983
1. C++的特性
- const 的新用法
- C++的const是真的常量,可以用于设置数组的长度
- const常见的用法
- 定义常量: const int PI = 3.14;
- 用作修饰函数的参数,确保不被修改,参考strstr
- 函数返回一个常量。
- 修饰当前对象的内容(成员函数)不能被修改。int get() const;
- const 修饰的是当前对象this(ecx+偏移)
- 引用类型
- 引用类型的定义: 类型 & 引用名称 = 被引用对象;
- 引用和指针的关系
- 引用必须初始化,非常量指针不用初始化。
- 引用一经初始化,不能指向其它变量。非常量指针可以。 (int * const p)
- 使用引用可以直接访问被引用对象,指针需要通过(*)间接的访问。
- 在实现上,引用和指针完全相同,推荐使用引用。
- 函数的重载
- 函数重载的底层实现依赖于编译环境提供的名称粉碎机制,本质上就是编译器为每进一个函数维护了一个只被编译器识别的名称
- 函数重载的目的:静态联编多态
- 通过一个函数名称可以传入不同的参数调用不同的函数
- 函数重载的要求
- int show(int);
- 函数的名称及作用域相同
- 函数的参数个数不同
- int show(int, double);
- 函数的参数个数不同
- 函数的参数类型不同
- int show(double);
- 函数的参数类型不同
- 函数的参数顺序不同
- int show(double, int);
- 函数的参数顺序不同
- 函数的返回值不是函数重载的要求之一
- void show(int); 不可以
- 函数的返回值不是函数重载的要求之一
- 函数的默认参数
- 声明函数的默认参数
- 当只存在函数定义的时候,直接写在函数定义中
- 当同时存在定义和声明的时候,只能写在其中的一个中,推荐写在声明中。
- 使用默认参数的要求
- 默认参数的顺序必须是从右到左不能断开
- 声明函数的默认参数
- C++中堆的使用(new/delete)
- 申请堆空间: new / new[]
- 释放堆空间 delete / delete[]
- C语言和C++中使用堆的区别
- new/delete会调用构造和析构,但是malloc/free 不会调用
- new 的单位是元素个数 new int[15]; ,malloc的单位是字节 malloc(sizeof(int) *15)
- new 的返回值是类型的指针,但是malloc的返回值始终是void*
- new/delete是运算符(关键字),malloc和free是库函数
- new/delete的底层实现通常都是 malloc/free
- C++中的类型转换
- static_cast:静态类型转换
- const_cast: 常量转换,用于传参的时候
- reinterpret_cast : 通常用于指针间的转换
- dynamic_cast:通常用于子类对象和父类对象的转换(多态)
- 注释:除 dynamic_cast 之外,所有的转换都可以使用 () 代替
2. 输入和输出
- 使用输入函数 iostream
- std::cin >> 变量1 >> 变量2;
- (std::cin.operator>>(变量1)).operator>>(变量2);
- 使用输出函数 iostream
- std::cout << 变量1 << 变量2 << std::endl << std::ends;
- 格式化输出数据 iomanip
- setfill: 设置填充数据
- setw: 设置宽度
- hex: 十六进制显示
- 推荐使用printf格式化输出
3. 类的定义与使用
- 面向对象的三大特性:
- 封装、继承、多态
- 三大权限:
- public: 公有的,可以直接在外部访问
- protected: 保护的,可以被继承,但是不能在外部访问
- private: 不能被继承,且不能再外部访问
- 成员函数和数据:
- 数据的定义
- 再类内定义的数据都是数据成员,作用域是当前类
- 成员函数的定义
- 在类内编写声明和实现
- 在类内声明,在类外实现(类外实现必须加上类域)
- 在单独的头文件中声明,并且在单独的cpp中实现
- 数据的定义
- 静态函数和数据:
- 静态数据:
- 静态数据的定义: 在类内数据的定义前使用static关键字进行声明
- COBJ 类内的 static int n; 不能初始化的
- 必须要在类外进行初始化,否则报错: 无法解析的外部符号
- int COBJ::n = 0;
- 静态数据:
- 静态函数:
- 静态函数的定义就是在函数前使用static,静态函数没有this指针,并且不能访问非静态函数以及非静态成员 , 通常使用静态函数访问静态成员。
- 访问静态数据的方法:
- 直接用对象访问 TEST test; test.static_int;
- 直接用类名访问(推荐) TEST::static_int
- 使用初始化列表:
- 初始化列表用于对类内的数据进行初始化,只能写在构造函数中
- 引用必须在初始化列表内初始化
- 常量必须在初始化列表内初始化
- 没有无参构造的成员对象必须在初始化列表内初始化
- 没有无参构造的父类引用必须在初始化列表内初始化
- 初始化列表用于对类内的数据进行初始化,只能写在构造函数中
- 结构体和类的区别
- class默认的继承方式是private,struct默认继承方式是public
- class默认的访问属性是private,struct默认访问属性是public
4. 构造和析构
- 默认构造(析构\拷贝构造)函数(赋值运算符,取地址符)
- 默认的构造函数没有参数,没有实现任何内容
- TEST() = defult; // delete
- 默认的析构也没有任何的实现。
- ~TEST() = defult;
- 默认的构造函数没有参数,没有实现任何内容
- 无参构造函数
- 特点: 没有参数,通常用于进行默认初始化
- 有参构造函数(构造函数可以重载,可以设置默认值)
- 特点:需要传入的参数个数大于1,
- 转换构造函数
- 特点:需要传入一个参数的构造就是转换构造
- 用途:将其他类型的值转换成当前类型,基本不用
- explicit关键字: 禁止隐式的调用转换构造函数
- 拷贝构造函数
- 特点: 只有一个参数,是当前类对象的引用
- 使用当前类对象的引用是为了防止递归调用。
- OBJ(OBJ & obj);
- OBJ(const OBJ & obj);
- 拷贝构造的调用时机
- 使用同类型的对象进行初始化的时候
- 将对象作为值传递的时候
- 将对象作为值 返回的时候
- 使用同类型的对象进行初始化的时候
- 深拷贝和浅拷贝
- 浅拷贝: 默认生成的拷贝构造函数是浅拷贝,浅拷贝就是指针的值拷贝
- 深拷贝: 深拷贝就是指针的内存拷贝
- 可能会用到的函数(深拷贝必须涉及到内存的拷贝):
- memcpy
- strcpy
- _strdup
- new \ malloc
- 可能会用到的函数(深拷贝必须涉及到内存的拷贝):
- 析构函数
- 特点: 析构函数只能有一个,没有返回值,没有参数,名字是~类名();
- 用途: 对象销毁的时候自动调用一次,通常用于清理内存关闭句柄或文件。
- 析构函数应该被设置成虚函数,目的是防止子类对象的析构函数不被调用
5. 友元特性
- 友元: friend
- 破坏封装性,为了访问其它类中的私有数据。
- 友元的声明应该写在类内,也就是主人家里,主人要求客人。
- 友元类
- 友元类的声明同样写在主人类里面
- friend class XXX;
- 缩写自: class XXX;(类外) friend XXX;(类内)
- friend class XXX;
- 友元类的声明同样写在主人类里面
- 友元函数
- 友元函数是不是成员函数?不是,通产是一个全局函数
- 使用友元函数可以访问到类内的所有私有数据
- friend void show(TEST &test);
- 友元成员函数
- 指定某一个类的某一个函数能够访问当前类的所有私有数据
- friend void 类名:show(TEST &test);
- 使用十分的复杂,封装性高于友元类和友元函数
- 指定某一个类的某一个函数能够访问当前类的所有私有数据
6. 单继承和多继承
- 继承方式
- public: 公有继承下,除了私有属性,其余在子类中保持不变
- protected: 保护继承下,除了私有属性,其余全都变成保护的
- private:私有继承下,除了私有属性,全都变成私有的
- 单继承中的重定义
- 单继成的语法: class 当前类 : 继承方式 父类 { };
- 当子类中的变量或函数名和父类中的重复,那么子类中的变量或函数会隐藏父类中的数据,(重定义)
- 想要访问父类中的同名数据,需要使用作用域
- child.Base::number; 加载数据的前面
- 想要访问父类中的同名数据,需要使用作用域
- 多继承中的二义性
- 多继承的语法: class 当前类名: 访问属性 父类1, 访问属性 父类2 …. { };
- 当多个父类中有同名的数据,在子类中访问时,会导致调用不明确
- 解决方法是在使用时,添加父类的作用域.
- 缺点:同样的数据在内存中保存有两份,同样的数据越多,浪费的内存越大。
- 菱形继承中的二义性
- 将多继承中的公有数据放入到新创建的爷爷类中,继续继承会产生菱形继承
- 解决方法:
- 不要写出这样的代码
- 使用作用域进行访问
- 采用虚继承的方式
- 使用虚继承
- 用于解决菱形继承中产生的二义性问题。
- 虚基表和虚基表指针
- 当存在虚继承时,有多少个父类进行了虚继承,就会产生多少个虚基表指针
- 虚基表指针指向的是对应的虚基表,虚基表中保存的是距离爷爷类的偏移。 ```cpp class Yeye { public: int number = 0x66666666; };
// 父亲1类虚继承自爷爷类 class Base1 : public virtual Yeye { // 关键字 virtual 放置的位置可以在 public 的前后 int numberA = 0x11111111; };
// 父亲2类虚继承自爷爷类 class Base2 : virtual public Yeye { int numberB = 0x22222222; };
// 子类的继承不做修改 class Child : public Base1, public Base2 { int numberC = 0x33333333; };
int main() { Child c; c.number; // 虚继承后,爷爷只有一份,访问到的就是爷爷类中的number return 0; }
0x001DFE3C 00b37b48 H{ ? . 0x00B37B48 00000000 …. // 保存的是Base1到爷爷类的偏移 0x00B37B4C 00000014 …. 0x001DFE40 11111111 …. 0x001DFE44 00b37b54 T{ ? . 0x00B37B54 00000000 …. // 保存的是Base2到爷爷类的偏移 0x00B37B58 0000000c …. 0x001DFE48 22222222 “””” 0x001DFE4C 33333333 3333 0x001DFE50 66666666 ffff
<a name="y55sy"></a>
## 7. 多态和虚函数
---
- **C++中的多态**
- **函数重载(静态联编)**
- **函数重定义(静态联编)**
- **模板(静态联编)**
- **虚函数(动态联编)**
- **动态联编和静态联编**
- **动态联编:**在程序运行的时候确定调用的是哪一个函数
- 使用动态联编的要求
1. 使用对象或者引用调用虚函数就会产生动态联编
- **静态联编:**在程序编译时就确定了调用的是哪一个函数
- **虚函数的定义**
- 在普通成员函数的前面加上 virtual 关键字
- 实现起来和普通的函数是一样的
- _**使用父类指针指向不同子类,调用的是对应子类类型的虚函数****,**_构成了多态
- 子类重写父类的虚函数时,可以不添加 virtual 关键字,虚函数是向下继承的,也就是说子类中额外添加的的虚函数在父类中不存在
- **虚函数表和虚表指针**
- 当一个类中拥有虚函数时,就会存在虚函数指针
- 虚函数指针的数量由存在虚函数的父类决定
- 假设继承三个类,其中两个类有虚函数,那么有两个虚表指针
- 关于虚函数表
- **每个类都有对应的虚函数表,相同的类对象,使用的是同一张虚函数表**
- 虚函数表在构造函数内被初始化(深入探索C++对象模型)
- 虚函数表内存储的是什么?
1. 父类的虚函数(纯虚函数)
2. 子类重写的虚函数
3. 子类多写的虚函数(子类中多添加的虚函数被添加在第一张虚函数表内)
- **虚析构函数的使用**
- 为什么有虚析构函数?
- 答: 如果不存在虚析构函数,那么当父类指针指向子类对象的时候,释放父类指针,只会调用父类的析构函数,可能导致子类对象的内存泄漏等问题。
- 虚析构函数的目的是为了防止子类对象的析构函数不被调用。
- **纯虚函数和抽象类**
- **抽象类:** 有纯虚函数的类就是抽象类
- 抽象类不能被实例化,如果一个类继承自抽象类,没有实现它的所有纯虚函数,那么它仍然是一个抽象类。
- **纯虚函数:**用于定义一个必须要子类实现的函数
- 语法: 虚函数 = 0;
<a name="wYkAc"></a>
## 8. 运算符重载
---
- ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22743586/1640224824727-fd04b457-fe7a-4d24-ae47-0cfcf7a99bcf.png#averageHue=%23f7f6f4&clientId=ubf2049d8-0e70-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=589&id=ubabd2f57&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1098&originWidth=1044&originalType=binary&ratio=1&rotation=0&showTitle=false&size=106671&status=done&style=none&taskId=u443943f2-1ace-424b-b499-05b1dedb059&title=&width=560)
- **不能被重载的运算符**
- :: 、:? 、. 、sizeof
- **只能被重载为非静态成员函数的运算符**
- =,[ ],(),->*
- **使用成员函数重载和使用友元函数重载**
- 成员函数重载运算符,参数最少为0个
- 友元函数重载运算符,参数最少为1个
- **需要特殊对待的运算符**
- **自增自减: int operator++(int); 后置++ ,括号内有int 的是后置++**
- 流运算符: << >>, 通常被重载为友元函数
- cin >> xxxx; xxx >> cin;(不推荐的,修改了运算符的意义)
- 返回值和参数是引用类型,原因是 istream/ostream的拷贝构造被删除了
- xxxx(const ostream& o) = delete; **了解
**
<a name="XpHBu"></a>
## 9. 函数模板和类模板
---
- **模板的使用:**模板用于实现类型不同但是逻辑相同的类和函数
- template [class | typename]
- class 和 typename 没有区别
- **函数模板的使用**
- 函数的寻找优先级: 普通函数 > 特化函数 > 模板函数
- 函数模板和模板函数:
- 函数模板:是一个模板,不会生成具体的代码,只提供逻辑
- 模板函数:当一个函数模板被使用的时候,会检查参数,并根据传入的参数实例化出一个模板函数,模板函数会生成实际的代码;
- 函数模板有全特化,但是没有偏特化,函数模板的偏特化就是函数的重载。
- 特化的目的是为了对一些特殊的类型进行特殊的操作,比如为字符串比较大小,对类对象进行排序操作。
- **类模板的使用**
- 类模板可以全特化也可以偏特化
1. 全特化就是特化所有的类型
2. 偏特化就是特化部分的类型
3. 类模板在定义对象的时候必须提供类型 vector< int>
- **类模板的实现和定义必须要放在同一个文件,否则会报错**
- 当在类模板外实现函数时,需要重新的指定模板关键字和参数
```cpp
- ```cpp
template <class T>
class Test
{
public:
T number;
T operator-();
};
template <class T>
// 需要加上T表示当前是一个模板
T Test<T>::operator-()
{
return -number;
}
int main()
{
// 使用模板类需要指定类的类型
Test<int> test;
return 0;
}
**不管是函数模板还是类模板的特化都必须写在模板的下面**
10. 重载重写和重定义*
- 重载(函数重载)
- 作用域相同,参数不同,名称相同。
- 函数重载和运算符重载
- 底层实现是 名称粉碎
- 重写 override(虚函数实现多态)
- 作用域不同,必须存在虚函数,参数和返回值需要一致。
- 重定义(子父类同名函数,子类的函数覆盖父类的)
- 作用域不同, 参数可以相同也可以不同,子类的同名函数会隐藏父类的函数。
- 二义性(多继承)
- 情况1:子类的两个父类中两个函数同名,不知调用哪个,造成二义性
- 更改函数名
- 在使用的函数前加 {类名::}来明确使用的父类
- 情况2:子类继承了两个父类,两个父类继承自同一个爷爷类。造成二义性
- 使用虚继承来解决:将父类的继承都加上虚继承
- 情况1:子类的两个父类中两个函数同名,不知调用哪个,造成二义性
11. 命名空间的使用
- 命名空间的作用
- 命名空间用于解决变量名冲突的问题
- 访问命名空间中的数据
- 添加作用域: std::cout
- 使用 using namespace std; cout
- 缺点是,可能会造成名称的冲突
- 使用 using namespace std; cout
- 推荐使用: using std::cout:
- 缺点是太长了
- 推荐使用: using std::cout:
12. STL库的使用
- vector: 动态数组(顺序表) STL 源码剖析
- push_back
- pop_back
- at,安全版本的 [ ]
- erase 迭代器
- insert 迭代器
- list: 双向循环链表
- 它没有重载[]运算符,原因是链表不能动态存取
- map: 红黑树(不那么平衡的平衡二叉树)
- 键值对,相同的键只有一个,后面的值会覆盖前面的值,可以使用中括号访问值
- string: 字符串,C++推荐使用
- +: 拼接字符串,没有 改变原有的值
- +=: strcat,拼接字符串
- =: strcpy 拷贝字符串
- .length(): strlen() 获取字符串的长度
- append: 追加字符串
13. 异常处理
- try: 包含的是可能产生异常的代码 ,用于捕获异常
- throw : 用于主动的抛出异常,RaiseException->KiDispatchException(分发函数)
- catch: 用于获取对应类型的异常,可以有多个catch,如果最后一个catch内参数是…表示捕获所有类型的异常