在4种情况下,编译器会为未声明ctor的类合成一个默认的ctor。
1 带有默认ctor的成员类对象
如果一个类内含一个或一个以上的成员类对象,那么类的每一个ctor必须调用每一个成员类的默认ctor,编译器会扩张已经存在的ctor,在其中插入一些代码,使得用户写的代码被执行前,先调用必要的默认ctor。调用的顺序是这些类对象在类中声明的顺序。
2 带有默认ctor的基类
如果一个没有任何ctor的类派生自一个有默认构造函数的类,那么这个派生类的构造函数会被视为非平凡的,在内部按照基类的声明顺序调用它们的默认ctor。
如果这个类声明了除默认ctor的其他ctor,那么在这些ctor的内部,编译器仍然会扩充这些构造函数,但不会合成一个默认ctor了(因为声明了显式的ctor),内部按照基类声明顺序调用基类的ctor。
如果这个类还包括内部成员类对象,那么编译器会先调用基类的ctor,然后调用内部成员类对象的ctor。
3 带有虚函数的类
当类声明或继承一个虚函数;或者类派生自一个继承链,内部有虚基类,那么编译器会自动合成默认构造函数。编译器会生成一个虚函数表,内部放类的虚函数地址;在每一个类对象中都有一个虚表指针,都会内含相关的类虚函数表的地址。
4 有虚基类的类
对于类定义的每一个基类,编译器会安插“允许每一个虚基类的执行期存取操作”的代码,没有声明ctor的时候,编译器会自动合成一个默认ctor。
对于不是以上4种情况,且没有声明ctor的类,这些类实际上不会构造隐式ctor。
1 默认对每一个成员变量初始化
如果没有显式提供拷贝ctor,那么在编译器执行拷贝时会默认对每一个成员变量进行拷贝
class A{int a; int b};
A a1;
A a2;
a1 = a2;//内部实现为a1.a = a2.a; a1.b = a2.b;
如果类内有类对象(有默认拷贝ctor),那么初始化到这个成员变量的时候,会递归执行拷贝ctor(执行此成员变量的ctor)。
C++拷贝ctor分为重要和不重要两种,只有重要的实例才会被合成到程序中去,是否为重要的拷贝ctor取决于“位逐次拷贝”。
2 位逐次拷贝
在以下四种情况中:
编译器会合成一个拷贝ctor,以便调用成员变量(类对象)的拷贝ctor,否则不需要生成拷贝ctor,直接对每一个成员变量进行拷贝赋值就行(包括整数,指针,数组等)。
3 重新设定虚表指针
当拥有虚函数的类对象进行赋值(调用拷贝ctor)时,要生成拷贝ctor用以正确拷贝虚表指针,如果是以下这种拷贝:
class A{virtual fun();};
class B:public A{fun()}; //虽然fun()没有写明,但还是虚函数
A a1;
A a2 = a1; //可以直接拷贝虚表指针的值
可以直接拷贝续表指针,这两个是同一个类对象,指向的是同一个虚表。
如果是下面这种拷贝:
B b;
A a1 = b; //不能直接拷贝虚表
那就不能直接拷贝虚表指针,因为将派生类对象赋值给基类的对象不会有多态的行为,反而会发生切割,将派生类中的部分去掉,仅仅将基类的部分赋值给a1。这样很明显a1指向的虚表和b不是同一个,a1指向的是基类的虚表。所以是不能直接拷贝的,而要重新设定a1虚表指针的值。
4 处理虚基类的子对象
一个类对象以其派生类的摸个对象作为初值的时候,编译器会生成拷贝ctor,以确保生成正确的虚基类指针和偏移。
1 显示初始化
X x1(x0);
X x2 = x0;
X x1 = X(x0);
程序内部会重写每一个定义,去除初始化操作,并将拷贝ctor的调用放进去。代码转化为以下:
X x1;
X x2;
X x1;
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
2 参数初始化
void func(X x0){ ... }
X xx;
func(xx);
以上代码会把局部实例x0把xx当作初始化的初值,调用ctor初始化x0,然后将其1以引用方式交给函数,这个临时的x0会在函数调用结束后被类内的dtor所析构。
X temp; //创造一个临时变量
temp.X::X(xx); //以xx作为初值初始化
void func(X &x0){ ... } //函数被转换为按引用传值
func(temp); //进行参数传递
3 返回值初始化
X func(){
X xx;
//处理xx
return xx;
}
解决方法是进行一个双阶段转化:(被称作NRV(named return value)
优化)
这里有一个重要的前提:X类内部要实现拷贝ctor,如果没有显式指定拷贝ctor的话,拷贝将无法进行,就没法执行NRV优化了。
编译器对func()
转换出的代码如下:
void func(X &ret){ //返回值变为void,增加一个引用参数
X xx;
//编译器产生的默认ctor
xx.X::X();
//处理xx
ret.X::X(xx);
//编译器所产生的拷贝ctor调用
return ;
}
相关的调用被转换为:
X xx = func(); //原来的代码
func().memfunc(); //memfunc()是X类的成员函数
X (*pf)(); pf = func;
X xx;
func(xx); //转换的代码
X temp;
(func(temp), temp).memfunc();
void (*pf)(X &);
pf = func();
4 使用者层面做优化
X func(const T &y, const T &z){
X xx;
//用y,z来处理xx得到相应的值
return xx;
}
这里在编译器优化的时候,xx会被转换为额外的一个引用参数,所以需要额外的一次拷贝,把xx复制到参数中,如果在X中有相应的构造函数的话,即X::X(const T &y, const T &z);
,就可以减去这一次额外的拷贝。
X func(const T &y, const T &z){
return X(y, z);
}
5 拷贝ctor
当类中没有成员类对象(带有拷贝ctor),没有虚基类或虚函数,那么可以不显式指定拷贝ctor,因为编译器合成的默认拷贝ctor效率很高。
如果需要进行类对象大量的拷贝,那么可以显式指定拷贝ctor,触发NRV优化。
如果类对象中没有隐式产生的内部成员(如虚函数表指针),此时拷贝构造函数可以直接通过memset()
,memcpy()
来执行。
以下几种情况必须使用成员初始化列表:
对于4来说,如果不使用成员初始化列表,效率比较低
class Word{
private:
string name;
int cnt;
public:
Word(){
name = 0;
cnt = 0;
}
};
这样的代码,会导致Word的对象在初始化的时候,首先会生成string
的一个临时对象,并用0对其进行初始化,然后对name进行默认构造(无参),最后通过赋值运算符进行内容的赋值。
Word::Word(/*this pointer*/){ //隐式传入的this指针
//调用string默认构造函数
name.string::string();
//产生临时对象
string temp = string(0);
//拷贝name
name.string::operator=(temp);
//摧毁临时对象
temp.string::~string();
cnt = 0;
}
使用成员初始化列表可以显著提升效率
Word::Word:name(0), cnt(0){ }
这个时候会减少不必要的构造和拷贝过程
Word::Word(/*this pointer*/){ //隐式传入的this指针
name.string::string(0);
cnt = 0;
}
编译器会一一操作初始化列表的值,按照一定的顺序在构造函数之内安插初始化操作,并且在用户代码之前处理。
初始化列表的处理顺序是类中的成员变量声明顺序决定的,和初始化列表中的顺序无关。并且基类的初始化在派生类之前完成。