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

最佳处理方法:永远在使用对象之前先将它初始化.

int x = 0;      
const char* text = "A C-style string";

double d;
std::cin >> d;

 

对于构造函数:确保每一个构造函数都将对象的每一个成员初始化。

//区分赋值和初始化

 

例如以下构造函数:

AB::AB(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
    //以下几个左值都是类的成员
    theName = name;
    theAddress = address;
    thePhones = phones;
    numTimesConsulted = 0;
    //敲黑板!!以上都是赋值,不是初始化!!
}

这会导致AB对象带有你期望的值,不是最佳做法!

 

构造函数的最佳写法:使用所谓的member initialization list(成员初值列) 替换赋值动作

AB::AB(const std::string& name, const std::string& address; const std::list<PhoneNumber>& phones)
        :theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}
        //通过copy构造初始化

现在,这些都是初始。这个构造函数和上一个的最终结果相同,但通常效率较高。

 

默认构造函数也可以使用成员初值列。

AB::AB() : theName(), theAddress(), thePhones(), numTimesConsulted(0) {}    //没有指定初值则自动调用default构造函数

重要规则:规定总是在初值列中列出所有成员变量。以免还得记住哪些成员变量可以无需初值。

如果成员初值列遗漏某个成员,它就没有初值,因此可能开启”不明确行为“的潘多拉盒子~

 

若成员变量是const或reference,就一定需要初值,不能被赋值!

所以最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效。

 

C++有着固定的”成员初始化次序“:

base classes基类总是比derived classes继承类更早被初始化。

class的成员变量总是以其声明次序被初始化。

例如:

class AB {
    public:
        AB::AB(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
        :theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) {}
    private:
        std::list<PhoneNumber> thePhones;
        std::string theAddress;
        int numTimesConsulted;
        std::string theName;
}

即使在构造函数成员初值列中出现的次序和private的声明次序不同,初始化次序依旧和声明次序相同(即thePhones最先初始化)

为避免你或读者被迷惑,并避免某些可能存在的晦涩错误,当在成员初值列中条列各个成员时,最好总是以其声明次序为次序!

 

 

”不同编译单元内定义之non-local static对象”的初始化次序:

static对象,其寿命从被构造出来直到程序结束为止。

这种对象包括:global对象,定义于namespace作用域内的对象,在class内,在函数内,以及在file作用域内被声明为static的对象。

函数内的static对象称为local static对象(对函数而言是local),其它对象称为non-local static对象。

static对象在程序结束时自动销毁,它们的析构函数会在main()结束时被自动调用。

 

//补充:所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。

 

真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,

它所用到的这个对象可能尚未被初始化,因为C++对定义在不同编译单元内的non-local static对象的初始化次序并无明确定义。

 

实例:

class FileSystem { //来自你的程序库
 public:
    ...
    std::size_t numDisks() const;   //众多成员函数之一
    ...
};
extern FileSystem tfs;          //预备给客服使用的对象
                                //tfs代表"the file system"​

class Directory {
 public:
    Directory( params );
    ...
};
Directory::Directory( params )
{
    ...
    std::size_t disks = tfs.numDisks(); //使用tfs对象
}​

然后执行:

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

现在,初始化次序的重要性显示出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。

 

如何确定tfs在tempDir之前先被初始化?

唯一需要做的是:

将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。

这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。

即:non-local static 被 local static对象替换了。


这种方法基于:C++保证,函数内的local static对象会在 “函数被调用期间" 或 "首次遇上该对象之定义式” 时被初始化!

 

所以以"函数调用“(返回一个reference指向local static对象) 替换”直接访问non-local static对象".

这样获得的reference将指向一个历经初始化的对象。

 

  这个手法的基础在于:C++保证,函数内的local static 对象会在"该函数被调用期间""首次遇上该对象的定义式"时被初始化.所以如果以"函数调用"(返回一个reference指向local static 对象)替换"直接访问non-local static 对象",就可保证所获得的那个reference将指向一个已经初始化的对象.更棒的是,如果从未调用non-local static 对象的"仿真函数",就绝不会引发构造和析构成本:真正的non-local static 对象没有这个优点.

 

所以,经过此技术施行:

class FileSystem { //来自你的程序库
 public:
    ...
    std::size_t numDisks() const;   //众多成员函数之一
    ...
};
FileSystem& tfs()                   //这个函数用来替换tfs对象:它在FileSystem class中可能是个static.
{
    static FileSystem fs;           //定义并初始化一个local static对象
    return fs;                      //返回一个reference指向上诉对象。
}

class Directory {
 public:
    Directory( params );
    ...
};
Directory::Directory( params )
{
    ...
    std::size_t disks = tfs().numDisks(); //使用tfs()对象
}​

Directory& tempDir()                //这个函数用来替换tempDir对象,在Directory class中可能是个static
{
    static Directory td;            //定义并初始化local static对象
    return td;                      //返回一个reference指向上述对象
}

现在这个程序调用就没有问题了,唯一不同的是现在使用tfs()和tempDir()而不再是tfs和tempDir.

也就是说使用函数返回的"指向static对象"的reference,而不再使用static对象本身。

 

注意:任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦!

处理麻烦的一种做法:在程序的单线程启动阶段手工调用所有reference-returning函数。

 

/*    补充,内置类型:包括算术类型和空类型在内的基本数据类型。

       算术类型包括:字符型,整型,bool型,浮点型                   */

 

为避免在对象初始化之前过早地使用它们,需要做三件事:

第一,手工初始化内置型non-member对象。

第二,使用成员初值列对付对象的所有成分。

第三,在“初始化次序不确定性”氛围下加强设计!

 

 

总结:

-对内置型对象进行手工初始化,因为C++不保证初始化它们。(避免出现不确定情况)

-构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的  声明次序相同。

-为免除“跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。


猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80611926