首页 > 代码库 > 【深度探索C++对象模型】第二章 构造函数语意学(上)

【深度探索C++对象模型】第二章 构造函数语意学(上)

第二章 构造函数语意学(The Semantics of Constructors)
—— 本书作者:Stanley B.Lippman

一、前言
    首先让我们来梳理一个概念:
  • 默认构造函数(Default Constructor) : 是在没有显示提供初始化式时调用的构造函数。它由不带任何参数的构造函数,或是为所有形参提供默认实参的构造函数定义。如果定义的某个类的成员变量没有提供显示的初始化式时,就会调用默认构造函数(Default Contructor)。
    如果用户的类里面,没有显示的定义任何构造函数,编译器就会为该类合成一个默认构造函数。这种说法对吗?
    在 C++ Annotated Reference Manual (ARM)[ELLIS90] 中的 Section 12.1 告诉我们:"default contructors ... 在需要的时候被编译器产生出来"。关键字眼是"在需要的时候"。被谁需要?何时需要?请看下面这段代码:
class Foo
{
public:
    int val;
    Foo *pNext;
};

//... 
void foo_bar()
{
    Foo bar;    // 这里程序要求 bar‘s data members 都被清为 0
    if ( bar.val || bar.pNext )
        // do something
    // ...
}
    在这个例子中,正确的程序语意是要求 Foo 要有一个 default constructor,可以将它的两个 members 初始化为 0。这段代码是否符合上面的 ARM 所说的“在需要的时候”?答案是:NO! 其间的差别在于一个是程序的需要,一个是编译器的需要。程序要如果需要,那是程序员的责任!!!在上面这个例子中,就是 class Foo 的负责人。
    是的,上面的代码并不会合成一个 默认构造函数(default constructor)。
【注】Global objects 的内存保证会在程序激活时被清 0。Local objects 配置于程序的堆栈中,heap objects 配置于自由空间中,都不一定会被清为 0,它们的内容将是内存上次被使用后留下的痕迹。
【再注】上面这一段是原书中的注释,我现在对这翻译疑惑的是,heap objects 不就是配置于堆中吗?local objects 不就是配置于栈中吗?
    那么,在什么时候才会合成一个 默认构造函数 呢? 当编译器需要的时候!而且,被合成出来的 contructor 只执行编译器所需的行动。我们慢慢来理解这句话。
    C++ Standard 已经修改了 C++ Annotated Reference Manual (ARM)[ELLIS90] 的说法,虽然其行为事实上仍然是相同的:
    对于 class X,如果没有任何 用户声明的构造函数( user-declared constructor ),那么会有一个 默认构造函数(default constructor) 被暗中( implicitly ) 声明出来……一个被暗中声明出来的 默认构造函数 将是一个 trivial constructor(没啥用的构造函数)。
    说实话,上面这段话我没有理解,如果大家有更好的解释,请在评论里告诉我。接下来,我们看一下,在什么情况下,编译器会合成出一个有用的(nontrivial)默认构造函数。

二、构造函数的建构
  • "带有 Default Constructor" 的 Member Class Object
    如果一个 class 没有任何 constructor,但它内含一个 成员对象(member class object),而这个成员对象有 默认构造函数,那么这个 class 需要一个 有意义的构造函数(Nontrivial default constructor ) 就需要编译器为其合成出来。
    在这里,我们要牢记,编译器合成的构造函数只满足编译器的需要。来看个例子:
class Foo
{
public:
    Foo(); // 默认构造函数
};
class Bar
{
public:
    // 缺少默认构造函数
    Foo foo;    // Bar 内含了一个 带有默认构造函数的 class : Foo
    char *str;
};

// 测试
void foo_bar()
{
    Bar bar;    // 编译器需要 Bar::foo 在这里初始化
    if ( str ) 
        // ... 
}
    被合成出来的构造函数,只会调用 其内含的 class Foo 的默认构造函数来处理 Bar::foo,但并不会产生任何代码来初始化 Bar::str。将 Bar::foo 初始化是编译器的责任,而 Bar::str 则是程序员的责任!这点要牢记。
    如果 Bar 已经有一个默认的构造函数了,如下:
class Foo
{
public:
    Foo(); // 默认构造函数
};
class Bar
{
public:
    Bar() { str = 0; }    // 默认构造函数,初始化了 str,但是没有初始化 foo
    Foo foo;    // Bar 内含了一个 带有默认构造函数的 class : Foo
    char *str;
};

    如上面这个例子,Bar 的构造函数还是不满足编译器需要,因此,编译器会再合成一个构造函数?答案是NO。
    “如果 class A 内含一个或多个成员对象(member class objects),那 class A 的每一个构造函数 必须调用每一个 成员对象的默认构造函数(default constructor), 调用顺序依照这些 成员对象 在 class A 中的声明次序。”
    编译器会扩张已存在构造函数,具体的做法是,在用户代码之前安插相应代码以满足编译器需要。
    被编译器扩张后的构造函数看起来可能如下: 
Bar()
{
    // 编译器安插的代码
    foo();    // 原书的写法是:foo.Foo::Foo();
    // 用户代码
    str = 0;
}
  • “带有 Default Constructor”的 Base Class
    如果一个没有任何 构造函数 的 class 派生子一个“带有 默认构造函数”的基类,那编译器会为这个 class 合成默认构造函数。它将调用上一层 base classes 的 默认构造函数(根据派生链的顺序,自上而下的调用基类构造函数)。
    如果这个 class 有多个 构造函数,但其中都没有 默认构造函数,编译器则会扩张每一个构造函数,用以“调用所有必要的 默认构造函数”。注意,编译器所做的是扩张,而不是合成一个新的构造函数。
  • “带有一个 Virtual Function”的 Class
    一下两种情况,也需要合成出 默认构造函数。
    1. class 声明(或继承)一个 virtual function。
    2. class 派生自一个继承串链,其中有一个或更多的 virtual base classes。
    在第一章中,我们了解了 c++ 对象模型是如何支持 virtual 的。编译器必须为每一个 object 的 vptr 设定初值,使其指向相关的 vtbl。对于 class 所定义的每一个构造函数,编译器会安插一些代码来做这样的事情,对于没有任何构造函数的 class,编译器会为他们合成一个默认的构造函数,以便初始化类的每一个 object 的vptr。
  • “带有一个 Virtual Base Class”的 Class
    上面一段话已经提过,这里单独说一点:Virtual base class 的实现方法在不同编译器之间存在差异,但他们的共同点在于必须使 virtual base class 在其每一个 derived class object 中的位置,能够于执行期准备妥当。
小结】
    C++ 新手一般有两个常见的误解:
    1. 任何 class 如果没有定义  默认构造函数( default constructor ) ,就会被合成出一个来。
    2. 编译器合成出来的 默认构造函数( default constructor ) 会明确设定“class 内每一个 成员变量 的默认值”。
    实际上,只有种情况会导致“编译器必须为没有任何构造函数的 类 合成一个 默认构造函数”。被合成出来的 默认构造函数只满足编译器需要,而不满足程序需要。
    至于没有上述四种情况同时又没有任何构造函数的类,我们说它们拥有的是 implicit trivial default constructors(隐式的,无用的默认构造函数),它们实际上并不会被合成出来。
引申】
    在 Effective C++ 中,对 默认构造函数(default constructors) 的介绍如下:
    Default constructor 意指可以“无需任何参数就被调用”者。这样的一个构造函数,如果不是没有任何参数,就是每个参数都有默认值。
    (第二章内容较多,本次先更新到这里,下一篇文章里,将会看到拷贝构造函数的建构操作,以及构造函数的初始化列表等等。敬请期待。)