条款04:确定对象被使用前已经被初始化
Make sure that objects are initialized before they’re used.
该条款内容较多,分成两章来进行学习记录。
将对象初始化
关于对象初始化这一事件,C++在不同情境下有不同结果,比如:
int x;
在有些语境下,x会被初始化为0,但有些语境却不是。最直接的例子就是在不同的IDE下,这样的初始化不一致会导致相同的代码产生不同的结果。
再比如:
class Point {
int x, y;
};
...
Point p;
p的成员变量有时会被初始化为0,有时则有不会。这就造成了不一致!
读取未初始化的值是不安全的,因为会导致不明确的行为。有些时候,读取这样的未初始化的值,实际是读取一些“半随机”bits,从而对正在进行的读取动作造成了污染,最终导致了不可知的程序行为。
因此,最佳处理办法就是:永远在使用对象之前将它初始化。
对于无任何成员的内置类型,必须手工完成这样的初始化。
举个例子:
内置类型的初始化
//内置类型
int x = 0; //对int进行手工初始化
const char* text = "A C_style string."; //对指针进行手工初始化
double d;
std::cin >> d; //以读取input stream的方式完成初始化
非内置类型的初始化
而对于内置类型之外的其他类型,初始化由构造函数(constructor)来完成:确保每一个构造函数都讲对象的每一个成员初始化。
但是需要注意的是,赋值(Assignment)和初始化(Initialization)的区别要搞清楚。
举个例子:
//表示通讯簿的class
class PhoneNumber { ... };//表示电话号码的类
class ABEntry { //Address Book Entry,表示通讯簿的class
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;
};
//在类外定义构造函数
//版本1
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构造函数的一个较佳写法是:使用所谓的member initialization list(成员初始列)替代赋值动作:
//版本2
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(name), //这些操作都是初始化(initialization)
theAddress(address), //每一个都调用了copy构造函数
thePhones(phones),
numTimesConsulted(0)
{ } //因此,构造函数本体就没有其他的动作了
在版本2中的构造函数和版本1的最后结果是相同的,但是效率要更高。
- 在版本1(基于赋值),首先调用default构造函数为theName,theAddress和thePhones**设初值,然后立即在再对它们赋予新值**。这样看来,default构造函数的所有动作都浪费了。
- 在版本2(基于成员初值列(member initialization list)),则避免了这个问题。因为初值列中针对各个成员变量而设的实参,被拿去作为各个成员变量的构造函数的实参,依次调用copy构造函数。
在上面的例子中,对于内置类型numTimesConsulted,其初始化成本和赋值的成本基本相同,但是为了一致性,最好也通过成员元初值列来进行初始化。
同样的,当用default构造一个成员变量,可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可:
ABEntry::ABEntry( )
:theName(), //调用theName的default构造函数
theAddress(), //调用theAddress的default构造函数
thePhones(), //调用thePhones的default构造函数
numTimesConsulted(0) //将numTimesConsulted显示初始化为0
{ }
由此,我们有了以下这条规则:
- 规定总是在初值列中,列出所有的成员变量,以免忘记某些成员变量没有赋值。
例如,由于numTimesConsulted属于内置类型,如果成员初值列遗漏了它,就会造成它没有初值,从而引发可能的“不明确行为”。
总是使用成员初值列
在有的情况下,即使成员变量属于内置类型(初始化成本和赋值成本相同),也一定要使用初值列。
即:
- 当成员变量为const或者reference,它们就一定需要初值,不能被赋值。
因此,为了避免需要记住成员变量何时必须在成员初值列中初始化,何时又不需要,最简单的做法: - 总是使用成员初值列。
这样做有时候绝对必要,而且往往比赋值更高效。
赋值得到的“伪初始化”
但是凡事也都有例外。许多classes拥有多个构造函数,而每一个构造函数都有自己的成员初值列。如果这种classes存在许多成员变量和base classes,也也就会导致多份的成员初值列,从而造成大量的重复工作。
在这种情况下:
- 合理的在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,进而改用它们的赋值操作,并将这些赋值操作移动到某个函数之中(通常是private),以供所这些构造函数调用。
当然了,比起由赋值操作完成的“伪初始化(pseudo-initialization)”,通过成员初值列(member initialization list)完成的“真正初始化”通常更为可取。