1️⃣ 问题引入

  • 在不同的语境下,对象是否会被初始化时不一定的 !
  • int x;
    • 如果 x是全局变量,则会被初始化为 0 ; 如果 x是局部变量,则不会被初始化。
  • 类相关
    1
    2
    3
    4
    5
    6
    class Point
    {
    int x , y;
    }
    ···
    Point P;
    • 同理的,p的成员变量有时候被初始化为 0,有时候不会。
  • 为什么要初始化对象 ?
    • 读取未初始化的值会导致不明确的行为。
    • 在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。

2️⃣ 最佳处理方式

  • 规则一:永远在使用对象之前就先将它初始化
    • 对于内置类型,必须手工完成 ;对于类而言,初始化的责任就落在构造函数将对象的每一个成员都初始化。
  • 规则二:总是在成员初值列中列出所有的变量,使得记住还有哪些成员变量。

3️⃣ 别混淆 “赋值” 和 “初始化”

  • 案例引入
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class PhoneNumber {... };

    class ABEntry //ABEntry = "Address Book Entry"
    {
    public:
    ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>&phones);

    private:
    std::string theName;
    std::stringtheAddress;
    std::list<PhoneNumber>thePhones;
    int numTimesConsulted;
    };

    ABEntry::ABEntry(const std::string& name, const std::string& address,
    const std::list<PhoneNumber>&phones)
    {
    theName = name; //这些都是赋值(assignments),
    theAddress = address; //而非初始化(initializations)。
    thePhones = phones;
    numTimesConsulted = 0;
    }
  • 解析
    • 这会导致ABEntry对象带有指定的值,但却不是最佳做法。
    • 原因
      • C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前 。
      • ABEntry构造函数内,theNametheAddressthePhones都不是被初始化,而是被赋值 。
      • 初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时,比进入ABEntry构造函数本体的时间更早 。
      • 但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值 。
  • 最佳写法
    • 使用 member initialization list 替换复制动作 。
      1
      2
      3
      4
      5
      6
      ABEntry::ABEntry(const std::string& name, const std::string& address,const std::list<PhoneNumber>&phones)
      : theName(name), //现在,这些都是初始化(initializations)
      theAddress(address),
      thePhones(phones),
      numTimesConsulted(0)
      {} //现在,构造函数本体不必有任何动作
    • 构造函数和上一个的最终结果相同,但通常效率较高。
    • 原因
      • 赋值版本
        • 首先调用default构造函数为theNametheAddressthePhones设初值, 然后再调用copy assignment
      • 成员初始化版本
        • 初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。
        • 本例中的theNamename为初值进行copy构造,theAddressaddress为初值进行copy构造,thePhonesphones为初值进行copy构造。

4️⃣ 成员初始化次序

  • 定义
    • C++中有着十分固定的 “成员初始化次序” 。
  • 次序
    • base classes更早于其derived classes被初始。
    • class的成员变量总是以其被初始化。

5️⃣ 不同编译单元定义的 “ 非局部静态变量” 的初始化次序

  • static对象
    • 定义
      • 所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack
        heap-based对象都被排除。
    • 种类
      • local static对象 <——> 函数内的static对象
      • non-localstatic对象 <——> 非函数内的static对象。
    • 生命周期
      • 程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
  • 编译单元
    • 是指产出 ”单一目标文件“ 的 ”源码“ 文件。

关心的问题

  • 涉及两个源码文件,每一个内含至少一个non-local static

  • 关键的原因

    • 如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-localstatic对象,它所用到的这个对象可能尚未被初始化,因为C++对定义于不同编译单元内的non-localstatic对象”的初始化次序并无明确定义。
  • 案例引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class FileSystem 
    {
    public:
    ···
    std::size_t numDisks () const; //众多成员函数之一
    ···
    };

    extern FileSystem tfs; //预备给客户使用的对象;
    //tfs代表"the file system"

    FileSystem对象绝不是一个稀松平常无关痛痒的(trivial)对象,因此你的客户如果在theFileSystem对象构造完成前就使用它,会得到惨重的代价。

    现在假设某些客户建立了一个class用以处理文件系统内的目录(directories)。很自然他们的class会用上theFileSystem对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Directory    //由程序库客户建立
    {
    public:
    Directory(params);
    };

    Directory:: Directory(params);

    std::size_t disks = tfs.numDisks(); //使用tfs对象

    进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件

    1
    Directory tempDir(params);    //为临时文件而做出的目录

    😈😈😈

    现在,初始化次序的重要性显现出来了

    • 除非tfstempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfstempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-localstatic对象。如何能够确定tfs会在tempDir之前先被初始化?
  • 原理

    • 无法确定,因为C++对 “定义于不同的编译单元内的non-localstatic
      对象” 的初始化相对次序并无明确定义。
    • 这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。
    • 在其最常见形式,也就是多个编译单元内的non-local static对象经由“模板隐式具现化,implicit template instantiations”形成(而后者自己可能也是经由“模板隐式具现化”形成),不但不可能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。
  • 解决措施

    • Singleton 模板的实现
      • C++保证,函数内的local static对象会在 “该函数被调用期间” “首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没这等便宜!