关于“将对象初始化”这事,C++似乎反复无常。如果你这么写:
int x;
在某些语境下x保证被初始化(为0),但在其他语境中却不保证。如果你这么写:
class Point {
int x, y;
};
// ...
Point p;
p的成员变量有时候被初始化(为0),有时候不会。如果你来自其他语言阵营而那儿并不存在“无初值对象”,那么请小心,因为这颇为重要。
永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你必须手工完成此事。例如:
int x = 0;
const char* text = "A C-style string";
double d;
std::cin >> d;
至于内置类型以外的任何其他东西,初始化责任落在构造函数( constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,重要的是别混淆了赋值( assignment)和初始化( initialization)。考虑一个用来表现通讯簿的class,其构造函数如下:
class PhoneNumber { };
class ABEntry {
public:
// 构造函数
ABEntry(const std::string& name, const std::string& address,
const std::list& phones);
private:
std::string theName;
std::string theAddress;
std::list thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& theName, const std::string& theAddress,
const std::list& phones)
{
// 这些都是赋值,而不是初始化
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在 ABEntry构造函数内,theName,theAddress和 thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作:
ABEntry::ABEntry(const std::string& theName, const std::string& theAddress,
const std::list& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{ }
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本(本例第一版本)首先调用default构造函数为theName, theAddress和thePhones设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列( member initialization list)的做法(本例第二版本)避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的theName 以name为初值进行copy构造,theAddress以 address为初值进行copy构造,thePhones 以 phones为初值进行copy构造。
对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。对于内置型对象如numTimesConsulted,只9o过万出你相再default构造一个成员变量,也通过成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可。假设ABEntry有一个无参数构造函数,我们可将它实现如下:
ABEntry::ABEntry()
:theName(), // 调用theName的默认构造函数
theAddress(), // 类似
thePhones(), // 类似
numTimesConsulted(0) //显示初始化为0
{ }
许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种classes存在许多成员变量和/或base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言))。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用。这种做法在“成员变量的初值系由文件或数据库读入”时特别有用。然而,比起经由赋值操作完成的“伪初始化”( pseudo-initialization),通过成员初值列(memberinitialization list)完成的“真正初始化”通常更加可取。
C有着十分固定的“成员初始化次序”。是的,次序总是相同: base classes更早于其 derived classes被初始化(见条款12),而class 的成员变量总是以其声明次序被初始化。回头看看ABEntry,其 theName成员永远最先被初始化,然后是theAddress,再来是thePhones,最后是numTimesConsulted。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。为避免你或你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序。
所谓static对象,其寿命从被构造出来直到程序结束为止,因此 stack 和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个non-local-static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C+对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
实例可以帮助理解。假设你有一个File system class,它让互联网上的文件看起来好像位于本机(local)。由于这个class 使世界看起来像个单一文件系统,你可能会产出一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem {
public:
// ...
std::size_t numDisks() const; // 众多成员函数之一
// ...
};
extern FileSystem tfs; // 预备给客户使用的对象,tfs代表the file system
FileSystem对象绝不是一个稀松平常无关痛痒的( trivial)对象,因此你的客户如果在theFilesystem对象构造完成前就使用它,会得到惨重的灾情。
现在假设某些客户建立了一个class用以处理文件系统内的目录(directories)。很自然他们的class会用上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对象。如何能够确定tfs会在tempDir之前先被初始化?
幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。DesignPatterns迷哥迷姊们想必认出来了,这是Singleton模式的一个常见实现手法。
这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没这等便宜!
既然这样,为避免在对象初始化之前过早地使用它们,你需要做三件事。第一,手工初始化内置型non-member对象。第二,使用成员初值列(member initializationlists)对付对象的所有成分。最后,在“初始化次序不确定性”(这对不同编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。
请记住