一、默认构造函数
如果一个类中不存在默认构造函数,那么它会在被编译器需要的时候由编译器生成,且该默认构造函数只能满足编译器所需的行动,是trivial constructor。
Tips:类中成员变量的初始化是程序的需要,而不是编译器的需要。
1..一个类没有默认构造函数且类中含有带有显示默认构造函数的成员子对象
该类被编译器生成的默认构造函数是nontrivial constructor
例1
class Foo{
public:
Foo();
Foo(int);
...
};
class Bar{
public:
Foo foo;
char* str;
}
int main(int){
Bar b; //在需要的时候编译器为Bear类生成一个默认构造函数
if(b.str){...}
return 0;
}
在上面的代码中。将b.foo对象初始化是编译器的责任(需要),而将b.str初始化是程序的需要(编译器不管),则为其生成的默认构造函数为
inline Bar::Bar(){
//伪代码
foo.Foo::Foo();
}
为了成员str的初始化,假设显式写了一个默认构造函数,编译器就不会再生成一个默认构造函数了
Bar::Bar(){
str = 0;
}
这时满足了程序的需要,而编译器的需要还未满足,则编译器还会对该函数进行扩展,具体的做法是:在执行用户代码之前,按照类中成员子对象声明 的顺序,依次调用其默认构造函数
则扩展后的函数变为
Bar::Bar(){
foo.Foo::Foo();
str = 0;
}
2. 一个类的基类有显式的默认构造函数
该类被编译器生成的默认构造函数同样是nontrivial constructor
假设类中有一个显式的默认构造函数,被编译器扩张后,在执行用户代码之前,先按继承表的顺序依次调用基类的默认构造函数,若类中还包含成员子对象,则这些子对象的默认构造函数会被编译器安插到基类子对象之后执行。
若类中有非默认构造函数(编译器不会再生成默认构造函数),编译器会扩张每一个非默认构造函数。
3.一个类带有虚函数
还有两种情况编译器会生成默认构造函数
1.类中声明或继承虚函数
2.类派生自一个虚拟继承链,其中有一个以上的虚基类
对于情况1:在编译期间,会生成虚表来存放虚函数的地址,编译器为每一个类对象生成虚指针,指向虚表。
编译器会扩张每个构造函数,来初始化虚指针。
4.一个类带有虚基类
假设某编译器以虚基类表来实现虚基类,编译器会扩张每个构造函数来初始化虚基类表指针
Tips:并不是任何没有默认构造函数的类都会被编译器来生成一个
编译器生成的默认构造函数并不会初始化成员变量
二、拷贝构造函数
发生拷贝构造的几种情况:以一个对象对另一个对象做明确的初始化,传入参数,函数返回。
1.默认拷贝构造
若一个类中没有提供显式的拷贝构造函数的时候,编译器会在需要的时候提供一个默认拷贝构造函数,对类中每一个成员变量进行按值的拷贝,对其中的成员子对象以递归的方式进行拷贝。
2.逐位拷贝:两块内存区域的逐位数据复制
逐位拷贝的语境不需要编译器为其产生一个拷贝构造函数。
一个展现bitwise语境的类,如:
class Word{
public:
Word(const char* );
~Word();
private:
int cnt;
char *str;
};
不会产生逐位拷贝的情况:
1.当类中存在成员子对象且成员子对象声明有拷贝构造函数(不论是声明的或是编译器给生成的)。
2.当某个类的基类有拷贝构造函数(不论是声明的或是编译器给生成的)。
3. 类中声明有一个或者多个虚函数。
4. 一个类派生自一个继承串链,有一个或者多个虚基类。
3.重新设定指向虚表的指针
一个类中若含有虚指针,当其子类对象为该类对象初始化的时候,这个类类就不再展现bitwise语境(因为如果这样的话,父类对象的虚指针会指向子类的虚表),则编译器需要生成一个拷贝构造函数以便使虚指针更好的初始化。
4.处理虚基类子对象
一个类对象以另一个类对象作为初值且后者中具有基类子对象,也会使bitiwse语境失效。
三、程序转化语义
1.参数的初始化
把一个类对象当作一个实参传给一个函数(或从函数返回一个类对象),实际上发生了拷贝。
当以实参传入的时候,要求函数体内的局部对象以memberwise的方式。
2.返回值的初始化
四、成员初始化
必须使用初始化表的情况:
1.初始化一个引用对象的时候;
2.初始化一个常量对象的时候;
3.调用基类子对象的非默认构造函数;
4.调用成员子对象的非默认构造参数;
初始化列表的效率问题:
class Word{}
public :
Word(){
cnt = 0;
name = 0;
}
private:
string name;
int cnt ;
;
上面代码效率是低下的,构造函数中会产生一个string匿名对象,再将该对象赋值给name
代码扩展之后
Word::Word(){
name.string::string();
string temp = string(0);
name.string::operator=(temp);
temp.string::~string();
cnt = 0;
}
当使用初始化列表之后
Word::Word():name(0){
cnt=0;
}
代码扩张之后
Word::Word(){
name.string::string(0);
cnt=0;
}
实质上:编译器会把初始化表中的变量按照在类中声明的次序安插在构造函数的用户代码之前。
由于这个特性,为了避免不必要的麻烦,应该两个有关联的成员初始化操作放到构造函数体中,如:
class X{
int i;
int j;
public:
X(int val):j(val){
i = j;
}
};
//编译器扩展之后变为
X(int val){
j = val;//初始化表中的操作安插在构造函数的用户代码之前。
i = j;
}
还有另外一个问题
X::X(int val):i(X::foo(val)),j(val){}
这样虽然是合法的,但是不见尾这样用:因为你并不知道成员函数foo()对X对象的依赖程度有多高。
代码会被扩展成
X::X(){
i = this -> foo(val);
j = val;
}
这时,并不能保证this指向的对象构造完整,而foo函数也可能用到类内一些尚未初始化的成员变量。(所谓的依赖)