EffectiveC++详解:条款04-确定对象被使用前已被初始化

文章目录

  • 条款04-确定对象被使用前已被初始化
    • 1. 切勿混淆赋值和初始化
    • 2. 内置类型的赋值操作移到函数里,以避免重复的工作
    • 3. 成员初始化次序
      • 3.1 类的成员初始化次序
      • 3.2 不同编译单元内的 `non-local static` 对象的初始化次序
    • 4. 总结

@Author:CSU张扬
@Email:[email protected] or [email protected]
@我的网站: https://www.cppbug.com

条款04-确定对象被使用前已被初始化

对象的初始化动作何时一定发生,何时不一定发生,这些规则太过复杂。最佳的处理办法就是:永远在使用对象之前将它初始化。

  • 对于内置类型,我们必须手动完成初始化。例如:

      int x = 0;
      const char *str = "A C-style String";
    
      double d;
      std::cin >> d;  // 以读取输入流的方式初始化
    
  • 对于其他类型,初始化由构造函数完成,我们要确保每一个构造函数都将对象的每一个成员初始化。

1. 切勿混淆赋值和初始化

例如:

class PhoneNumber { ... };
class ABEntry {
public:

private:
    string theName;
    list<PhoneNumber> thePhones;
    int num;
}
ABEntry::ABEntry(const string& name, const list<PhoneNumber>& phones) {
    theName = name;     // 这些是赋值,而非初始化
    thePhones = phone;
    num = 0;
}

对象的成员变量的初始化动作发生在构造函数本体之前(即 { 之前)。所以上例中这些是赋值,而非初始化。例如 theName 它会先调用 string 类的默认构造函数,再对它进行赋新值。

构造函数应该使用成员初值列代替赋值动作,下面的 theName 直接调用 string 类的拷贝构造函数。

ABEntry::ABEntry(const string& name, const list<PhoneNumber>& phones) :
                 theName(name), thePhones(phone), num(0) { }
  • 对于大多数类型而言,先调用默认构造函数再赋值 没有 只调用一次拷贝构造函数高效。
  • 对于内置对象而言,初始化和赋值的成本相同。

注意: 如果成员变量是 const 或者是 引用,就更不能用赋值。

2. 内置类型的赋值操作移到函数里,以避免重复的工作

许多类拥有多个构造函数,如果某个类含有许多成员变量和基类,多份成员初值列就会导致重复的工作。在成员初值列中合理的遗漏那些 “赋值和初始化成本相同” 的成员变量,并将这些 赋值操作 移动到某个函数里(通常是 private),供所有构造函数使用。

在 成员变量的初值由文件或者数据库导入时 特别适用。

3. 成员初始化次序

3.1 类的成员初始化次序

  1. 基类更早于其派生类初始化。
  2. 类的成员变量初始化次序和声明次序相同,与成员初值列的次序无关。

例如 array 必须要知道维度大小,所以一定要保证次序。

3.2 不同编译单元内的 non-local static 对象的初始化次序

函数内的静态对象是 local static,其他静态对象是non-local static

假设在两个文件里,每个都至少含有一个 non-local static 对象。若某文件里的 non-local static 对象的初始化使用了另一个文件里的 non-local static 对象,它所用到的这个对象可能未被初始化,因为 C+++ 对于 “定义于不同编译单元内的对象” 的初始化次序没有明确规定。

例如:

  1. 某个文件系统:

    class FileSystem {
    public:
        ... ...
        size_t numDisks() const;
        ... ...
    };
    extern FileSystem tfs;
    
  2. 客户用于处理文件系统的目录

    class Directory {
    public:
        ... ...
        Directory( params ) {
            ... ...
            size_t disks = tfs.numDisks();
            ... ...
        }
    };
    Directory tempdir( params );
    

这里的初始化的次序非常重要:除非 tfstempDir 之前先被初始化,否则 tempDir 的构造函数会用到尚未初始化的 tfs。我们如何确定 tfs 会在 tempDir 之前被初始化呢?

一个解决办法是:将每个 non-local 对象搬到自己的专属函数内(该对象在此函数内被声明为 static),函数返回一个引用绑定它所含的对象。然后用户调用这个函数,而不是直接使用其中的对象。(Singleton模式

C++保证函数内的 local static 对象会在 调用该函数时,首次遇到该表达式时,被初始化。

这样不仅解决了初始化次序问题,并且如果你从未调用这个函数(从未有需求使用这个对象),就绝不会引发构造和析构的成本。真正的 non-local static 对象可不会这样。

我们修改代码:

    class FileSystem {
    public:
        ... ...
        size_t numDisks() const;
        ... ...
    };
    FileSystem& tfs() {
        static FileSystem fs;
        return fs;
    }

    class Directory {
    public:
        ... ...
        Directory( params ) {
            ... ...
            size_t disks = tfs().numDisks();
            ... ...
        }
    };
    Directory& tempdir() {
        static Directory td;
        return td;
    }

当然这种 reference-returning 函数可以,避免初始化次序问题,前提是两个对象有着合理的次序。如果对象A必须在对象B前初始化,但A的初始化又受制于B是否已经初始化,这种情况无法解决,应该避免这种情况发生。

4. 总结

  • 为内置类型进行手动初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。成员初值列的成员变量的次序应该和声明次序相同。
  • 为了避免 “不同编译单元的初始化次序” 问题,用 reference-returning 函数内的 local static 对象替代 non-local static 对象。

你可能感兴趣的:(EffectiveC++详解)