使用C++语言实现面向对象的设计时,不可避免会遇到对象的构造,对象的拷贝和对象的析构等问题。
虽然C++对象的构造,拷贝和析构是该语言的基本要素,但是不小心的使用,不正确语义设计也会带来不少的麻烦。
目前的系统中出现的一些很奇怪的问题也已经扩展到对对象构造析构的层次上了,本人也遇到不少的类似的问题。
对象的构造
对象的构造是一个对象存在的必要条件,即使你的声明中不写构造函数,它也会在需要的时候被构造出来。
如下四种情况没有声明的构造函数会被合成出来: “带有默认构造函数”的 成员类对象;“带有默认构造函数”的基类;“带有一个虚拟函数”的类;“带有一个虚拟基类”的类;
上述四种情况虽然可以合成默认的构造函数,但是不会初始化你真正定义的成员变量。所以一般情况下请提供默认的构造函数来初始化成员变量。当然成员变量的初始化应该使用哪种方式比较高效,咱就不提了!
编译器对构造函数的扩展
在继承体系下,构造函数显然十分必要,我们需要了解它的工作机理才能不犯一些错误!
在进行编译的时候,编译器会扩展构造函数,一般编译器所做的扩充如下:
1. 记录在成员初始化列表中的数据成员初始化操作会被放进构造函数本身,并以成员声明的顺序依次初始化。
2. 如果有一个成员并没有出现在成员初始化列表之中,但是它有一个默认构造函数,那么该默认构造函数必须被调用。
3. 在那之前,如果类对象有虚拟函数表指针,它(们)必须被设定初始值,指向适当的虚拟函数表。
4. 在那之前,所有上层的基类的构造函数必须被调用,以基类的声明顺序为顺序(与成员初始化列表中的顺序没有关联):
4.1 如果基类被列于成员初始化列表中,那么任何明确指定的参数都应该传递过去。
4.2 如果基类没有列于成员初始化列表中,而它有默认构造函数(或者默认的按成员拷贝函数),那么调用之。
4.3 如果基类是多重继承下的第二或后续的基类,那么this指针必须有所调整。
5. 在那之前,所有虚拟基类的构造函数必须被调用,从左到右,从最深到最浅。
5.1 如果类被列于成员初始化列表中,那么如果有任何明确指定的参数,都应该传递过去。若没有列于表中,而类有一个默认构造函数,也应该调用之。
5.2 此外,类中的每一个虚基类的子对象的偏移量必须在执行期可以被存取。
5.3 如果类对象的最底层的类,其构造函数可能被调用;某些用以支持这个行为的机制必须被放进来。
虚函数表指针的初始化
关于虚函数表指针,通行的执行算法如下
1. 在继承类中,所有的虚基类以及上一层的基类的构造函数被调用。
2. 上述完成后,对象的虚函数表指针被初始化,指向关联的虚函数表。
3. 如果有成员初始化列表的话,将在构造函数体内扩展开来,这必须在虚函数表指针被设定之后才进行,以免一个虚函数被调用。
4. 最后,才执行程序员的代码。
对象的拷贝
一个类对象可以从两种方式复制得到,一种是被初始化,另一种则是给指定,从概念上讲,就是拷贝构造和拷贝赋值构造。
当你不提供拷贝构造函数和拷贝赋值的时候,他们也未必被构造出来,只有当类不展现逐位次拷贝(Bitwise Copy Semantics)时,构造函数才会被合成出来。
对于一个类对象而言,一下四种情况不会展现Bitwise Copy。
1. 当类内带一个成员对象,而改成员对象有一个拷贝构造或(赋值操作)时。
2. 当一个类的基类有一个拷贝构造函数(拷贝赋值)时。
3. 当一个类声明了任何虚函数时。
4. 当类继承自一个虚基类时。
因此,并不是所有的类都需要拷贝构造函数和赋值构造函数,至于需不需要拷贝构造函数,需不需要赋值构造函数,需要视类所使用的资源以及语义来定。
对象的析构
如果类没有定义析构函数,那么只有在类内带的成员对象(或者类自己的基类)拥有析构函数的情况下,编译器才会自动合成出一个来,否则,析构函数被视为不需要,也就不需要被合成。
当析构函数存在的时候,编译器回进行必要的扩展,以使对象可以被正确的析构。
编译器对析构函数的扩展
1. 析构函数的函数本身首先被调用。
2. 如果类拥有成员类对象,而后者拥有析构函数,那么他们会以其声明顺序的相反顺序被调用。
3. 如果对象内带一个虚函数表指针,则现在会被重新设定,指向适当的基类的虚函数表。
4. 如果有任何直接的(上一层)非虚基类拥有析构函数,他们会以其声明顺序的相反顺序被调用。
5. 如果有任何虚基类拥有析构函数,而当前的类是最尾端的类,那么他会以其原来的构造顺序的相反顺序被调用。
参考 《Inside The C++ Object Model》