C++|侯捷C++视频笔记——C++面向对象高级编程(下)

C++面向对象高级编程(下) 01、简介 学习目标: 1.探讨上篇没有探讨过的问题
2.继续深入了解面向对象,包括虚指针,虚表,虚函数,以及多态
02、转换函数与explicit 1.转换函数
转换函数的特点:
1.转换函数不需要像普通函数声明返回值类型
2.转换一般一般都为const,因为它只是对一个值进行一个临时转换然后返回转换之后的值,而不是修改原有的值
3.转换函数可以有多种
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

如图

Fraction f(3,5) //这一句是调用了构造函数
double d = 4 + f;
这一句实际上进行了如下判断
首先是在查找有无符号“+”的重载函数,发现没有
然后再尝试寻找转换函数,发现4和f都有对应的转化函数,于是执行转换
2.构造函数的类型转换
构造函数会进行自动地隐式类型转换
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

如图,Fraction(译为分数)只需要一个参数num(分子),分母默认为1
当遇到类似这种语句时
Fraction d2 = f + 4;
编译器查找有没有关于符号"+"的重载,发现找到了,但是符号所需要的参数不对,应该是一个const Fraction& 类型,而这里是一个int类型,于是编译器试图对齐进行一个类型转换,由于类Fraction的构造函数仅需要一个int类型的参数,所以这里由于4满足条件,所以类Fraction的构造函数将其隐式转换成了Fraction类型的对象来执行了这个操作。
3.explict
如果同时存在可以隐式类型转换的构造函数Fraction以及转换函数double()时,会造成二义性调用而报错
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

为了解决这个问题,可以在函数前面加上关键字explicit
explicit关键字修饰的构造函数不能进行自动地隐式类型转换,只能显式地进行类型转换。 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

如图
Fraction d2 = f + 4;
这里报错的原因是,无法将4转换成一个Fraction值,因为Fraction类中重载了运算符+,需要右边也是Fraction类型的对象,但是Fraction的构造函数又不允许将int值4隐式转换成Fraction类对象,所以报错。
03、类的两种特殊形态:pointer-like class 和 function-like class 为了拓展指针的用法,我们会写出一些看起来“像指针”或者“像函数”一样的类。
1.pointer-like class
这里以智能指针和迭代器作为例子
1.智能指针 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

在智能指针中,一定需要两种指针最基础的运算符,即 * 和 -> 运算符。接受的是一个模板类型
Q:智能指针中关于->运算符的重载,为什么返回的是指针? A:C++中特别的将->运算符设置成了一种“可传递”的运算符,也就是说虽然运算式"sp->method()"中sp“消耗掉”这个->运算符之后,得到的应该会是“pxmethod()”,但是这里特别的让表达式结果为“px->method()”,只是一个特例,记住即可 深入实践C++11智能指针
2.迭代器 迭代器也可以认为是一个功能较为特别的智能指针
迭代器由于需要在一个容器中遍历,必然需要一些类似于“向前”,“向后”的操作,在迭代器中的体现就是++和–运算符,这些对于一般的智能指针来说没有必要
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.function-like class
C++中也存在一些形似函数的类,也叫仿函数,它重载了()运算符,使得调用它就像调用函数一样,在STL中广泛运用,此处先了解概念,不进行详解
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

04、namespace经验谈 使用namespace可以比较方便的进行函数和变量之间的隔离
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

05、模板 一个小知识:我们声明模板时使用了以下两种语法,它只是C++的历史遗留问题,因为早期C++没有模板的概念,所以是使用class代替的,在这里声明模板时,typename和class是可以随意写的,也就只有这个地方可以,其它地方不行
template //这里的class和template其实是可以随意换的 template

由于模板中的参数类型并不确定,所以模板在首次编译时其实也只是一个半成品,等到真正使用的时候会进行第二次编译。
这样带来的缺点就是我们不知道未来使用的时候会出现什么问题
1.类模板
在设计一个类时,如果设计者认为可以将一些参数类型让使用者在使用的时候再指定,那么就可以使用类模板
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.函数模板
几乎和类模板的设计策略一样,如果设计者认为可以将一些参数类型让使用者在使用的时候再指定,那么就可以使用函数模板。
函数模板使用更简单,它会自动进行实参推导,不用我们像类模板一样显示声明
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.成员模板
类本身可以是一个模板,但是同时类中的某个成员即使在类本身也已经确定的情况下,仍然可以再次模板化,即成员的参数类型仍然可以暂定
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

在智能指针中,成员模板也有应用,主要体现在智能指针(普通指针也是)也需要进行继承之后的类型升级的操作,比如说指向动物的指针,将来如果想要让它指向一只狗,由于狗也是动物,那也应当是可行的。
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

4.模板特化
1.模板特化的概念 如果说模板本身是一种泛化(在编写时不确定,在使用时确定),那么这里讲的就是泛化中的特化
由于在模板中,又是不一定希望所有的操作都一样,即可能遇到需要特殊处理的类型,那么就可以进行额外的模板特化操作。
特化的操作很简单,只要在编写时替换那些原来应当为模板表示的变量,将它们替换成我们希望进行特化处理的函数即可
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.模板偏特化——个数 因为模板参数可能有多个,如果我们仅仅相对其它的一个或几个情况进行特化,那么就是模板参数个数的偏特化
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.模板偏特化——范围 这里的范围其实是指这个模板的作用域范围,比如说这里同样是T,但是T可以接受一个普通类型,也可以接受一个指针,但是它们的作用域不用,如果是一个指针,就需要使用下面的
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

4.模板模板参数 直接看例子吧,就是一个模板里面又套了模板参数,比如说这里的container本身就是一个模板,它自己又接受了一个模板参数T作为参数。
需要注意的是,由于一些变量会有第二,第三参数,有时候表面看貌似可以,但实际上不行。比如说list可以写成list,但是实际上里面还有参数,用模板模板参数不行。智能指针也是,不是全部的智能指针都可以写成这种形式
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

另外,如果已经明确指定了参数,那么不能算模板。比如说这里的sequence表面也是一个模板,但是它使用的时候,必须要明确声明它的参数类型。所以它不是一个模板模板参数
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

06、C++ 11的三个新特性 1.可变模板参数
1.概念 一般的模板参数,参数类型必须是固定的。
然而在C++11中,出现了可变模板参数,它能表示0到任意个数、任意类型的参数,会随着我们输入参数的不同而自动调整。同时这些参数也可以进一步展开来拿到他们之中的某一个参数。
template //普通模板的声明形式 template //可变模板的声明形式

2.两种展开方式 1.递归展开
//递归终止函数,必须与展开函数同名,相当于为下面的展开函数做一个参数全部遍历完之后的重载 void print() { } //展开函数 template void print(T cur, Args... rest) { cout << cur << endl; //cur就是当前的元素,可以做对cur的任何操作 print(rest...); //递归调用本身,直至参数全部遍历完成之后,调用不需要参数的print() }

2.逗号展开 利用了逗号表达式来进行展开,逗号表达式就是
d = (a = b, c); //b会先赋值给a,接着括号中的逗号表达式返回c的值,因此d将等于c

然后利用这个性质,我们可以写出逗号展开
template void printarg(T t)//每个参数是在这个函数里面处理的 { cout << t << endl; }template void expand(Args... args) { int arr[] = {(printarg(args), 0)...}; //逗号表达式 }

2.关键字auto
auto关键字会根据右边的表达式返回类型来倒推出auto应该表示的类型。对于lamda表达式这种较难推断出结果的表达式很有用。
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

使用auto必须要有一个初始值,不能单独声明如下
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.范围for语句
使用范围for时,编译器会将容器(必须是一个容器)中的元素一一赋值到左边的变量。使用auto关键字会更加简洁。
注意默认的auto是传值,不是传引用。如果想要传引用,那么需要额外声明
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

07、从内存角度探讨传值,传指针,传引用 x是一个整数
p是一个int类型指针
r是x的一个引用,引用的内部实现其实就是指针
r其实是x的一个“代表”,体现在r所占内存空间和地址与x全部相同,但是这是表面上的(使用sizeof()查看他们是一样的),但实际上使用sizeof查看引用的大小是不正确的,如果使用一个仅含有引用的类,那么会看到引用所占内存空间实际上和指针相同
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

引用最常见的使用是使用在参数类型以及返回类型,引用不会造成函数重载,而且引用不会影响调用,同时提高传递效率
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

08、对象模型 1.虚函数和虚函数表
当一个类有虚函数(不论数量),带有虚函数的类会比不带虚函数的类多一个指针的大小。比如图中B继承A,C继承B。所以A,B,C都将含有一个虚指针。
这个虚指针指向虚表,虚表中保存的全部都是虚函数,虚表中每个元素存放的就是对应函数的地址
所有虚函数的的调用取的是哪个函数(地址)是在运行期间通过查虚表确定的 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.静态绑定和动态绑定 只有虚函数才使用的是动态绑定,其他的全部是静态绑定 在C中,调用函数指针是一个静态的指针,通过call()函数来调用
在C++中,调用函数分为静态绑定和动态绑定。这里着重讲动态绑定。
动态绑定的三个条件:
1.通过指针调用
2.指针必须是向上转型
3.是虚函数
深入理解C++的动态绑定和静态绑定
动态绑定的形式最终如下:
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.关于this指针
通过一个对象来调用函数,那么this其实就是这个对象的地址。
如图,调用myFileOpen的this指针就是this。
所有的函数调用其实都有一个隐藏参数this,所以调用函数时,其实应该是这样:
this->Serialize();

这个this的调用同时满足了以下三个条件:
1.是指针调用:是的,this就是一个指针
2.向上转型:继承了CDocument
3.是虚函数(Serialize)
所以编译器找到了子类对它进行的重载的虚函数。
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.从汇编角度讲动态绑定
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

4.关于虚函数的讲解补充
1.C++虚函数表,虚表指针,内存分布 2.构造函数能否声明为虚函数或者纯虚函数,析构函数呢? 构造函数不能定义为虚函数。在构造函数中可以调用虚函数,不过此时调用的是正在构造的类中的虚函数,而不是子类的虚函数,因为此时子类尚未构造好。虚函数对应一个 vtable (虚函数表),类中存储一个 vptr 指向这个 vtable。如果构造函数是虚函数,就需要通过 vtable 调用,可是对象没有初始化就没有 vptr,无法找到 vtable,所以构造函数不能是虚函数。
析构函数可以为虚函数,并且一般情况下基类析构函数要定义为虚函数,只有在基类析构函数定义为虚函数时,调用操作符 delete 销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据。
3.为什么父类的析构函数必须是虚函数? 如果析构函数不被声明成虚函数,那么编译器将实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不会调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
4.哪些函数不能声明成虚函数?
  1. 非成员函数
    非成员函数只能被重载(overload),不能被继承(override),而虚函数主要的作用是在继承中实现动态多态,非成员函数早在编译期间就已经绑定函数了,无法实现动态多态,那声明成虚函数还有什么意义呢?
  2. 构造函数
    要想调用虚函数必须要通过“虚函数表”来进行的,但虚函数表是要在对象实例化之后才能够进行调用。而在构造函数运行期间,还没有为虚函数表分配空间,自然就没法调用虚函数了。
  3. 静态成员函数
    静态成员函数对于每个类来说只有一份,所有的对象都共享这一份代码,它是属于类的而不是属于对象。虚函数必须根据对象类型才能知道调用哪一个虚函数,故虚函数是一定要在对象的基础上才可以的,两者一个是与实例相关,一个是与类相关。
  4. 内联成员函数
    内联函数是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,并且inline函数在编译时被展开,虚函数在运行时才能动态地绑定函数。
  5. 友元函数
    因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数,不能被继承。
09、一些补充 1.再谈const
同时存在const和非const重载时,如果除了const其他都相同,那么const成员会调用const版本,非const会调用非const版本,
const对象 非const对象
const成员函数 可以调用 可以调用
非const成员函数 不可以调用 可以调用
C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.再谈new和delete,以及重载operator new,new[],delete,delete[]
1.概念区分 具体的在上篇文章中已经详细了分解了new和delete。这里我们主要是再区分一下概念
对于一个表达式
string *p = new string("Hello");

这里的new其实是一个表达式,在实际分解成三步之后出现的new操作符才是可以重载的 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

2.重载全局operator new,new[],delete,delete[] C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.重载实例 使用::可以强制调用全局函数 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

delete实际上是通过内存中分配的一个计数器来计算这个数组delete[]时的次数的 C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

重载new()和delete() C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

C++|侯捷C++视频笔记——C++面向对象高级编程(下)
文章图片

3.placement new
placement new就是在用户指定的内存位置上构建新的对象,这个构建过程不需要额外分配内存,只需要调用对象的构造函数即可。
placement new的好处:
1)在已分配好的内存上进行对象的构建,构建速度快。
2)已分配好的内存可以反复利用,有效的避免内存碎片问题。
placement new的用法及用途
4.malloc 底层实现及原理
1)当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
2)当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
malloc 底层实现及原理
5.编译期多态与运行期多态(静态多态和动态多态)
运行期多态/动态多态
运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。
编译期多态/静态多态
编译期多态对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。
相比较于运行期多态,实现编译期多态的类之间并不需要成为一个继承体系,它们之间可以没有什么关系,但约束是它们都有相同的隐式接口。
C++编译期多态与运行期多态
6.关键字补充:extern,mutable
mutable mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
C++中的mutable关键字
extern 1、extern “C”
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
2、关键字extern
关键字extern是全局变量声明,只要声明全局变量就默认前面加extern(程序员可以不加,但编译器默认加上)。若本文件引用别的文件中的全局变量 一定要加上extern声明一下。
【校招面试 之 C/C++】第29题 C/C++ 关键字extern
7.C++程序编译的四个过程
1、预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
2、编译:将预处理后的文件转换成汇编语言,生成.s文件
3、汇编:汇编变为目标代码(机器代码)生成.o的文件
4、链接:连接目标代码,生成可执行程序
C++ —— C++程序编译的四个过程
8.四种cast操作符
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void* 转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。向上转换:指的是子类向基类的转换向下转换:指的是基类向子类的转换它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
Reference 【C++|侯捷C++视频笔记——C++面向对象高级编程(下)】1.C++ 哪些函数不能声明成虚函数
2.超多电子书与视频资料分享
3.placement new的用法及用途
4.【C++】智能指针详解
5.C++虚函数表,虚表指针,内存分布
6.C++的那些事:你真的了解引用吗
7.malloc 底层实现及原理
8.C++编译期多态与运行期多态
9.C++中的mutable关键字
10.【校招面试 之 C/C++】第29题 C/C++ 关键字extern
11.C++ —— C++程序编译的四个过程
12.深入实践C++11智能指针

    推荐阅读