第一章 关于对象
指针其实存的就是一个地址,不管啥类型的指针大小都是一样的,对不同类型的指针,怎么知道要读多少字节,这是编译器干的事情,指针类型告诉编译器,我到底要读多少字节。
32位,指针大小4个字节,64位8个字节
由此引申开来,不能用void * 操作对象,针对void *指针的static_cast,其实是一种编译器指令,并没有改变一个指针所含的地址
第二章 构造函数语意学
1)默认构造函数
主要还是参考 prime
当你没有默认构造函数的时候,且你没有定义其他构造函数,啥时候编译器不会为你合成一个呢,也就是定义为删除?
1) 类的某个数据类成员,它的析构函数是删除或者无法访问的(access level)
2) 类的某个数据类成员无法默认构造的时候
a)它就是无法默认构造,比如有个其他的构造函数了,却没定义默认构造函数
b)它是个引用,那么它必须得显式初始化,然后又没给它类内初始值
c) 它是个const,但是没有显式的默认构造函数,(const 对象必须要初始化,但是一个有显式的默认构造函数的const 也是ok的, 比如 const string s1; 不会报错,它可能会有一个编译器合成的默认构造函数,但是对于const 对象来说,不顶事),且没有类内初始值
以上这些数据类成员包括基类 (prime p451)
编译器合成版本的默认构造函数会为你干这3样事:
1. 给你的数据类成员 (包括基类) 默认初始化
2. 然后还会给你一个vptr帮你初始化好,指向类的虚函数表 (共享一个)
3 . 还有一个虚基类指针(以后补充) 只有这两个指针编译器会帮你初始化好
假如你自己定义的默认构造函数也好,别的构造函数也好,应该要有这些3个内容,却缺了点啥,编译器都会帮你补上,顺序按声明顺序(先基类,再自己的数据成员,两个指针不知道在哪一块 最开始or最下面 以后补充),那假如你没有默认构造函数,编译器也不给你合(也就是被定义为删除的),你又默认初始化了一个东东 那就gg,编译器报错。
并且下面这段代码也会报错:
class test{
public:
test(int _a):a(_a){}
private:
int a;
};
class test1{
public:
test a;
int b;
test1(int i):b(i){}
};
int main()
{
test1 ab(1);
}
因为test1的构造函数test1(int i)体内实际会调用test的默认构造函数(编译器帮你干的),但是test又没有默认构造函数,所以gg!
然后有个点,内置类型的,比如整型 浮点型 字符型 指针 内置类型数组 都不会被编译器初始化,这些是你要干的事情,然后还有个tips,global区一开始会被清0,local和heap不会清0,所以是上一次的遗留物
2)拷贝构造函数
什么情况会调用copy构造函数:
1.用一个对象作为参数初始化另一个对象时.
2. 对象作为函数参数时, 会用参数对象在函数作用域构造一个新的对象.
3. 对象作为返回值时, 会用函数内部的对象在返回值所在作用域构造一个新的对象.
注意, 2, 3不一定会发生, 因为可能会存在右值参数, 返回值优化等, 具体情况不做详述.
如果不显式定义复制构造函数, 编译器有两种复制对象的方法: bitwise copy和default memberwise copy, 区别如下:
类的copy整体思路是用 default memberwise initialization 实现的,具体点就是以一个个member的搞,对于内置类型的,int,指针,数组,是以bitwise copy实现的,而对于类类型,则是递归的调用的memberwise initialization实现的
bitwise copy(位逐次拷贝),就是复制一个个位,并不调用copy构造函数, 可能的实现方式如利用memcpy等, 因此效率高, 复制出的对象和原对象完全相同.
假如类靠bitwise copy就ok的话,就万事大吉了,但是有些情况,bitwise copy会出错,这时候,编译器会帮你隐式生成一个“有用的”copy构造函数。
插一句,bitwise copy也好,“有用的”copy构造函数也好,其实都可以理解编译器帮我们定义的一个copy构造函数,只是前者是“没用的”,所以编译器不会帮我们合成于程序中,我猜可能bitwise copy 可能是一个默认的机制?但是对于一个string类,它的data是int 和char * ,假如我们没用定义copy构造函数,显然它的实现会是bitwise copy,这时候我们对一个对象用拷贝来初始化,程序是不会报错的
那啥时候程序会gg呢?当 copy构造函数是delete的时候,
根据prime p450
当类的数据类成员析构函数(拷贝构造函数)是删除的或不可访问的,这个类的copy构造函数是delete的。
ok,回来,下一个问题,啥时候编译器会搞个有用的copy构造函数呢?
- 当类含有类对象成员(这里有一个,不用要求全部), 且这个成员含有copy构造函数时(不论是编译器合成的(合成的要是有用的!)还是显式定义的).
- 当类继承自一个基类, 并且基类含有copy构造函数时(不论是编译器合成的还是显式定义的).
- 当类含有虚函数时.
- 当类有虚基类时.
上面的情况很容易理解. 对于1和2, 由于copy对象时, 要copy数据成员和基类, 既然它们提供了copy构造函数, 就可以认为需要在它们的copy构造函数中进行某些bitwise copy无法实现的操作, 因此不能采用bitwise copy.
对于3, 由于含有虚函数, 所以需要初始化对象的vtpr, 而vptr的值显然不一定等于参数对象的值, 例如用子类对象初始化父类对象时. 所以bitwise不能满足需求.
对于4, 由于含有虚基类, 父子基类的内存布局可能存在区别, 更不能采用bitwise copy.
ok 总结一哈,一个编译器合成的copy构造函数,大部分情况下是bitwise copy,但是当我们有个类对象成员,它的copy构造函数不是bitwise copy的时候,(类对象成员包括基类),或者当类有虚函数,虚基类的时候,不能对那两指针简单粗暴的bitwise copy,这时候会对那些刺头进行 不是bitwise copy, 具体做法是调用 类对象成员的 “有用” copy构造函数 对那两指针进行显式设定
3)程序转化语意学
尽管在程序中可以使用不同的形式来初始化一个类对象, 但在编译阶段都会被转化成相同的形式. 例如:
class X;
X x0(paras);
X x1 = X(paras);
X x2(x0);
X x3 = x0;
X x4 = X(x0);
会被转化为:
X x0; // 声明但不初始化
X x1; // 声明但不初始化
X x2; // 声明但不初始化
X x3; // 声明但不初始化
X x4; // 声明但不初始化
// 调用构造函数初始化对象
x0.X::X(paras)
x1.X::X(paras)
// 调用复制构造函数初始化对象
x2.X::X(x0)
x3.X::X(x0)
x4.X::X(x0)
参数初始化:
void foo(X x0)
调用这个函数 我们在程序里写的是 X xx;foo(xx); 但实际编译器的实现 两种方法是: 看书 p62
返回值的初始化:
看书 主要思路就是一个返回值的函数 典型的编译器实现方法是在函数参数里面加一个引用的参数,这个参数用来放return 的值,靠拷贝构造函数来实现,然后函数返回void
然后有两种优化方法: 1)码农的脑洞版:定义一个计算用的构造函数,参数就是原函数的参数,然后函数体就是一个return 这个构造函数的返回值, 然后编译器帮你实现的就是(见上) 一个引用的参数,直接计算,这样就省了一个拷贝构造
2)编译器层面的优化:就是NRV (named return value 具名返回值?maybe) 就是把我原来不是要加一个引用的参数,然后把我要返回的值,拷贝到这个参数里面来嘛,现在我就把函数体里面这个返回值的对象,同时变成我引用参数的对象,这样就省了一个拷贝构造 (有个tip,要想编译器帮你NRV优化,必须要显示定义拷贝构造函数,我也不懂为啥?)
4)成员们的初始化队伍
构造函数的初始化列表,1个是可以提升效率,还有一个是你有时候必须要这么做。
1. 提升效率:
因为编译器会对构造函数进行扩充,先对数据成员进行初始化(默认构造函数),再执行构造函数体,那么就会先进行初始化,然后进行赋值操作(可能还会有先构造一个临时对象,然后进行赋值操作,再析构这样的操作),如果用初始化列表,就能初始化一步到位(实际编译器转换后的代码是先执行初始化列表(对应的构造函数),再执行函数体的代码),初始化顺序是类定义时候数据成员的声明顺序决定,不是由列表排列顺序决定。(不过,数据成员是内置类型的话无所谓,你放在初始化列表里面,还是放在函数体里面,都ok,因为编译器不会对一个内置类型初始化作什么动作)。
2. 必须这么做
比如有引用或者const对象,这种必须要初始化的成员的时候,还有就是你要调用某个成员的非默认构造函数,或者这个成员它没有默认构造函数的时候(如果这样,会报错)