学习视频参考
1 面向对象的三大特征
1.1 封装
1.1.1 目的
隐藏实现细节,实现模块化。
1.1.2 特性
访问权限:
- public:可以给所有对象访问。
- protected:仅对子类开放。
- private:仅对自己开放,可以通过友元进行访问。
能够对属性和方法进行限定。
1.2 继承
1.2.1 目的
在不修改原有类的情况下,通过继承实现功能的扩展。
1.2.2 特性
解决基类成员在子类中的最高权限
- public:基类中的属性和方法原来是什么权限,在子类就还是什么权限。
- protected:基类中的public权限在子类中会变成protected权限。
- private:基类中的public和protected权限会变成private权限。解决基类成员在子类中的最高权限
注意基类的private权限在子类中存在但不能访问。
子类可以通过using修改基类在子类中的权限,这样就可以访问基类中private权限下的属性和方法。
多继承,C++可以继承多个父类。
接口继承,存虚函数。
1.3 多态
1.3.1 目的
一个接口多种形态,通过实现接口重用,增强可扩展性。
1.3.2 特性
静态多态
- 指编译器的多态。
- 函数重载:同样的函数名不同的参数。
动态多态
- 指运行时的多态。
- 通过虚函数重写的方式实现。
- 基类中有虚函数,子类通过重写基类的虚函数来实现多态。
- 基类* p = new 子类;通过这种方式实现多态。
2 多态的实现原理
2.1 静态多态
2.1.1 函数重载
- 允许同一作用域中声明多个功能类似的同名函数。
- 其参数列表,参数个数,参数类型,参数顺序有所不同。
- 不能通过返回值类型来区别函数重载。
2.1.2 原理
编译过程中
- 预编译:把头文件中的函数声明拷贝到源文件当中去。避免编译过程中的语法分析找不到函数定义
- 编译:语法分析,同时进行符号汇总。
- 汇编:生成函数名和函数地址之间的映射。方便之后通过函数名找到函数定义位置,从而执行函数。
- 链接:将多个文件中的符号表汇总合并。
通过函数名修饰达成函数重载的目的。
函数名修饰:在符号汇总中,相同函数名在符号表中的符号是不同的一般是函数名+有规则的前后缀。所以是编译器的多态。
2.2 动态多态
2.2.1 虚函数重写
虚函数重写,运行时确定。
- 在基类的函数前+virtual关键字,在基类中重写该函数。
- 运行时会根据对象的类型来调用相应的函数。
- 如果对象的类型是基类,就调用基类的函数。
- 如果对象的类型是子类,就调用子类的函数。
2.2.2 原理
- 早绑定:编译器编译时已经确定了调用对象的函数地址。
- 晚绑定:使用virtual函数,为类生成虚函数表—一个一维数组,存放了虚函数地址。类对象构造时会初始化该虚函数表指针。其中,虚函数表指针放在类的最前面的8个字节,指向虚函数表,虚函数表中存放类的虚函数地址。
3 菱形继承
3.1 什么是菱形继承
- 前提:C++具备其他语言所没有的多重继承特性。
- 原因:一个子类可以继承多个父类,而这些父类可能继承至相同的父类,这样就会造成菱形继承。
3.2 菱形继承有什么样的问题
一个类C,继承B1,B2,而B1,B2又继承至同一个类A。
那么构造类C的时候,我们的B1,B2是分别构造了自己的类A还是构造了不同的类A。也就是C的背后是有一个A还是有两个A?
一般情况下是构造了两个类A 这样就出现了如下的问题:
- 造成存储空间的浪费。
- 二义性。
3.3 解决方案
虚继承:
- 目的:使得子类C只继承一次A。
- 方法:继承的时候带上virtual关键字。
- 原理:通过虚表偏移来实现虚继承。父类的虚指针都有到共同基类的偏移量,从而让子类多继承时能指向同一个 父类的父类。
感觉这个菱形继承一般不会有什么人用吧,毕竟其他语言也没有多继承这种东西。
4 override和final
C++11中引入这两个关键字
4.1 引入原因
- 虚函数复写:1、不能阻止某个虚函数的进一步复写。2、本意是写一个新函数,结果复写了基类的虚函数。3、本意是重写虚函数,但是由于签名不一样导致子类中重新构建了一个虚函数。
- 类继承:不能阻止某个类的进一步派生。
4.2 override
- 指定子类的一个虚函数复写基类的一个虚函数。
- 保证该重写的虚函数与基类的虚函数具体相同的签名。
解决虚函数复写中的3号问题。
4.3 final
- 指定某个类不能被派生。
- 指定某个虚函数不能再派生类中被覆写。
解决其他问题。
5 类型推导用法
5.1 类型:
- 模板参数类型的推导。
- auto。
- decltype。
5.2 为什么要引入类型推导
- C++是强类型语言。
- 编译器来处理类型推导。
- 提升语言的编码效率。
5.3 auto
5.3.1 原理:
- 用于推导变量的类型,通过强制声明一个变量的初始值,编译器会通过初始值进行推导类型。
5.3.2 规则:
-
auto变量必须在定义时进行初始化。
- 如果用auto定义多个变量,那么这些变量必须是同一类型。
- 类型推导时会丢失引用&或者丢失const和volitte语义。
- 保留引用或者cv语义,用auto &。
- 万能引用使用auto &&。根据初始值的属性来判断是左值引用还是右值引用。
- auto不能推导数组类型,推导的是数组头部指针。
- C++14中auto可以推导函数的返回类型。
5.3.3 应用:
- 尽量使用auto声明变量,除非影响可读性。
- 使用容器时,迭代器很长。
- 匿名函数返回值。
- 模板函数中使用auto,节约模板参数类型。
5.4 decltype
5.4.1 原理
- decltype用于推导表达式的类型,只分析表达式而不参与运算。
5.4.2 规则
- exp是一个普通的表达式,推导表达式类型。
- exp是函数调用,推导函数返回值类型。
- exp是一个左值,推导左值引用。
5.4.3 应用
- 泛型编程
6 function,lambda和bind
- function:类模板。
- lambda:表达式。
- bind:函数适配器。
6.1 function
C++11以前,保存函数地址用的是和C语言一样的方式—函数指针。于是C++11就引入了std::function类模板。
6.1.1 function是什么
- function是一个抽象了函数参数以及函数返回值的一个类模板。
6.1.2 function原理
- function把任意函数包装成一个对象,该对象可以保存,传递以及复制。
- 动态绑定,只需修改改对象(赋值不同的function对象),实现类似多态的效果。
6.1.3 function用途
- 保存普通函数,类的静态成员函数。
- 保存仿函数(函数对象)。
- 保存类的成员函数。
- 保存lambda表达式。
- 保存bind返回的函数对象。
6.2 仿函数(函数对象)
6.2.1 仿函数是什么
- 重载了operator()的类。
6.2.2 仿函数的特征
- 可以有状态(成员变量存储状态)。
- 有状态的函数称之为闭包。
6.3 lambda表达式
6.3.1 lambda表达式是什么
- 一种方便创建匿名函数对象的语法糖。
6.3.2 lambda的构成
- []:捕获列表。捕获外部变量转为类的成员变量。
- 值捕获:访问匿名函数的外部变量,默认只读,不能修改,使用mutable可以可读可写,这样就不会修改外部变量的值。
- 引用捕获:可读可写,会修改外部变量的值。
- ():参数列表。
- ->:指定返回值。可省略,有类型推导。
- {}:函数的具体实现。
6.3.3 lambda原理
- 将lambda表达式转变为一个函数对象。
- 将lambda的参数列表重载operator(),类似仿函数。
6.4 bind函数适配器
6.4.1 bind函数适配器是什么
- 用来通过绑定函数以及函数参数的方式生成函数对象的模板函数。
- 提供占位符,实现灵活的参数绑定。
6.4.2 bind函数适配器的特征
- 绑定函数以及函数参数,从而构成一个函数对象
- 允许修改参数顺序
- 函数对象是一个闭包
- function是用来描述函数对象的类型。
- lambda是生成一个能访问外部变量的函数对象。
- bind是生成一个能够和参数函数对象。
7 继承下的构造函数和析构函数的执行顺序
- 构造:按照依赖链,越强越先。
- 析构:按照依赖链,越弱越先。
单继承中:
- 成员类按顺序构造,按相反顺序析构。
- 类的构造依赖成员类的构造。
- 基类比成员类依赖性更强。
多继承中:
- 成员类按顺序构造,按相反顺序析构。
- 类的构造依赖成员类的构造。
- 基类比成员类依赖性更强。
- 基类按照声明顺序构造,按相反顺序析构。
8 虚函数表和虚函数表指针的创建时机
先产生虚函数表,每个对象都有虚函数表指针。
8.1 虚函数表的创建时机
- 编译器发现类中包含virtual关键字修饰的函数。
- 虚函数表的内容在编译器编译的时候就已经生成了。
- 虚函数表存放在全局数据区中的只读数据段中。
- 虚函数表是存放虚函数地址的数组。
8.2 虚函数表指针的创建时机
- 对象构造的时候,在构造函数,将虚函数表的地址赋值给对象的虚函数指针。
- 如果类没有构造函数,编译器会为该类自动生成一个默认构造函数,从而为类对象初始化虚函数指针。
继承下,虚函数表指针赋值过程:
- 调用基类构造函数时,先将基类的虚函数表指针地址赋值给vptr。
- 接着调用子类的构造函数时,将子类的虚函数表地址赋值给vptr。也就是覆盖上面一步的vptr。
9 虚析构函数
- 作用:在继承下,为了使子类析构函数能够得到正常调用,需要将基类的析构函数设置为虚析构函数。
- 场景:子类对象指针赋值给基类指针,在调用析构函数时,子类的析构函数不会被调用。
- 在C++看来,设计某个类的时候,不一定是基类
- 如果是基类,我们应该手动的将基类的析构函数设置为虚函数。
10 智能指针种类以及使用场景
10.1 指针管理的困境
- 资源释放了,指针没有置空。野指针:只有一个指针指向资源。指针悬挂:多个指针指向同一个资源。踩内存。
- 资源忘记释放了。
- 重复释放资源。
10.2 解决
智能指针采用RAII思想来自动化管理指针所指向的动态资源的释放。
RAII主要利用了对象的生命周期来控制程序资源。
智能指针利用类的构造函数和析构函数来管理资源。
10.3 智能指针的种类
10.3.1 shared_ptr
解决指针悬挂问题。
- 语义:共享所有权。资源没有明确的拥有者。
- 原理:引用技术。
- 场景:容器中管理指针。资源通过函数传递。
- 使用规范:不要使用原来的裸指针—构造智能指针时,不要暴露裸指针。尽量使用make_shared来构造智能指针。不要通过get来操作裸指针。不要用一个指针构造多个智能指针对象。不要用类对象this作为shared_ptr返回。
10.3.2 weak_ptr
- 辅助shared_ptr,解决shared_ptr出现的循环引用的问题。
- 原因是弱引用不占用强引用计数。
10.3.3 unique_ptr
解决指针悬挂问题。
- 语义:独享所有权。没有赋值运算操作符。仅提供移动构造和移动赋值。
- 明确某个对象只有一个拥有者。
- 场景:
- 使用规范:不支持拷贝,但是可以从函数中返回一个unique_ptr,编译器优化可以解决。make_unique C++14提供的。