Effective C++ 学习笔记——条款04:确定对象被使用前已先被初始化
C++语言是由多种“次语言”组成的(见条款01),因此有时定义的变量并不能如我们所想得进行初始化,即读取未初始化的值会导致不明确的行为。如:
C part of C++ 中的整型数组 int[],未初始化时其中可能包含非零初始化元素。
STL 中整型容器 std::vector,可以保证所有元素均被零初始化。
内置类型(built-in type)的初始化——C part of C++
C++的一部分基础数据类型继承于 C,因此不能保证该类型变量在定义时初始化为一定的数值。使用未初始化变量可能导致程序不正常工作。
未初始化变量如:
int a; //未初始化int
double b; //未初始化double
char* text; //未初始化字符指针
需改为:
int a = 0; //对 int 进行手动初始化
const char* text = "A C-style string"; //对指针进行手动初始化
double b;
std::cin >> b; //以读取 intput stream 的方式完成初始化
类的初始化(构造函数)
自定义类型参数的初始化依靠构造函数(constructors)完成,其原则为:确保每一个构造函数都将对象的每一个成员初始化
其难点主要在于区分 赋值(assignment) 和 初始化(initialization)。
例如,构造一个通讯录class,其构造函数如下:
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 numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{
theName = name; // 这些操作均是赋值,并不是初始化
theAddress = address; / /初始化发生在成员的默认构造函数被自动调用时
thePhones = phones; // 比进入构造函数本体时间早
numTimesConsulted = 0; // 该参数属于内置类型,与上述不同
}
该方法本质是在默认构造函数完成后,再次对参数进行赋值,则默认构造函数的行为浪费了
正确构造函数书写方法,即使用成员初值列表(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)
{ } // 构造函数本体不需要做任何动作
该方法,初值列表中针对各个成员变量而设的实参,被拿去作为成员变量构造函数的实参
本例中 theName 以 name 为初值进行 copy构造,后两者同理,单调用一次拷贝构造函数效率更高
其中,numTimesConsulted 初始化和赋值成本相同,但推荐一直通过成员初值列表完成初始化
无参构造函数也可通过,指定无物作为初始化实参,如:
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
:theName(), // 调用 theName、theAddress、thePhones 的默认构造函数
theAddress(),
thePhones(),
numTimesConsulted(0) // 内置 int 需要显示初始化为 0
{ }
如果成员变量是 const 或 references,就一定需要初值,不能被赋值。因此为避免错误发生,必须对所有成员使用成员初值列表完成初始化。
例如:
const int a; //报错,需要初始化
int& b; //报错,需要初始化
//现在对其进行初始化:
const int a = 3; //编译通过
int c = 3;
int& b = c; //编译通过
在继承关系中,基类(base class)总是先被初始化。
在同一类中,成员数据的初始化顺序与其声明顺序是一致的,而不是初始化列表的顺序。
因此,为了代码一致性,要保证初始化列表的顺序与成员数据声明的顺序是一样的。
初始化非本地静态对象
在两个编译单元中,分别包含至少一个非本地静态对象,当这些对象发生互动时,它们的初始化顺序是不确定的,所以直接使用这些变量,就会给程序的运行带来风险。
编译单元(translation unit): 可以让编译器生成代码的基本单元,一般一个源代码文件就是一个编译单元。
非本地静态对象(non-local static object): 静态对象可以是在全局范围定义的变量,在名空间范围定义的变量,函数范围内定义为 static 的变量,类的范围内定义为 static 的变量,而除了函数中的静态对象是本地的,其他都是非本地的。
注意:静态对象存在于程序的开始到结束,所以它不是基于堆(heap)或者栈(stack)的。初始化的静态对象存在于 .data 中,未初始化的则存在于 .bss 中。
例如,现有以下服务器代码:
class Server{...};
extern Server server; //在全局范围声明外部对象server,供外部使用
客户端:
class Client{...};
Client::Client(...){
number = server.number;
}
Client client; //在全局范围定义client对象,自动调用了Client类的构造函数
主要问题:定义对象 client 自动调用了 Client 类的构造函数,此时需要读取对象 server 的数据,但全局变量的不可控性让我们不能保证对象 server 在此时被读取时是初始化的。
试想如果还有对象 client1, client2 等等不同的用户读写,我们不能保证当前 server 的数据是我们想要的。
解决方法: 将全局变量变为本地静态变量。使用一个函数,只用来定义一个本地静态变量并返回它的引用。因为C++规定在本地范围(函数范围)内定义某静态对象时,当此函数被调用,该静态变量一定会被初始化。
解决方法如下:
class Server{...};
Server& server(){ //将直接的声明改为一个函数
static Server server;
return server;
}
class Client{...};
Client::client(){ //客户端构造函数通过函数访问服务器数据
number = server().number;
}
Client& client(){ //同样将客户端的声明改为一个函数
static Client client;
return client;
}
总结
- 内置型对象进行手动初始化,C++代码不保证初始化参数;
- 构造函数使用成员初值列表,不要再构造函数内使用赋值操作。初值列表列出的成员变量,其排列次序应该和 class 中的声明次序相同;
- 为免除“跨编译单元的初始化次序”问题,请以 local static 对象替换 non-local static 对象。