深度探索C++对象模型-第二章

构造函数语意学

一 默认ctor的构造操作

在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。

二 拷贝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 位逐次拷贝

在以下四种情况中:

  1. 类的成员包括一个类对象的时候(如string对象),这个对象有拷贝ctor(无论是显示声明还是编译器默认生成)
  2. 类继承自一个拥有拷贝ctor的基类(无论是显示声明还是编译器默认生成)
  3. 类声明虚函数
  4. 类有虚基类

编译器会合成一个拷贝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优化了。

  1. 首先加上一个额外参数,类型是类对象的一个引用,这个参数将用来放拷贝构造出来的返回值,将返回值改成void类型
  2. 在return前返回一个拷贝ctor的调用,将传回的对象的内容作为上述新增参数的初值。

编译器对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()来执行。

四 成员们的初始化队伍

以下几种情况必须使用成员初始化列表:

  1. 初始化引用成员变量
  2. 初始化const成员变量
  3. 调用基类的构造函数,并且拥有参数的时候
  4. 调用成员类对象的构造函数,并且拥有参数的时候

对于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;
}

编译器会一一操作初始化列表的值,按照一定的顺序在构造函数之内安插初始化操作,并且在用户代码之前处理。

初始化列表的处理顺序是类中的成员变量声明顺序决定的,和初始化列表中的顺序无关。并且基类的初始化在派生类之前完成。

你可能感兴趣的:(Inside,the,C++,Object,Model,深度探索C++对象模型)