Item 4: 确保对象在使用前被初始化

对象初始化方法

对于内建类型的非成员对象,初始化手动执行:

int x = 0;  //@ manual initialization of an int
const char * text = "A C-style string";   //@ manual initialization of a pointer
double d;
std::cin >> d;  //@ "initialization" by reading from an input stream

除此之外的几乎全部情况,初始化的重任就落到了构造函数的身上。这里的规则很简单:确保所有的构造函数都初始化了对象中的每一样东西。

这个规则很容易遵守,但重要的是不要把赋值和初始化搞混。考虑下面这个表现一个通讯录条目的类的构造函数:

class PhoneNumber { ... };
//@ ABEntry = "Address Book Entry"
class ABEntry {   
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 num TimesConsulted;

};

ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)

{
  //@ these are all assignments,not initializations
  theName = name;                       
  theAddress = address;                
  thePhones = phones;
  numTimesConsulted = 0;
}

C++ 的规则规定一个对象的数据成员在进入构造函数的函数体之前被初始化:

  • 在 ABEntry 的构造函数内,theName,theAddress 和 thePhones 不是被初始化,而是被赋值。
  • 初始化发生得更早——在进入 ABEntry 的构造函数的函数体之前,它们的缺省的构造函数已经被自动调用。但不包括 numTimesConsulted,因为它是一个 内建类型。不能保证它在被赋值之前被初始化。

一个更好的写 ABEntry 构造函数的方法是用成员初始化列表来代替赋值:

ABEntry::ABEntry(const std::string& name, const std::string& address,
                 const std::list<PhoneNumber>& phones)
: theName(name),
  theAddress(address),                  //@ these are now all initializations
  thePhones(phones),
  numTimesConsulted(0)
{}   //@ the ctor body is now empty

基于赋值的版本会首先调用缺省构造函数初始化 theName,theAddress 和 thePhones,然而很快又在缺省构造的值之上赋予新值。那些缺省构造函数所做的工作被浪费了。

成员初始化列表的方法避免了这个问题,因为初始化列表中的参数就可以作为各种数据成员的构造函数所使用的参数。在这种情况下,theName 从 name 中 copy-constructed(拷贝构造),theAddress 从 address 中 copy-constructed(拷贝构造),thePhones 从 phones 中 copy-constructed(拷贝构造)。对于大多数类型来说,只调用一次拷贝构造函数的效率比先调用一次缺省构造函数再调用一次拷贝赋值运算符的效率要高(有时会高很多)。

对于 numTimesConsulted 这样的内建类型的对象,初始化和赋值没有什么不同,但为了统一性,最好是经由成员初始化来初始化每一件东西。类似地,当你只想缺省构造一个数据成员时也可以使用成员初始化列表,只是不必指定初始化参数而已。例如,如果 ABEntry 有一个不取得参数的构造函数,它可以像这样实现:

ABEntry::ABEntry()
:theName(),                         //@ call theName's default ctor;
 theAddress(),                      //@ do the same for theAddress;
 thePhones(),                       //@ and for thePhones;
 numTimesConsulted(0)               //@ but explicitly initialize
{}      

在初始化列表中总是列出每一个数据成员,这就可以避免一旦发生疏漏就必须回忆起可能是哪一个数据成员没有被初始化。因为 numTimesConsulted 是一个内建类型,如果将它从成员初始化列表中删除,就为未定义行为打开了方便之门。

有时候即使是内建类型,初始化列表也必须使用。比如,const 或 references data members 是必须被初始化的,它们不能被赋值。

C++ 对象的数据被初始化的顺序总是相同的:

  • 基类在派生类之前被初始化。
  • 在一个类内部,数据成员按照它们被声明的顺序被初始化。

例如,在 ABEntry 中,theName 总是首先被初始化,theAddress 是第二个,thePhones 第三,numTimesConsulted 最后。即使它们在成员初始化列表中以一种不同的顺序排列,这依然是成立的。为了避免读者混淆,以及一些模糊不清的行为引起错误的可能性,初始化列表中的成员的排列顺序应该总是与它们在类中被声明的顺序保持一致。

定义在不同转换单元内的非局部静态对象的初始化的相对顺序是未定义的

static 对象

一个静态对象的生存期是从它创建开始直到程序结束。程序结束时静态对象会自动销毁,也就是当 main 停止执行时会自动调用它们的析构函数。

静态对象按照定义的位置可以分为:

  • 在函数内部的静态对象称为 局部静态对象
  • 全局对象、定义在命名空间范围内的对象、在类内部声明为静态的对象、在文件范围内被声明为静态的对象称为 非局部静态对象

转换单元

一个转换单元是可以形成一个单独的目标文件的源代码。基本上是一个单独的源文件,再加上它全部的 #include 文件。

跨转换单元的初始化问题

class FileSystem { 
public:
  ...
  std::size_t numDisks() const;  
  ...
};
extern FileSystem tfs;  //@ object for clients to use;"tfs" = "the file system"

现在假设一些客户为一个文件系统中的目录创建了一个类,他们的类使用了对象:

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

Directory::Directory(params)
{
  ...
  std::size_t disks = tfs.numDisks();   //@ use the tfs object
  ...
}

更进一步,假设这个客户决定为临时文件创建一个单独的对象:

Directory tempDir(params); //@ directory for temporary files

现在初始化顺序的重要性变得明显了:除非 tfs 在 tempDir 之前初始化,否则,tempDir 的构造函数就会在 tfs 被初始化之前试图使用它。但是,tfs 和 tempDir 是被不同的人于不同的时间在不同的源文件中创建的——它们是定义在不同转换单元中的非局部静态对象。因此,无法确定它们的初始化顺序。

正确的做法是将每一个非局部静态对象移到它自己的函数中,在那里它被声明为静态。这些函数返回它所包含的 对象的引用。换一种说法,就是用局部静态对象取代非局部静态对象。

class FileSystem { ... };          

FileSystem& tfs()                   
{                                  
  static FileSystem fs;          
  return fs;                      
}

class Directory { ... };           

Directory::Directory( params )     
{                                
  ...
  std::size_t disks = tfs().numDisks();
  ...
}

Directory& tempDir()             
{                                  
  static Directory td;              
  return td;                      
}

任何种类的非常量静态对象——局部的或非局部的,在多线程存在的场合都会发生麻烦。解决这个麻烦的方法之一是在程序的单线程的启动部分手动调用所有的返回引用的函数。以此来避免与初始化相关的混乱环境。

总结

  • 对象初始化
    • 手动初始化内建类型的对象,因为 C++ 只在某些时候才会自己初始化它们。
    • C++ 的规则规定一个对象的数据成员在进入构造函数的函数体之前被初始化。
    • 列表初始化通常比在构造函数中赋值效率更高。
    • 在构造函数中,用成员初始化列表代替函数体中的赋值初始化列表中数据成员的排列顺序要与它们在类中被声明的顺序相同。
    • 列表初始化时要初始化每一个成员,防止遗漏。
    • 类中的 const 成员和引用成员必须使用初始化列表初始化。
  • 静态对象
    • 定义在不同转换单元内的非局部静态对象的初始化的相对顺序是未定义的。
    • 通过用局部静态对象代替非局部静态对象来避免跨转换单元的初始化顺序问题。
posted @ 2019-12-31 09:42  刘-皇叔  阅读(226)  评论(0编辑  收藏  举报