Item 4: Make sure that objects are initialized before they’re used
这一节谈论的就是C++的初始化问题。这也是学习C++的时候容易犯错的地方。
比如下面这段代码,类Point中的x和y就是未初始化的,所以打印得到的值是没有意义的。
class Point {
public:
inline int getx() { return x; }
inline int gety() { return y; }
private:
int x, y;
};
int main() {
Point p;
cout << p.getx() << " " << p.gety() << endl;
return 0;
}
C++中初始化的种类很多,发生的时机也很多,想全部记住,确实是很头疼的事情,这一点作者也有所表示。记得之前在C++ Primer上看到的印象很深的一段,倒是给出了一类普遍的情况。用我的理解来说就是对于内置类型(int, char, double)如果是在局部作用域(函数内部,类中),则是未初始化的,需要手动初始化,但如果是全局作用域中(全局变量),会执行默认初始化,比如int默认初始化为0。而对于类类型,无论在哪个作用域,都会调用它的构造函数来进行初始化,比如string, vector这些,因为构造函数担负着初始化的任务
接着,书上强调了初始化和赋值的区别,给出了这样一个例子
class PhoneNumber { ... };
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){
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
这个例子中,在类外部实现的构造函数中,将实参赋值给类的各个成员变量,这其实是赋值操作而不是初始化,这一点在C++ Primer上也有所强调,也就是说在进入这个构造函数的内容之前,theName, theAddress, thePhones这三个成员已经发生了初始化的过程(类的构造函数),比如此时的theName和theAddress都是空串,但是对于numTimesConsulted是内置类型,没有这个过程。然后再构造函数体中,又对这些成员进行赋值。
理解了这个过程,就会发现前三个成员的构造函数做了无用功,因为我们的意愿是将它们的值变成对应的实参,所以刚初始化之后马上就会被实参的新值覆盖,这样的无用功会带来效率上的降低,所以推荐的做法是列表初始化。
作者在写书的时候C++11还没有出来,但是书上提到的很多点都成为了后来的C++11的标准,比如这里的列表初始化。改了之后的代码就是这样
ABEntry::ABEntry(const std::string& name, const std::string& address,const std::list<PhoneNumber>& phones)
: theName (name),
theAddress (address),
thePhones (phones),
numTimesConsulted (0)
{ }
此时,对于类的各个成员就换成了用实参作为参数的构造函数进行初始化,那么就不需要再在函数中进行赋值,从而提高效率。
另外,对于内置类型来说,初始化和赋值的成本一样,但是为了一致也这么写,而且还有一种情况可能会被忽略就是如果成员是const或者引用类型,因为const或是引用是只能被初始化而不能被赋值,所以只能写在列表初始化;
再就是如果只想对这些成员执行默认初始化,也就是没有实参的情况,那也很简单,只需要不写参数就可以了,就像这样
ABEntry::ABEntry(const std::string& name, const std::string& address,const std::list<PhoneNumber>& phones)
: theName (),
theAddress (),
thePhones (),
numTimesConsulted (0)
{ }
不过要记得对内置类型单独处理。
上面说了很多种情况,总结起来就是总是用初始化列表,肯定是安全高效的。
另外,书上还提到了如果类中拥有多个构造函数,如果又有很多成员或者基类,那么它需要初始化的成员就很多,而且每个构造函数都要写这种列表初始化就会是一种很繁琐重复的工作。对于这种情况的话,就可以合理的略去一些赋值和初始化表现的一样好的成员,对这些成员进行赋值,为了方便可以写进一个函数来供调用。
接下来,书上提到了成员的初始化顺序问题。C++中的成员初始化顺序是固定的,首先基类早于派生类被初始化,这一点是很显然的,而且在后面也会讲到。再者,在一个类中,成员变量的初始化顺序是按照声明的顺序,比如上面的例子
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
theName就是第一个被初始化,然后是theAddress,最后是numTimesConsulted,即使在初始化列表中出现的次序不同,这一点在C++ Primer中也是强调过,所以推荐的做法就是按照声明的顺序来写初始化列表。
接着,书上提到了一句很隐晦的话
the order of initialization of non-local static objects defined in different translation units
什么叫non-local static objects呢?书上有这么一段
Static objects inside functions are known as local static objects (because they’re local to a function), and the other kinds of static objects are known as non-local static objects
意思就是在函数中定义的局部static对象叫做局部static对象,那么其他的static对象就是non-local
那什么是 translation units
A translation unit is the source code giving rise to a single object file. It’s basically a single source file, plus all of its #include files.
简单说能生成单一目标文件的源码加上一些头文件
那前面那句话的指什么呢,来看一个例子。
class FileSystem { // from your library
public:
...
std::size_t numDisks() const; // one of many member functions
...
};
extern FileSystem tfs;
这里定义了一个文件系统类FileSystem,然后定义了一个全局对象tfs,
class Directory { // created by library client
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks() ; // use the tfs object
...
}
Directory tempDir( params )
接着又有一个目录类,并且实例化了一个对象。而且这个目录类的构造函数中,用到了tfs这个实例。很显然,在使用tfs之前,必须完成对tfs的初始化。
所以对这两个对象的初始化顺序就非常重要,除非tfs在tempDir之前就初始化好,不然tempDir的构造函数就会尝试去使用未初始化好的tfs的内容。
但是问题就是如果这两个类是不同的人在不同的时间在不同的源文件中写的话,也就是所谓的non-local static objects defined in different translation units,但是这一点是保证不了的,书上说到非常困难,而且提到甚至不值得这么做。
那怎么保证tfs能在tempDir之前初始化好呢?书上说到只需要一点小的改变就能解决这个问题。解决办法就是把每个non-local static objects移动到它自己的函数中,并且把这个non-local static objects声明为static 并且这些函数返回的是对象的引用
换句话说就是non-local static objects被换成local static objects ,书上还说到这是设计模式中的单例模式。
那为什么这样就可以是因为C++保证了local static objects会在它所在的函数被调用的过程中,第一次碰到这个local static objects的定义的时候被初始化。所以当你把对non-local static objects的直接访问替换成对返回local static objects的引用的函数的调用,就能保证拿到的这个返回的引用是初始化过的。
我们来看一下改过的代码
文件系统:
class FileSystem { // from your library
public:
...
std::size_t numDisks() const; // one of many member functions
...
};
//还是上面的文件系统
//但是不再直接实例化一个对象 而是写进一个函数
FileSystem& tfs() { //函数返回文件系统类的引用
static FileSystem fs; //local static object
return fs; //返回这个引用
}
//此时返回的就是一个初始化好了的FileSystem对象的引用,而且因为是static 只会在第一次调用的时候初始化
目录类:
class Directory { // created by library client
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs().numDisks() ; // 这里不再是直接引用tfs对象,而是调用tfs函数,
//返回一个static的FileSystem类引用 再调用它的numDisks
...
}
//类似的 这里对目录类的实例化也改为函数中返回引用
Directory& tempDir() {
static Directory td;
return td;
}
经过这样的修改,当需要实例一个Dictory类时,调用tempDir,其中声明一个local static Directory实例td, 这个td则会调用Directory的构造函数,在这个构造函数中又会调用tfs(),类似的过程,tfs实例化后将对象返回。使用局部的static,从而避免使用全局的变量,从而避免了使用未初始化的全局变量的风险。
但是书上也说到对于多线程的问题,由于用到了static,就会产生不确定性。
最后书上做了一个总结,为了避免未初始化的问题,做到三件事: