Effective C++
Chapter 1. 让自己习惯C++ (Accustoming Yourself to C++)
Item 4. 确定对象被使用前已先被初始化
(Make sure that objects are initialized before they're used.)
通常如果你使用 C part of C++ 而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入 non-C part of C++, 规则有些变化。这就很好地解释了为什么 array (来自 C part of C++)不保证其内容被初始化,而 vector (来自 non-C part of C++)却有次保证。表面上这似乎是一个无法决定的状态,而最佳的处理办法就是:永远在使用对象之前先将它初始化。
1. 对于无任何成员的内置类型,必须手工完成此事。例如:
int x = 0; // 对 int 进行手工初始化
const char* text = "A C-style string"; //对指针进行手工初始化
double d;
std::cin >> d; //以读取 input stream 的方式完成初始化
2. 对于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化。这个规则很容易奉行,重要的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个用来表现通讯簿的 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; //这些都是赋值(assignment),
theAdress = address; //而不是初始化(initialization)。
thePhones = phones;
numTimesConsulted = 0;
}
这会导致 ABEntry 对象带有你期望(你指定)的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在 ABEntry 构造函数内,theName, theAddress 和 thePhones 都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的 default 构造函数被自动调用之时(比进入 ABEntry 的构造函数本体的时间更早)。但这对 numTimesConsulted 不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。
ABEntry 构造函数的一个较佳的写法是,使用所谓的 member initialization list (成员初值列)替换赋值动作:
ABEntry::ABEntry (const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
: theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) //现在,这些都是初始化(initialization)
{
//现在,构造函数本体不必有任何动作
}
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本首先调用 default 构造函数为 theName, theAddress 和 thePhones 设初值,然后立刻再对它们赋予新值,default 构造函数的一切作为因此浪费了。成员初值列(member initialization list)的做法避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。本例中的 theName 以 name 为初值进行 copy 构造,theAddress 以 address 为初值进行 copy 构造,thePhones 以 phones 为初值进行 copy 构造。
对于大多数类型而言,比起先调用 default 构造函数然后再调用 copy assignment 操作符,单只调用一次 copy 构造函数是比较高效的,有时甚至高效的多。对于内置型对象如 numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样道理,甚至当你想要 default 构造一个成员变量,都可以使用成员初值列,只要指定无物作为初始化实参即可。假设 ABEntry 有一个无参数构造函数,可将它实现如下:
ABEntry::ABEntry( ) : theName(), theAddress(), thePhones(), numTimesConsulted(0)
{
//调用 theName, theAddress, thePhones 的default 构造函数
//记得将 numTimesConsulted 显示初始化为 0
}
记住总是在初值列中列出所有的成员变量,以免还得记住哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。
有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初值列。如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值(见 Item 5)。为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。
许多 classes 拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种 classes 存在许多成员变量和/或 base classes,多份成员初值列的存在就会导致不受欢迎的重复(在初值列内)和无聊的工作(对程序员而言)。这种情况下可以合理地在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常为 private),供所有构造函数调用。这种做法在“成员变量的初值由文件或数据库读入”时特别有用。然而,比起经常由赋值操作完成的“伪初始化”(pseudo-initialization),通过成员初值列完成的“真正初始化”通常更加可取。
C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes 更早于其 derived classes 被初始化(见 Item 12),而 class 的成员变量总是以其声明次序被初始化。回头看看 ABEntry,其 theName 成员永远最先被初始化,然后是 theAddress, 再来是 thePhones,最后是 numTimesConsulted。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。因而在成员初值列中列各个成员时,最好以其声明次序为次序。<通俗来讲,两个成员变量的初始化带有次序性,例如初始化 array 时需要指定大小,因此代表大小的那个成员变量必须先有初值。>
一旦已经很小心地将“内置型成员变量”明确地加以初始化,而且也确保构造函数运用“成员初值列”初始化 base classes 和成员变量,那就只剩唯一一件事情需要操心,就是“不同编译单元内定义的 non-local static 对象”的初值化次序。
具体略。
请记住:
- 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
- 为免除“跨编译单元的初始化次序”问题,请以 local static 对象替换 non-local static 对象。