Item4:确保对象在使用之前已经初始化

文章强调了C++中对象初始化的重要性,特别是对于内置类型和复杂类型的成员变量。推荐使用成员初始化列表而不是构造函数体内的赋值操作来初始化成员。此外,文章讨论了非局部静态对象在不同编译单元间的初始化顺序问题,提出了通过函数内部的局部静态对象来解决此问题,确保对象按需初始化且避免未定义的行为。
摘要由CSDN通过智能技术生成

0.概述

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作( assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  • 为免除“跨编译单元的初始化次序”问题,以local static对象替换non-local static对象。

1.永远在使用对象之前将其初始化

1.1 内置类型:手动初始化

int x = 0; //对int进行手工初始化
const char* text = "A C-style string"; //对指针进行手工初始化
double d;
std::cin >> d; //以读取input stream的方式完成初始化.

1.2 其他类型:构造函数

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

别混淆赋值和初始化:考虑一个表现通信录的class,构造函数如下:

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::string theAddress;
    std::list<PhoneNumber> thePhones;
    int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
    theName = name; // 这些都是赋值
    theAddress = address; // 而非初始化
    thePhones = phones;
    numTimesConsulted = 0;
}

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。

  • 在 ABEntry构造函数内,theName、theAddress和 thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。
  • 但numTimesConsulted属于内置类型,不保证一定在所看到的那个赋值动作的时间点之前获得初值。

ABEntry的较佳写法是使用成员初始化列表替换赋值动作:

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
 theAddress(address),
 thePhones(phones),
 numTimesConsulted(0)
{}

成员初始化列表通常效率更高:

  • 基于赋值的版本首先调用默认构造函数为theName、theAddress和 thePhones设初值,然后立刻再对他们赋新值,默认构造函数的一切作为都浪费了;
  • 为成员初始化列表中的各个成员变量所设的实参,被拿去作为各个成员变量构造函数的实参:本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。

对于大多数类型,只调用一次copy构造比先调用默认构造函数再调用拷贝赋值操作符要更高效;对于内置类型如numTimesConsulted,初始化和赋值的成本相同,但为了一致性也使用成员初始化列表进行初始化;如果想要默认构造一个成员变量,可以使用成员初始化列,只要指定nothing作为初始化实参即可。

无参数构造函数的实现如下:

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(),
 theAddress(),
 thePhones(),
 numTimesConsulted(0)
{}

class含有多个构造函数等原因导致成员初始化列表的重复:可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。

成员初始化次序

  • 基类总是更早于其派生类被初始化
  • class的成员变量总是以其声明顺序被初始化(即使在成员初始化列表中以不同的顺序出现,也不会有任何影响)

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

static对象:寿命从被构造出来直到程序结束为止。这种对象包括global对象、定义于namespace作用域内的对象、在classes 内、在函数内、以及在file作用域内被声明为static的对象。

函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

编译单元:是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。

现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。

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

例FileSystem类,可能会产出一个特殊对象,位于global或namespace作用域,象征单一文件系统

class FileSystem { // from your library’s header file
public:
    ...
std::size_t numDisks() const; // one of many member functions
...
};
extern FileSystem tfs; // 预备给客户使用的对象

假设客户建立了一个class会用上FileSystem对象:

class Directory { // created by library client
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对象,无法确定tfs会在tempDir之前先被初始化。

解决方案:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static).这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这是 Singleton模式的一个常见实现手法。

这么做的原因在于:

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

上面的例子可以被写成:

class FileSystem{...};
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}
class Directory {...};
Directory::Directory(params)
{
    ...
    std::size_t disks=tfs().numDisks(); //原本的reference to tfs改为tfs()
    ...
}
Directory& tempDir()
{
    static Directory td;
    return td;
}

这样以后,用户将使用tfs()和tempDir()而不再是tfs和tempDir。也就是说他们使用函数返回的“指向static对象”的references,而不再使用static对象自身。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值