侯捷C++面向对象高级编程(下)笔记
文章目录
- 侯捷C++面向对象高级编程(下)笔记
- 前言
- 1. 转换函数
- 2. explicit与隐式转换
- 3. pointer-like classes
- 4. 仿函数
- 5. namespace
- 6. 类模板
- 7. 函数模板
- 8.成员模板
- 9.模板特化
- 10. template template parameter,模板模板参数
- 一点小说明
- 11. 可变数目模板参数
- 12. auto关键字和增强型for循环
- 13. reference
- 14. vptr和vtbl:虚指针和虚函数表
- 15. 关于this指针
- 16. 动态绑定
- 谈谈const
- 关于delete和new操作符
- 17. 重载delete和new操作符
- 18. 重载delete()和new()操作符:placement new和placement delete
前言
感谢侯捷老师精彩的课程,PPT在GitHub上搜到的,一并感谢,来源:
https://github.com/harvestlamb/Cpp_houjie
1. 转换函数
转换函数,一个是转出去(即类A可以不可以转换成别的对象),一个是转进来(即别的对象转为A)。
黄色部分描述的函数,一定是operator开头,意义是Fraction类可以转为double,任何时候编译器需要转为double的时候就调用。因为后面已经写了double,因此这种转换函数是没有return type的。并且转换函数通常是不应该改变类内的成员变量,因此通常都会在后加上const. 从上节课程我们应该知道const该加就要加。
对于double d = 4 + f,编译器编到这里的时候,首先可能会去找有没有一个全局函数operator +,且第一个参数是整数或浮点数,而第二个函数是类Fraction;但是我们这里没有写,于是编译器又会去看能不能把f转为double类型。
注意:任何一个类,只要合理可以写多个转换函数,比如转为double的,再加转为string的。不需要是基本类型,只要编译器认得可以通过就行。
2. explicit与隐式转换
+作用在左手边身上(f),找到了函数Fraction operator+,然后就会看能不能把4转为Fraction operator+的参数,可以转,于是编译通过。
non-explicit-one-argument ctor,作用就可以看出,可以把别的东西转为我这个类,即“转进来”。
而如果把上两幅图的情况合并放一起,就会因为二义性编译器不知道怎么办而报错(只要多于一条路可行编译器就不知道走哪条路好就会报错)。既可以把4转换为Fraction类中的+运算符重载,又可以把f转换为double。
explicit关键字,几乎只有在构造函数前加explicit这个用法。这个关键字的意思就是明白地、明确的。加上之后就告诉编译器“不要那么自动,要明白、确定地”,不能直接把3变成3/1了。
下图为C++ stl中操作符重载和转换函数的一个应用:
(C++操作符重载很重要)
3. pointer-like classes
关于智能指针,就是一个类用起来类似指针,其中封装了很多用法,早期的写法:
一般智能指针都会有这样一个构造函数,接受C++的“天然的”指针,如上图中的构造函数,将new出来的对象指针赋值进去。
但是上例中 -> 符号非常奇怪,明明sp->已经返回px了,怎么sp->method 还会变成 px->method呢?其实是因为->符号很特别,得到的东西会继续用->符号继续下去(暂时不要继续深究下去)
特别一提,对于智能指针 * 和 -> 符号重载几乎都是写成上图这样子。
关于迭代器,其实也可以说是一种特殊的智能指针:
如上一个链表的迭代器,data为Foo类型,即Foo类的链表,一个简写,非常清楚。这里也可以看到迭代器和智能指针对符号 * 和->的区别。
4. 仿函数
任何一个对象只要能接受符号 () 就说它是函数或者仿函数:
每个仿函数其实在背后都继承了一个奇怪的类,如下图所示,这个类不用程序员手动显式声明:
标准库中的仿函数也同样继承了一个奇怪的类:
如上,这两个类非常的特别,理论上sizeof是0,不过由于实现等原因,当你在编辑器真正尝试有可能sizeof会是1
5. namespace
namespace主要就是把一些东西区别出来:
6. 类模板
模板类或者叫类模板:
7. 函数模板
下图的class也可以改为typename:
与类模板不同的是,函数模板在使用是不需要显式地声明传入参数类型,编译器将自动推导类型。
所以函数模板编译只是一个”半成品“,会在后续使用的时候再次编译。
实参推导(argument deduction)。
8.成员模板
黄色这一块是一个模板的member,而其又是一个模板,就叫成员模板:
这个比较抽象,看具体的例子:
通过这种方法,只要传入的U1和U2的父类或者祖类是T1和T2,那么通过这样的方式可以实现继承和多态的巧妙利用,这样的方式在STL中用得很多。
9.模板特化
下图是标准库的一个片段:
上图中第一个空的()表示浅绿色部分
h
a
s
h
<
l
o
n
g
>
(
)
hash<long>()
hash<long>()是一个临时对象,然后编译器会去根据实参去找,然后就会跳过泛化版本,而是找到特化版本
s
t
r
u
c
t
h
a
s
h
<
l
o
n
g
>
struct \space hash<long>
struct hash<long>
泛化又叫做全泛化,对应的叫做偏特化,其实也就是模板偏化有程度之分,可以部分类型指定,称之为偏特化(老师这里分为了个数上的偏和范围上的偏):
如上图,比如bool只需要一位,用一个字节来表示太浪费,就做出了偏化,这是个数上的偏。
另一种叫范围上的偏,如下图:
此图就看到了范围上的偏特化,比如编译器此时看到:
C<string> obj1;
就会使用泛化版本:
template<typename T>
class C
{
...
}
而看到:
C<string*> obj1;
带有指针,则会用偏特化版本
template<typename T>
class C<T*>
{
...
}
10. template template parameter,模板模板参数
这里有一个困扰我的问题,就是class和typename什么时候共通。
事实上只有在template<>的尖括号<>里面才会共通,而至于为什么是一个历史遗留问题,因为早期并没有关键字typename,所以就先用class作为了替代。
这里打叉是因为容器需要好几个模板参数,事实上我们使用的时候后面的参数都是默认值,但是放在这里语法就是通不过…
而再如上图,有的智能指针只有一个模板参数,就不会报错,而另外两个就会有语法报错。
下图是标准库的一段源代码,这时已经写死绑定好了,所以不是模板模板参数:
一点小说明
可以在编辑器中输出一下__cplusplus查看版本。比如我在VS2019下用一下测试程序:
#include <iostream>
using namespace std;
int main()
{
cout << __cplusplus << endl;
return 0;
}
一般会输出199711L,而我的属性页的的确确是默认的C++14标准
这时可以参考微软官方文档
https://docs.microsoft.com/zh-cn/cpp/build/reference/zc-cplusplus?view=msvc-160&viewFallbackFrom=vs-2019
说明:
/Zc: __cplusplus 编译器选项使 _ _ cplusplus 预处理器宏能够报告最新 c + + 语言标准支持的更新值。 默认情况下,对于 _ _ cplusplus 预处理器宏,Visual Studio 始终返回值 “199711L”。
将 /Zc:__cplusplus 或 /Zc:__cplusplus- 添加到“其他选项:”窗格:
11. 可变数目模板参数
上图的print是一个递归的方式,args…是一包,传入进去又分为一个和一包…直到最后一个参数42被分为一个和零个,如果没有这个版本:
最后的print(42)就会失败。
非常好用的一个语法。
可以用sizeof…(args)来看那一包是几个。
标准库有很多用到了这样的语法。
比如示例程序(在vs2019下测试):
#include <iostream>
using namespace std;
void print()
{
}
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
{
cout << firstArg << endl;
cout << "args_num: " << sizeof...(args) << endl;
print(args...);
}
int main()
{
print(7.5, "hello", 42);
return 0;
}
结果:
而要是把前面的
void print()
{
}
给注释掉,就会编译出错。
12. auto关键字和增强型for循环
上图还有一个值传递和引用传递的差别,在之前的课程已经讲了。
过多内容将在C++11课程中讲解,这里暂时只做介绍
13. reference
reference可以看做是某个被引用变量的别名。
但其实所有的编译器对于x的引用 r 都是用指针来实现的,其实r里头有一个指针指向x。引用必须要设处置,设完之后就不能变了。
所以上图的r=x2就会同时改变r和x2的值。
我们刻意营造一种假象,即引用r与变量x他们的sizeof和地址都要相同。
same signature会引起ambiguity,即相同签名会带来二义性,矛盾。
签名signature不含return type。特别注意,上图灰色部分可以加const,const是函数签名的一部分。因此一个加const一个不加则可以并存。
14. vptr和vtbl:虚指针和虚函数表
所谓继承除了继承成员还会继承函数,继承函数实际上是继承了它的调用权而不是继承它内存的大小。因此父类有虚函数虚指针则子类一定也有。
只要一个类中有一个虚函数,类对象就会多一个指向虚表的虚指针:
如上图所示,定义了三个类,A、B和C,B继承于A,C继承于B,A中有两个虚函数,B重载了vfunc1,C中也是。
如上图,虚表中放的都是函数指针,指向虚函数所在的位置。
比如new C,得到一个指向对象C的一个指针p,想要通过p调用vfunc1,在过去C的时代会编译成一个特定的语法:call xxx,这里xxx是一个地址,要调用哪个函数编译器就把它解析出来跳到那个函数去,将来再return回来。这一种叫做静态绑定。(注:call是汇编语言的一个动作)
但是调用虚函数则是动态绑定,通过指针找到vptr,再找到vtbl,再找到函数地址。
c++编译器看到一个函数调用会有两个考量,是静态绑定还是动态绑定。
动态绑定:1. 通过指针 2. 这个指针向上转型(up-cast),比如指针当时声明的时候是动物类(如图中的A),但是赋值的时候new了一只猪(如图中的C),这保证安全 3. 调用的是虚函数。
只要符合这三个条件,编译器就编译成(* p->vptr[n] )§,即虚机制,动态绑定的方式。
静态绑定编译出来一定就是某块地址,而动态绑定则会看是哪一个虚表,即p指向哪里。
这便是面向对象多态的本质。
15. 关于this指针
简单地说通过一个对象调用一个函数这个对象的地址就是this pointer
这一点有一篇知乎文章讲的挺好的:
https://zhuanlan.zhihu.com/p/94864064
this指针其实可以认为是指向当前对象内存地址的一个指针,如上图所示,由于基类和子类中有虚函数,this->Serialize()将动态绑定,等价于(*(this->vptr)[n])(this)。可以结合上节虚指针和虚函数表来理解,至于最后为什么这样写是正确的,下面小结将会解释。
16. 动态绑定
如上图,a是一个对象而不是指针,所以a.vfunc1就是一个静态绑定,压根不是动态绑定。
而pa->vfunc1则是一个动态绑定:
从上面汇编的形式也可以看出特点。
谈谈const
const放函数后只能放成员函数后头,全局函数则不可以。
即告诉编译器这个函数不改变class的data数据。
之前也讲了const也算函数签名的一部分,如上图的右侧部分就做出了中括号重载的操作:
比如说一个string s,如果做赋值s[5] = ‘x’,那么此时就需要考虑COW:copy on write,而如果我们使用的是一个常量字符串 const string s,那么就不应该COW,应该区分开来,于是中括号就做出了两个版本(不考虑COW速度比较快)。
而当成员函数的const和non-const版本同时存在的时候,那么const object就只会调用const版本的成员函数,而non-const object只会调用non-const版本。
关于delete和new操作符
这是面向对象高级编程(上)的一页PPT,new和delete都是表达式,是不能重载的;而把他们行为往下分解则是有operator new和operator delete,是有区别的。
直接用的表达式的行为是不能变的,不能重载的,即new分解成上图的三步与delete分解成上图的两步是不能重载的。这里内部的operator new和operator delete底层其实是调用的malloc,这些内部的几步则是可以重载的
17. 重载delete和new操作符
上图的ppt已经很清楚了,new和delete被分解为1和2,然后重载的是operator new和operator delete,分别是其中的一步。
下面为一个示例:
如果加::,则调用了全局的函数而不是接管的函数。
如上,在G4.9下,new一个大小为5的数组,则里头还会有一个size为4的数代表数量5,构造函数从上往下,析构则从下往上。从打印信息可以看到构造时调用了5次构造函数、析构时调用了5次析构函数。
这种在一个Array前头多一个count的做法在GCC在VC都是一样的做法。
如上这样用(写上了global scope operator ::)则会绕过那些重载的函数。
18. 重载delete()和new()操作符:placement new和placement delete
如上,重载operator new时第一个参数必须时size_t
如上图,比如想多分配一些东西的时候(比如上图的标准库的string,为了多一个counting的设计就重载了placement new。