条款04(二):确定对象被使用前已经被初始化

127 篇文章 7 订阅
39 篇文章 3 订阅

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

Make sure that objects are initialized before they’re used.

成员初始化次序

在C++中,有十分固定的“成员初始化次序”:

  • base classes 早于其derived classes被初始化;
  • class的成员变量按照其声明顺序被初始化。
    即使成员变量在成员初值列以不同的次序出现(不合法的做法),也不会有影响。
    为了避免疑惑,成员初值列中的各个成员的次序,应该按照声明的次序来进行排列。

non-local static对象

除了以上的初始化,还需要考虑“不同编译单元内定义的non-local static对象”的初始化次序。

  • static
    所谓的static对象,其寿命从被构造出来直到程序结束为止,因此,stackheap-based对象都被排除了。这种对象包括:global对象定义在namespace作用域内的对象在classes内、在函数内、以及在file作用域内被声明的static对象。
    在函数内的static对象称为local static对象(因为它们对函数而言是local),而其他的static对象称为non-local static对象
    在程序结束时,static对象会被自动销毁,也就是说,它们的析构函数会在main()结束时自动调用。

  • 编译单元
    所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码
    基本上,就是指单一源码文件加上其所包含的头文件(#include files)

因此,这里所涉及到的问题至少为两个源码文件每个文件内至少包含一个non-local static对象(即,该对象是global或者位于namespace作用域内,或在class内或file作用域内被声明为static)。
真正的问题为:

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

举个例子:
假如现在有一个FileSystem class,它的作用是让互联网上的文件看起来好像是位于本机的。由于这样的特性,使得整个互联网文件系统看起来像是一个单一的文件系统,此时,可能会出现一个特殊对象,位于global或namespace作用域内,象征着单一文件系统:

class FileSystem {
public:
...
    std::size_t numDisks() const;//众多成员函数之一
    ...
};

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

在上面定义的对象中,如果客户在theFileSystem对象构造完成之前就去使用,将会出现严重的错误!

接下来,假设客户建立了一个class用以处理文件系统内的目录(directories),则必然会用到theFileSystem对象:

class Directory {
public:
    Directory( params ):
    ...
};

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

更进一步,用于决定创建一个Directory对象,用来放置临时文件:

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

于是,初始化次序的重要性就体现出来了

  • 除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。
  • 但是tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义在不同编译单元内的non-local static对象。

然而,C++对“定义在不同的编译单元内的non-local static对象”的初始化相对次序并没有明确的定义,因此,无法确定tfs会在tempDir之前被初始化。

解决思路:

  • 将每个non-local static对象放到自己的专属函数内(该对象在此函数内被声明为static)。
  • 这些函数返回一个reference指向它所含的对象。
  • 之后,用户调用这些函数,而不是直接涉及这些对象
  • 即,non-local static对象被local static对象替换了。

这个解决思路的基础在于:
C++保证,函数内的local static对象会在“该函数被调用期间、首次遇上该对象的定义式时”被初始化。
当我们以函数调用(返回一个reference指向local static对象)替换“直接访问non-local static对象”时,就获得了C++的这个保证——即获得的那个reference将指向一个已经经历初试化的对象。
另外,如果从未调用该non-local static对象的“仿真函数”,就不会引发构造和析构的成本
用这种方法改良上面的Directory class:

class FileSystem { ... };   //同上

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

class Directory { ... };            //同上
Directory :: Directory( params )    //同上,但原本的reference to tfs改为tfs()
{
    ...
    std::size_t disks = tfs().numDisks();
    ...
}

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

在经过了这样的修改之后,此时所使用的是函数返回的“指向static对象”的references,而不再使用static对象自身。

利用这种reference-returning函数防止“初始化次序问题”,前提是有这一个对对象而言合理的初始化次序
为了避免在对象初始化之前而过早的使用它们:

  • 第一,手工初始化内置型non-member对象。
  • 第二,使用成员初值列(member initialization lists)对付对象的所有成分。
  • 第三,在“初始化次序不确定性”环境下,加强程序逻辑的设计。

最后:

1,为内置类型对象进行手工初始化,因为C++并不保证初始化它们。

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

3,为了避免“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在创建 std::list 对象时,可以通过以下几种方式进行初始化: 1. 默认初始化:默认情况下,std::list 对象会被默认初始化。这意味着该对象将被创建,但是不会包含任何元素。可以通过 push_back() 或 push_front() 等函数向 std::list 中添加元素。 ```c++ std::list<int> my_list; // 默认初始化,不包含任何元素 my_list.push_back(1); // 向 my_list 中添加元素 ``` 2. 拷贝初始化:可以通过将一个 std::list 对象拷贝到另一个 std::list 对象来进行初始化。拷贝操作会将源 std::list 对象中的所有元素复制到目标 std::list 对象中。 ```c++ std::list<int> my_list1 = {1, 2, 3}; // 使用 initializer_list 进行初始化 std::list<int> my_list2(my_list1); // 拷贝初始化,将 my_list1 中的元素复制到 my_list2 中 ``` 3. 范围初始化:可以通过将一个范围内的元素复制到 std::list 对象中来进行初始化。 ```c++ std::vector<int> vec = {1, 2, 3}; std::list<int> my_list(vec.begin(), vec.end()); // 将 vec 中的元素复制到 my_list 中 ``` 4. 移动初始化:可以通过将一个 std::list 对象移动到另一个 std::list 对象来进行初始化。移动操作会将源 std::list 对象中的所有元素移动到目标 std::list 对象中,同时源 std::list 对象会被清空。 ```c++ std::list<int> my_list1 = {1, 2, 3}; std::list<int> my_list2(std::move(my_list1)); // 移动初始化,将 my_list1 中的元素移动到 my_list2 中 ``` 无论采用哪种方式进行初始化,在创建 std::list 对象时,需要确保所有的元素都被正确初始化,以避免在销毁 std::list 对象时访问未初始化的内存。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值