跳转至

📔 Chapter02 构造函数语意学 读书笔记

2.1 Default Constructor 的构建操作

C++ Annotated Reference Manual(ARM)中解释:默认构造函数 是在需要是被编译器生成出来。

  • 重点:谁需要?做什么事情?
    • 程序的需要:是程序员的责任。
    • 编译器需要时,默认构造函数会在调用操作发生时,被编译器生成,而被生成的默认构造函数只会执行程序所需要的动作。

对于一个类来说,如果没有用户自声明构造函数,那么编译器则会默认声明一个隐式默认构造函数,而这个implicit default constructor 在C++ standard 中被称为 nontrivial default constructor。而对于这种构造函数则是编译器所需要而生成,而不是程序所需要。

对于member的初始化,Global objects的内存保证会在程序激活时被清为0,Local Object 分配到程序的堆栈中,heap object 分配到自由空间中,都不一定会被清为0,他们的内容都是内存上次使用后的痕迹。

2.1.1 nontrivial default constructor 的四种情况

  • 带有 Default Constructor 的 member class object

    • 只有构造器真正被调用时才会生成一个default constructor。

    • C++中各个不同的编译模块中,编译器避免合成多个default constructor的方式:把合成的 default constructorcopy constructordestructorassignment copy operator 都是以 inline 的方式完成。

      • 一个 inline函数中有静态链接,不会被文件外可见。
      • 如果函数复杂,则不适合使用inline方式,则生成一个 explicit non-inline static实体。
    • 如果已显式地声明了constructor,则编译器会对已有的constructor进行扩展,在其中安插一些代码,使得user code在被执行前,优先调用必须要的default constructor。

    • 如果一个class中包含多个class member objects 都要求使用constructor 初始化操作,那么C++语言要求以 member object 在class 声明次序 来进行对各个constructor的调用。声明的顺序已经确定了初始化的顺序。

  • 带有 default constructor 的 Base Class

    • 如果base class中包含default constructor,派生出的class没有一个constructor,则这个派生类也叫做nontrivial
  • 对于派生类,则直接调用 base classdefault consturctor(根据声明的顺序来进行),对于派生类来说,如果还存在其他的default constructor 需要调用,则会在base class之后才可以被调用。

  • 带有 virtual Function 的 class

    • 另有两种情况,也需要生成 default constructor

      • class声明或者继承一个virtual function。

        • 编译器会生成一个virtual function table(在cfront中称为vbtl),存放class的 virtual function的地址。
      • class 派生自一个继承串链,其中一个或者更多的virtual base class。

        • 编译器会在每一个class object 生成一个额外的 pointer member,内含相关的class vtbl的地址。
  • 带有 virtual Base Class 的 class

    • virtual base class在不同的编译器中的实现是不同的。但有一个共同点:必须是virtual base class 在其每一个deprived class object 中的位置,能够在执行期准备妥当。
1
2
3
4
5
6
7
8
9
class X {public: int i;};
class A : public virtual X {public: int j;};
class B : public virtual X {public: double d;};
class C : public A, public B {public: int k;};

// 无法在编译期间确定出 pa->X::i 所在的位置
void foo(const A* pa) {
  ps->i = 1024;
}

2.1.2 总结

在生成的default constructor 中,只有base class subobjectsmember class object被初始化 ,所有其他的 非静态data member(如整数、整数指针、整数数组)都不会被初始化 。初始化操作对程序来说有必要,但是对编译器来说并非必要,所以这是程序员的责任。

2.1.3 C++新手一般有两个常见的误解

  • 任何class 如果没有定义 default constructor,就会被编译器生成一个。
  • 编译器生成出来的default constructor 会明确设定 “class 内每一个data member 的默认值”。

2.2 Copy Constructor的构建操作

以一个object的内容作为另一个class object 的初始值,分为如下三种情况:

class X { ... };
X x;
// 情况1:显式设置初始值
// 直接将一个object的内容作为另一个class object 的初始值
X xx = x;

// 情况2:隐式设置初始化
// 将 object 当作参数传给某个函数
extern void foo(X x);

void bar()
{
  X xx;
  foo(xx);
}

// 情况3:隐式设置
// 当一个函数返回一个class object时
X foo_bar()
{
  X xx;
  return xx;
}

如果class 设计者明确定义了 copy constructor,那么如上三种情况的初始化,则会直接调用。否则,则是使用逐成员初始化的方式来完成。

2.2.1 逐成员初始化

  • 定义

    • 将每一个内建或者派生的data member 的值,从某个object拷贝一份到了一个object上,但不会拷贝其中的member class object,而是以递归的方式执行逐个成员初始化。
  • default constructor 和 copy constructor 都是在必要时才会由编译器来产生。

  • 一个class object由两种方式获取:

    • 被初始化
      • 拷贝构造来完成
    • 被指定
      • 拷贝赋值构造来完成

对于拷贝构造函数,C++ Standard 也区分 trivial 和 nontrivial 两种。

2.2.2 按位拷贝

对于大部分的class来说,拷贝构造函数就直接使用 按位拷贝 的方式即可。

class不展现出 按位拷贝 操作的四种情况:

  • 当class内含有一个声明了copy constructor的 class member object。
  • 当class继承自存在一个copy construstor的base class。
  • class声明一个或者多个virtual function
  • class派生自一个继承串链,其中有一个或者多个virtual base class。

对于前2种,编译器必须将member 或者 base class 的 copy constructor调用操作 安插到被生成的copy constructor。

2.2.3 重设 virtual table的指针

对于情况3,编译期间的两个程序扩展操作(只有一个class声明了一个或者多个virtual function):

  • 增加一个vtbl,内含每一个有作用的virtual function的地址
  • 将一个指向vtbl的指针vtpr,安插在每一个class object内。

编译器导入一个vtpr到class之中时,此时class不再是bitwise语义。所以编译器就需要生成一个copy constructor 以求将 vptr 适当地初始化

2.2.4 处理virtual Base class subobject

对于情况4,一个class object 如果以另一个class object 作为初值,而后者有一个virtual base class subobject,那么也会使得bitwise copy semantics 失效。

对虚拟继承的每个实现支持,都需要让每个虚拟基类的子对象在派生类对象在运行时可用。维护位置完整性是编译器的工作。按位拷贝语义会直接破坏其位置完整性,所以编译器必须使用其自生成的拷贝构造函数进行仲裁。

2.3 程序转化语义学

2.3.1 显式初始化

程序转化的过程分为两个方面:

  • 重写每一个定义,剔除初始化操作。
  • 插入对类 copy constructot 的调用
X x0;

void foo_bar(){
    X x1(x0);
    X x2 = x0;
    X x3 = x(x0);
    // ...
}

// 程序转化后
void foo_bar() {
    // 定义被重写,初始化的操作会被剔除
    X x1;
    X x2;
    X x3;

    // 编译器会插入类X 的copy constructor的 调用操作
    x1.X::X(x0);
    x2.X::X(x0);
    x3.X::X(x0);
    // ...
}

2.3.2 参数的初始化

使用两种策略:

  • 策略1:导入临时性object,并调用copy constructor 将其初始化,然后将临时性的object交给函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void foo(X x0);
    
    X xx;
    foo(xx);
    
    // 程序代码转换后
    X __temp0; // 编译器产生的临时对象
    __temp0.X::X(xx); // 编译器对copy constructor的调用
    foo(__temp0); // 重写函数调用,便于调用临时对象
    

  • 策略2:以拷贝构造的方式将实际参数直接拷贝到堆栈上函数激活记录中的位置上。在函数返回之前,如果定义了局部对象析构函数,则应用于该对象。

2.3.3 返回值的初始化

双阶段转换:

  • 向类对象添加一个引用类型的额外参数,此实参将用来保存构造的 “返回值” 副本。
  • 在return语句之前插入对copy constructor的调用,用返回对象的值初始化,作为上述实参。
X bar()
{
    X xx;
    return xx;
}

// 转换后
void bar(X& __result) {
    X xx;

    xx.X::X(); // 编译器生成的默认构造函数的调用操作
    __result.X::X(xx); // 编译器生成的 拷贝构造函数 的调用操作
    return;
}

2.3.4 在使用者好的角度优化

定义一个 ”计算用“ 的constructor,用来提高效率。

// 原写法:
X bar(const T &y, const T &z) {
  X xx;
  return xx;
}

// 直接定义另一个constructor后,改写为:
X bar(const T &y, cosnt T &z){
    return X(y, z);
}

// 被编译器转化为:
void bar(X &__result, const T &y, const T &z) {
    __result.X::X(y, z); // __result 被直接计算出来,而不是经由copy constructor拷贝而来。
    return;
}

2.3.5 编译器级别的优化

类似 bar() 这种函数,所有的return语句都是返回相同的命名值,编译器可以通过result参数来替换命名的返回值来优化函数。

// 原函数
X bar() {
    X xx;
    return xx;
}

void bar(X & __result) {
    __result.X::X(); // 默认构造函数的调用
    return; // 直接返回 __result
}
这种编译器优化操作,也称为 Named Return Value(NRV)优化

对于NVR优化,虽然提高了效率,但是也饱受批评:

  • 优化由编译器默默完成,但是否真的被完成不得而知。
  • 一旦函数变复杂,优化就变得难以实施。
  • 对称性角度:优化打破了 copy constructor 和 destructor 的对称调用。

2.4 成员初始化列表

必须使用member initialization list 的四种情况:

  • 当初始化一个 reference member
  • 当初始化一个 const member
  • 当调用一个 base class 的constructor,而它有一组参数时
  • 当调用一个 member class 的constructor,而它有一组参数时

编译器会对initialization list 一一处理并可能重新排序,以反映出members的声明顺序。它会安插一些代码到constructor体内,并置于任何explicit user code之前。