默认构造函数与成员初始化列表

1. 默认构造函数

C++ Annotated Reference Manual (ARM) [ELLIS90] 的 Section 12.1 告诉我们: 默认构造函数在需要的时候被编译器产生出来

“在需要的时候是什么时候?” “被谁需要?” “做什么事情?”

C++ primer plus (第6版) 10.3.3 告诉我们: 如果没有提供任何构造函数, 则 C++ 将自动提供默认构造函数, 他是默认构造函数的隐式版本, 不做任何工作.

这种说法也不能说是错误的, 就是不够精确, 太不精确了, 有太多内容被隐藏掉了! 不过也对得起 primer~( PS:同事dalao说这个plus问题很多, 还不如 C++ primer )

class Foo { public: int cal; Foo* pnext; };

void foo_bar()
{
    // 程序需要 foo 的成员都被清 0
    Foo foo;
    if ( foo.val || foo.pnext )
        // do something
}

这个例子中, 程序的语义要求 Foo 有一个默认构造函数, 可以将两个 member 初始化为 0. 那么上面这段代码符合 ARM 所说的 “在需要的时候?” 么? 回答是 No!

这中间的差别在于一个是程序需要, 一个是编译器需要. 程序需要是程序员的责任, 上面例子中需要承担责任的是设计 class Foo 的人.

那么什么时候才会合成一个默认构造函数呢? 当编译器需要的时候. 另外被合成出来的构造函数只会执行编译器所需要的行动. 对于上面的例子来说, 即使有需要为 class Foo 合成一个默认构造函数, 这个构造函数也不会将 val 和 pnext 初始化为 0.

为了让例子程序正确执行, 类设计者需要明确地提供一个默认构造函数.

C++ Standard 中修改了 ARM 里的说法:
对于 class X, 如果没有任何 user-declared constructor, 那么会有一个 default constructor 被暗中( implicity )声明出来…一个被暗中声明出来的 default constructor 将是一个 trivial(浅薄而无能, 没啥用的) constructor

一个 nontrivial default constructor 在 ARM 的术语中就是编译器所需要的那种, 必要的话会由编译器合成出来. 下面介绍一种 nontrivial default constructor 的情况: 带有 default constructor 的 Member Class Object

如果一个 class 没有任何 constructor, 但是他内含一个 member object 而后者有一个 default constructor, 则这个 class 的 implicit default constructor 就是 nontrivial. 编译器需要为此 class 合成出一个 default constructor. 不过这个合成操作只有在 constructor 真正需要被调用的时候才会发生.

class Foo { public: Foo(), Foo( int ) ... };
class Bar { public: Foo foo; char* str; };

void foo_bar()
{
    Bar bar; // Bar::foo 必须在此处初始化
}

被合成的 Bar default constructor 内含必要的代码, 能够调用 class Foo 的 default constructor 来处理 member object Bar::foo, 但是同样的, 他并不会产生任何代码来初始化 Bar::str. Bar::foo 的初始化是编译器的责任, Bar::str 的初始化则是程序员的责任. 为了让程序正确执行, 我们需要将 str 初始化. 我们可能会写一个这样的构造函数.

Bar::Bar() { str = 0; }

现在程序的需求满足了, 但是编译器却发现了一个新问题: 这里已经有一个明确的 default constructor 被定义出来了, 编译器没法合成第二个了…

这时编译器采取的行动是: 如果 class A 包含一个或一个以上的 member class objects, 那么 class A 的每一个 constructor 必须调用每一个 member classes 的 default constructor. 编译器会扩张已存在的 constructors, 在其中安插一些代码, 使得 user code 在被执行之前, 先调用必要的 default constructor.


2. 成员初始化列表

下面四种情况必须使用成员初始化列表

  1. 当初始化一个 reference member 时;
  2. 当初始化一个 const member 时;
  3. 当调用一个 base class 的 constructor, 而他拥有一组参数时;
  4. 当调用一个 member class 的 constructor, 而他拥有一组参数时.

看下面这段代码

class Word
{
private:
    String _name;
    int    _cnt;

public:
    Word() {
        _name = 0;
        _cnt = 0;
    }
};

没有错误, 但是效率不好. 这里 Word 的 constructor 会先产生一个暂时性的 String object, 然后将它初始化, 再用一个 assignment 运算符将暂时性的 object 指定给 _name, 然后再销毁这个暂时的 object.

// C++ 伪代码
Word::Word( /* this pointer goes here*/ )
{
    // 调用 String 的 default constructor
    _name.String::String();

    // 产生暂时性对象
    String temp = String( 0 );

    // 拷贝 _name
    _name.String::operator=( temp );

    // 摧毁暂时性对象
    temp.String::~String();

    _cnt = 0;
}

一种更好的写法是:

Word::Word() : _name( 0 ) {
    _cnt = 0;
}

// C++ 伪代码
Word::Word( /* this pointer goes here*/ )
{
    // 调用 String(int) constructor
    _name.String::String(0);
    _cnt = 0;
}

需要注意的是 "初始化次序"和"初始化列表中项目的排列次序"无关, 它是由成员在类中声明的次序决定的.

class X
{
    int i;
    int j;

public:
    X( int val )
        : j( val ), i( j )
    { }
};

class X 构造函数的问题在于代码看起来是想把 j 的初始值设置为 val, 再把 i 的初始值设为 j, 但是由于声明次序, 初始化列表中的 i( j ) 实际上在 j( val ) 之前执行, i 会变成一个无法预期的值. 这种错误极其隐蔽, 虽然有的编译器可以正确编译, 但是程序运行时则可能会导致无法预测的行为.

写代码时应该避免这种用法, 时刻谨记编译器太过于按照字面意义来解释你的意图, 而没有在背后为你做更多事. 学习这一知识, 了解其背后的原理, 然后在实践中忘记这种 “怪异” 的做法吧, 让他停留在专业书籍里就好!

// 更好的做法
X::X( int val )
    : j( val )
{
    i = j;
}

这里还有一个有趣的问题不过可能很难意识到~ 初始化列表中的项目被安插到构造函数里的时候还会保持声明次序么? 因为我们都知道上面的做法是正确的, 所以问题的答案是不会. 这是由于初始化列表的项目会被放在 explicit user code 之前, 这一规则要优先于"按照成员在类中声明的次序进行初始化".

与上面的错误做法类似:

  1. 用成员函数的返回值来设定一个成员的初值.
  2. 用派生类的成员函数的返回值作为基类构造函数的参数.

这些都是符合C++语法的, 但是很明显, 这些奇奇怪怪的写法可读性极差, 而且维护困难. 难以确定这个方法与类的成员之间到底有多么紧密的联系, 也就很难保证正确性. 第二种也暴露出类设计上的不合理. 所以在实际编码中也应该极力避免.

简略地说, 编译器会对初始化列表一一处理, 并且可能重新排序以反映出成员的声明顺序. 他会安插一些代码到构造函数体内, 并置于任何 explicit user code 之前.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值