zoukankan      html  css  js  c++  java
  • 条款04:确定对象使用前已被初始化

    1. 总结

    • 无论是在初始化列表中,还是在构造函数体内,请为内置类型对象进行手工初始化,因为C++不保证初始化它们
    • 最好使用初始化列表进行初始化,而不要在构造函数体中使用赋值;初始化列表最好列出所有的成员变量,其排列顺序应该和它们在class中的声明顺序相同
    • 为了避免"不同源文件内定义的non-local static对象在编译时的初始化顺序"问题,请以local static对象替换non-local static对象

    2. 构造函数体 VS 初始化列表

    在C++中,关于对象的初始化动作何时一定发生,何时不一定发生这个问题,最佳的处理办法就是:永远在使用对象之前先将它初始化。

    • 对于内置类型对象,由于C++不保证是否初始化以及何时初始化它们,因此无论是在初始化列表中,还是在构造函数体内,你必须手工完成这项工作
    • 对于自定义类型对象,初始化工作由构造函数进行,规则也很简单:确保每一个构造函数都将对象的每一个成员初始化

    关于在构造函数中初始化,重要的一点是不要混淆了赋值和初始化。

    class PhoneNumber { ... };
    class ABEntry
    {
    private:
        std::string theName;
        std::string theAddress;
        std::list<PhoneNumber> thePhones;
        int numTimesConsulted;
    public:
        ABEntry{const std::string &name, const std::string &address,
                const std::list<PhoneNumber> &phones};
    };
    
    /* 正确可行但不是最好的方法:在构造函数体内对成员变量进行赋值 */
    ABEntry::ABEntry{const std::string &name, const std::string &address,
                     const std::list<PhoneNumber> &phones}
    {
        theName = name;         //theName、theAddress、thePhones都是赋值,
        theAddress = address;   //而不是初始化。
        thePhones = phones;
        numTimesConsulted = 0;
    }
    
    /* 较好的方法:使用初始化列表对成员变量进行初始化 */
    ABEntry::ABEntry{const std::string &name, const std::string &address,
                     const std::list<PhoneNumber> &phones}
            :theName(name),
             theAddress(address),
             thePhones(phones),
             numTimesConsulted(0)  //为了一致性,内置类型对象初始化最好也在初始化列表中进行
    {
    
    }
    
    • 第一种方法基于赋值,首先调用default构造函数对theName、theAddress和thePhones进行初始化,然后进入构造函数,再分别对它们进行赋值。
    • 第二种方法基于初始化列表,在初始化列表中使用3个参数分别对theName、theAddress和thePhones进行copy构造初始化。

    对大多数类型而言,比起先调用default构造函数然后再调用operator =,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。
    而对于内置类型对象如numTimesConsulted,其初始化和赋值的成本是一样的,但为了一致性最好也通过初始化列表来初始化。

    如果成员变量是const或reference,那么不管是内置类型还是自定义类型,都一定需要初值,不能被赋值,都只能通过初始化列表进行初始化(见条款5)。
    为避免需要记住何时必须使用初始化列表,何时不需要,最简单的做法就是:

    • 总是使用初始化列表
    • 总是在初始化列表中列出所有成员变量
    • 初始化列表中成员变量的排列顺序应该和它们在class中的声明顺序相同

    但是,一些class有多个构造函数,而且有许多成员变量和/或base class,如果每个构造函数都使用初始化列表,那么就会造成大量的代码重复。
    这种情况下,可以合理地对"赋值和初始化开销一样"的成员变量改用赋值操作,并将这些赋值操作封装到一个private init函数中,供所有构造函数调用。
    这种做法在"成员变量的初始值来自于文件或数据库读入"时特别有用。然而,比起经由赋值操作完成的"伪初始化",通过初始化列表完成的"真正初始化"通常更加可取。

    3. 对象的初始化顺序问题

    C++在单个对象创建时有着十分固定的成员初始化顺序,口诀就是"先父母,再客人,后自己"。

    • 先调用父类的构造函数
    • 再调用成员变量的构造函数,调用顺序与声明顺序相同
    • 最后调用类自身的构造函数

    如果已经在初始化列表中对base class和所有成员变量进行了初始化,那就只剩下一个问题——"不同源文件内定义的non-local static对象"的初始化问题。
    先来明确下概念,函数内定义的static对象称为local static对象,其他地方定义的static对象称为non-local static对象。
    现在,我们关心的问题涉及至少两个源文件,每个源文件中都至少含有一个non-local static对象,因此可能发生如下问题。

    • 某个源文件中的non-local static对象初始化需要使用另一个源文件中的non-local static对象
    • 但另一个源文件内的non-local static对象可能尚未被初始化

    产生该问题的原因是C++对不同源文件中的non-local static对象初始化顺序没有明确定义,幸运的是通过一个小小的设计便可完全消除该问题,唯一需要做的是:

    • 将每个non-local static对象放到自己的专属函数中,这些函数返回一个reference指向它所含的对象
    • 然后用户调用这些专属函数,而不直接使用这些对象

    该方法实际上是用local static对象替换了non-local static对象,这也是Singleton模式的一个常见实现手法。该方法之所以管用,是因为:

    • C++保证函数内的local static对象会在该第一次调用该函数时被初始化
    • 如果你从未调用过这些函数,就不会引发构造和析构成本

    可以看到,这种结构下的函数体往往十分简单固定:第一行定义并初始化一个local static对象,第二行返回一个引用指向它。
    这使得它们非常适合实现为inline函数,尤其是需要被频繁调用的场合;但从另一个角度看,内含static对象也使得它们成为线程不安全函数。

    class FileSystem { ... };
    
    //static FileSystem tfs;  //FileSystem.cpp中定义的non-local static对象
    
    //tfs的专属函数,用来替换tfs对象
    FileSystem &tfs()
    {
        static FileSystem fs;  //定义并初始化一个local static对象fs
        return fs;             //返回一个reference指向上述对象
    }
    
    class Directory { ... };
    
    Directory::Directory()
    {
        //...
        std::size_t disks = tfs().numDisks();
        //...
    }
    
    //static Directory tempDir;  //Directory.cpp中定义的non-local static对象,tempDir的初始化依赖于FileSystem.cpp中的tfs对象先初始化完成
    
    //tempDir的专属函数,用来替换tempDir对象
    Directory &tempDir()
    {
        static Directory td;  //定义并初始化一个local static对象td
        return td;            //返回一个reference指向上述对象
    }
    
  • 相关阅读:
    SpringBoot使用过滤器、拦截器、切面(AOP),及其之间的区别和执行顺序
    发送POST请求,包含文件MultipartFile参数,普通字符串参数,请求头参数
    Linux安装Mongodb(附带SpringBoot整合MongoDB项目Demo)
    博客目录
    Ubuntu+Hexo+Github搭建个人博客
    Hexo+Github搭建个人博客
    Linux设备驱动程序学习----3.模块的编译和装载
    Linux设备驱动程序学习----2.内核模块与应用程序的对比
    Linux设备驱动程序学习----1.设备驱动程序简介
    Linux设备驱动程序学习----目录
  • 原文地址:https://www.cnblogs.com/songhe364826110/p/12189804.html
Copyright © 2011-2022 走看看