C++对象模型目录
C++ - 对象模型之 编译器何时才会自行添加构造函数
Default Constructor的构建
如果没有声明构造函数,默认构造函数并不会都会构建,它必须符合四种情况才会构建。
情况一:包含的成员有一个带有Default Constructor的类对象
但是还有一个前提,该类对象成员必须有default constructor,因为生成Default Constructor的原因就是为了调用成员类的构造函数来初始化该成员。
如:
class C1{
public:
C1(){}
};
class C2{
private:
C1 c1;
};
public:
C1(){}
};
class C2{
private:
C1 c1;
};
由于C1有构造函数,那么C2就需要调用C1的构造函数来构造c1,所以C2会生成如下的构造函数:
inline: C2::C2(){
//伪码
c1.C1::C1();
}
另外,如果C2中已经有了构造函数,那么编译器会在构造函数里面也生成如上的伪码。
情况二:继承于一个带有Default Constructor的基类
还记得C++相关书籍说过,继承类构造的过程是基类先构造,然后是继承类再构造。那么如何才能使基类先构造呢?
1. 基类有Default Constructor;
2. 在继承类的构造函数首先调用基类的Default Constructor,然后再执行其他的初始化操作;
情况三:带有虚函数的类
带有虚函数类的对象有一个vptr指针指向虚函数表vtbl,那么就需要初始化这个vptr指针指向vtbl。这样编译器才能把每个对象的调用的虚函数通过虚函数表的方式调用到真正的函数。
情况四:虚拟继承的类
Class A{};
Class B : public virtual A{};
Class C : public virtual A{};
Class D : public B, public C{};
由于B* pb可以代表B对象也可以代表D对象,这些东西必须在执行期才能确定,由于对象B和对象D中A的偏移量(A一般都是在最后面)不一定,所以,就需要一个指针指向这个virtual base class。那么这个指针就需要在构造函数中初始化。
拷贝构造函数的构建
Bitwise Copy Semantics(位逐次拷贝)
什么是位逐次拷贝,C语言中Struct赋值,其实就将对应位置的元素进行赋值,这些对象在内存上是连续的,只需要将内存复制过去即可,在这样的简单情况下,是不需要拷贝构造函数的。也就是,当不符合位逐次拷贝时,拷贝构造函数才需要,才会被编译器创建出来。以下是,不符合位逐次拷贝的情况:
1. 类A有一个成员是类B的对象b1,并且类B有构造函数
试想,如果这个时候,类A没有构造函数,那么如何才能调用B的构造函数呢;既然没有构造函数,那么就是使用的位逐次拷贝,也就是对类B也是使用的位逐次拷贝,这显然是不对的,因为类B的构函数可能只是对部分成员进行的拷贝赋值。
2. 类A继承于类B,而类B有拷贝构造函数
情况和第一种情况差不多,拷贝时,必须调用B的构造函数,那么类A也就需要一个构造函数。
3. 类中含有虚函数
假设类Animal(动物类)和类Bear(熊类),熊继承于动物类,那么在Animal和Bear的对象里面都有vptr来指向应该调用的虚函数。如果Animal的对象A1复制给Animal的对象A2,或者Bear的对象B1复制给Bear的对象B2,是没有问题的,因为他们的调用的虚函数表是一样的,所以可以使用位逐次拷贝,直接将vptr拷贝过去即可。但是如果按照位逐次拷贝将A1复制给B1,就会出现问题,因为这样一来,B1的行为就变成了A1了,那么B1本来是一个熊,它喜欢偷蜂蜜,复制之后就不是一个熊了,它不再喜欢偷蜂蜜了,这显然是不妥的。同理,将B1复制给A1也不妥。所以,这样的情况下,就需要有一个构造函数,来设定vptr的指向。
4. 虚拟继承的类
如:
Class Bear : public virtual Animal
Class Pander : public Bear
Pander p;
Bear b = p;
如果以位逐次拷贝会出现什么情况,p和b的内存布局可能如下:
p:
Bear Data
Pander Data
Animal Data
b:
Bear Data
Animal Data
b=p会使,b丢失Animal虚拟基类的信息,而多了Pander Data(普通熊是不需要熊猫的属性的)
这显然是不妥的,所以需要一个构造函数,使b只获得p的部分属性(普通熊的属性)和Animal的属性。
总之,只要上面四种情况至少之一符合,则需要编译器创建一个构造函数。
成员初始化列表
以下几种情况,是必须使用初始化列表的:
1. 当member是引用或者const时
我们知道引用和const变量初始化时必须进行赋值,那么编译在初始化这些成员时,就会将成员初始化。如果不在初始化列表,而是在构造函数里赋值,那么就将分配变量和变量赋值分离,显然无法通过编译。
2. 基类没有Default Constructor而有带参数的Constructor
继承类构造时,会先构造基类,由于基类的构造函数有参数,而如果我们不显示指定这个参数,那么显然也是编译不同过的。但是如果构造函数参数都有默认值,就不会出错。
3. 成员类没有Default Constructor而有带参数的Constructor
和第二种情况相似,构造函数必须构造member Class,由于Member Class的构造函数有参数,而我们不指定这个参数,那么显然也是无法编译通过的。同样,如果构造函数参数都有默认值,就不会出错。
效率分析
对于普通类型成员,在初始化列表初始化和在构造函数初始化效率一样,但是如果初始化的是类对象,那么在初始化列表初始化,其实编译器会在构造函数直接通过构造函数构造类对象,而如果是在构造函数通过赋值构造类对象,那么编译器会先构造初始值,然后通过拷贝构造函数构造成员类对象,最后将初始值析构。明显效率会降低。
注意
初始化列表初始化的顺序是按照声明的次序而不是初始化列表的顺序初始化。这也可以理解,因为按照惯性思维,我们对内存的分配和初始化是顺序的,而内存上面的次序和声明次序是一致的。