2. 构造函数语意学
- Schwarz Error
Jerry Schwarz,iostream函数库建构师,曾为了让cin能够求得一个真假值,定义了conversion运算符
operator int()
。这样用户写出:
if (cin)
语句,就会很方便。
但当用户想要写cout << intVal
时,不小心写成了cin << intVal
; 结果编译器没有报错,哈哈,它的解析语义是:把cin转为整型,然后左移intVal
。
Jerry最后用operator void *()
取代operator int()
。
这个故事告诉我们,编译器背后可能会隐式地做点什么。
问题1: 编译器是否为每个类都产生默认的构造函数?
class A {
public:
int a;
};
int foo() {
A oa;
return oa.a;
}
有些人以为这里对象oa的成员a值是零,其实它是个未定义值,初始化它是程序员的责任。这里编译器并不会为A产生任何构造函数。
所以需要区别是编译器需要?还是程序的需要(用户需要)?
另外,即使编译器在它需要合成的情况下产生了一个默认构造函数,该函数也不会处理初始化的内容,仍然需要用户自行指定。
问题2: 编译器在什么时候会合成默认的构造函数? {P41-P47}
- 主要有四种情况,编译器需要合成一个隐式的默认构造函数 (implicit non-trivial default ctor),满足编译器的需要:
- 带有Default constructor的member class object
- 很容易理解,类成员对象在定义时,因为它有默认构造函数,那么它需要进行相应的构造,但何时调用呢?肯定是在本对象创建的时候,那么本对象得有一个构造函数去构造它们。
- 即使该函数已经具有一个构造函数了,编译器也需要在用户显式的语句前,安插代码来初始化。
eg:objA.A::A()
- 当有多个成员对象都需要调用时,调用顺序则为声明的顺序。
- 带有Default constructor的base class
- 容易理解,基类的内容创建时需要按照它指定的去初始化。
- 当用户已经有好多个ctor,但没有提供default ctor. 编译器会扩展当前的每一个ctor,将需要执行的工作放到每个ctor里,但不会再合成一个新的default ctor.
- 带有Virtual function的class
- 容易理解,初始化函数需要为产生的对象设置虚表指针
- 带有Virtual base class的class
- 跟3类似,需要为对象的布局初始化跟virtual base class相关的东西。
{P47} 至于没有存起那四种情况而又没有声明任何ctor的classes,我们说它们拥有的是implicit trivial default constructors,它们实际上并不会被合成出来。
一定要注意,合成出来的默认构造函数只会初始化编译器认为的必要的部分(基类对象,类对象),其他数据成员并不会被初始化。
最后,要记住两个误区(都是不对的):
1. 任何类只要没有定义默认构造函数,就会被合成出来一个。
2. 编译器合成出来的默认构造函数,会为类内的每一个成员设定默认值。
{P48} 拷贝构造函数运用
以一个对象的内容作为另一个对象的初值 (进行初始化)
- 对一个对象做明确的初始化操作, eg: A objA1 = objA0
- 一个对象被当作参数交给某个函数时,eg:foo(objA);
- 函数返回一个对象时
跟构造函数一样,拷贝构造也分为trivial和non-trivial两种。只有non-trivial的实体才会被合成出来。决定一个拷贝构造是否为trivial的标准在于class是否展现出所谓的"bitwise copy semantics"。
{P53} Bitwise Copy Semantics(位逐次拷贝语意),说白了就是按照字节宽度的拷贝。有四种情况,一个class不展现出bitwise copy sema.
- 类成员对象声明有或被编译器合成有一个copy ctor时,这些对象需要显式定义拷贝构造(或编译器提供),说明字节复制无法满足需求。
- 基类有copy ctor时 (显式或者编译器合成)
- 存在virtual函数
- 存在virtual继承的基类
前两种情况中,编译器必须将member或base class的copy ctor调用操作安插到被合成的copy ctor中。
没有拷贝构造函数的成员,将继续使用bitwise copy。
重点分析后两种情况,
{P54} 1. 重新设定virtual table指针
对于同一个类的对象进行拷贝构造,bitwise copy问题不大,对应vptr指向的是同一个类。
但可能存在情况是,B b = d (D继承B),这时候会发生切割,因为b实际的大小已经为B类的大小了,其 实际的虚表也需要指向B类本身的,
如果简单地进行bitwise copy,这时候的vptr就指向D,就错了!
细想下也不允许是d的vptr,假设有个虚函数foo,d的foo函数解析的是D大小的对象,而此时的b只有B类大小。而ref/point不会有这个问题,因为底层的大小还是没有变,该是D还是D。
{P57} 2. 处理Virtual Base Class suboject
类似的概念,同一个类对象不会有什么问题,但如果子类的对象去构造父类的对象,需要保证父类对象里vbase point/offset是对的。
显式初始化
{P61} 显式初始化操作会有两个必要的程序转化阶段:先重写每一个定义,剥除其中的初始化操作
,然后安插class的copy ctor调用操作。
eg:
X x1(x0)
=>
X x1; 定义被重写,初始化操作被删除 (仅留下内存占用 去掉初始化操作)
x1.X::X(x0) copy ctor
参数/返回值 重写
{P62-P63} 参数初始化/返回值初始化 编译器可以做一些函数签名引用重写,RVO等优化。
foo(A);
A a;
foo(a)
=>
A t;
t.A::A(a); 调用copy ctor
foo(t); 重写foo()函数调用,使用暂时对象。需要重写接口为引用效率更高
A foo(){A x0; return x0;}
=>
void foo(A &ret) {
x.A::A(); // default ctor
ret.A::A(x); // copy ctor
return;
}
=>
void foo(A &ret) { 优化后
ret.A::A();
return;
}
节省了一次copy ctor.
{P73} 有时候如果编译器的bitwise copy实现太差,可以考虑自己显式定义copy ctor,用memcpy等优化函数。 haha, 有这样的场合么???
初始化列表
- {P74} 当写下一个ctor时,就有机会设定class members的初值。不是经由member initialization list,就是在constructor函数本身之内。
{P75} 必须使用初始化列表的场合:
- 初始化一个reference member时;
- 初始化一个const member时;
- 调用一个base class的constructor,而它拥有一组参数时;
- 调用一个member class的constructor,而它拥有一组参数时。
- 可怕的事情:
class Word {
String _name;
int _cnt;
public:
Word(){
_name = 0;
_cnt = 0;
}
};
这里的ctor会先产生一个临时的String obj并初始化String(0),然后以assignement运算符指定给_name, 然后临时变量再销毁。
- 更佳的方式:
Word():_name(0) {
_cnt = 0;
}
即: _name.String::String(0);
{P81} 编译器会对initialization list一一处理并可能重新排序,以反映出members的声明次序,它会安插一些代码到constructor内,并置于任何explicit user code之前。list项目顺序是由members声明顺序决定的,不是由init list里排列顺序决定。 {P77}
class X {
int i;
int j;
public:
X(int val): j(val), i(j){}
}
i值未定义,因为按照声明次序,i在j前, i = j 时 j 还未赋值为val。
X::X(int val): j(val) {
i = j;
}
符合预期,因为init list先执行。