在C++中,对象的定义
和初始化
是两码事。
普通对象的初始化
例如,如果写出这样的代码:
int x;
编译器将仅仅为变量x
在栈上分配一块内存空间,但是不会向其中写入初始值。
对象的
定义
指的是为其分配内存的过程,往往由全局的operator new
函数完成;对象的初始化
指的是向这块内存中写入恰当的值,让对象变为可用状态的过程,往往由对象的构造函数
完成。
这也是体现C++复杂的一个方面。例如,C++完全继承了C语言中数组的特性。因此,仅定义的一个数组是仅定义而未被初始化的;但是STL
中的各种容器都实现了自己的构造函数,因而它们在定义时都保证定义和初始化都已经完成。
读取未被初始化的值将引发不确定的行为:可能会直接让程序崩溃,也可能让系统的逻辑出现错误。所以最佳的处理方法是:在定义任何一个对象的同时都将其显示初始化
。对于内置类型,在定义时直接初始化即可:
int x = 0;
const char *text = "A C-style string";
对于类类型,需要提供其构造函数:
class PhoneNumber { /* ... */ };
class ABSEntry
{
std::string _name;
std::string _address;
std::list<PhoneNumber> _phones;
int _numTimesConsulted;
public:
ABSEntry(const std::string &name, const std::string &address, const std::lis<PhoneNumber> &phones, int numTimesConsulted)
: _name(name), _address(address), _phones(phones), _numTimesConsulted(numTimesConsulted) {}
};
注意,这里使用了初始化列表,而不是在构造函数的函数体内对成员变量进行初始化,因为这样做更加高效:
- 如果在构造函数的函数体内进行成员变量的初始化,那么将直接损失掉默认构造函数的初始化性能:先将这些容器对象初始化为空值,然后再立刻对它们进行赋值。
- 调用一次拷贝构造函数,要比先调用依次默认构造函数,再调用一次拷贝赋值运算符的性能要强上不少。
- 当成员变量当中存在
const
或者引用成员时,必须使用初始化列表进行初始化。
初始化列表的初始化顺序和初始化列表的书写顺序无关,而是和成员变量在类中声明的顺序相同。因此,要特别注意这些变量初始化的顺序,避免其中的初始化依赖性而导致的BUG。
另外,即便编译器会在构造函数中默认对每个用户自定义类型的对象进行默认构造(即调用空的构造函数),我们也还是应该显示地在构造函数中进行手动调用,这样可以提醒我们哪些对象一定被初始化完毕,避免可能发生的遗漏:
class PhoneNumber { /* ... */ };
class ABSEntry
{
std::string _name;
std::string _address;
std::list<PhoneNumber> _phones;
int _numTimesConsulted;
public:
ABSEntry()
: _name(),
_address(),
_phones(),
_numTimesConsulted(0) {}
};
全局对象的初始化
C++未对全局对象的初始化顺序做出约定。例如下面的代码:
class FileSystem { /* ... */ };
extern FileSystem fileSystem; //给客户暴露的接口。
现在,客户可能会写出这样的代码:
class Directory
{
std::size_t disks;
//...
public:
Directory() : disks(fileSystem.getDisks()) /* , ... */
{
// ...
}
};
extern Directory tmpDirectory; //客户创建的一个临时文件,用于保存运行时信息。
这段代码中,Directory
对象的构造依赖于我们提供的FileSystem
对象的构造;也就是说,在理论上如果我们暴露给用户的全局fileSystem
对象尚未被初始化,那么Directory
对象就不应该被构造出来。
很遗憾的是,C++不保证全局对象的初始化顺序。也就是说,根据编译器的实现不同,很有可能是客户的临时文件对象tmpDirectory
先被构造,而后才构造我们提供的fileSystem
对象。这就意味着,tmpDirectory
对象是非法的。
这个问题的解决方案是摒弃掉这种C风格的写法,而是将这两个全局对象都封装到各自的一个全局函数当中:
class FileSystem { /* ... */ };
FileSystem &GetFileSystem()
{
static FileSystem fileSystem;
return fileSystem;
}
class Directory { /* ... */ };
Directory &GetTmpDirectory()
{
static Directory dir(GetFileSystem()/* , ... */);
return dir;
}
之所以可以这样写,是因为C++提供了这样的保证:局部静态对象会在第一次遇到或者使用它的时候被初始化。
当然,对于某些糟糕的情况,这种方式也无法完成需求。例如,A
类对象的构造依赖于B
类对象的构造;然而B
类对象的构造又反过来依赖于A
类对象的构造。这种情况下,不应该寻找语法特性来解决问题,而是应该修改这种糟糕的设计。
【注意】
- 为内置类型进行手工初始化,以免在使用它们的时候出现错误。
- 构造函数应该使用初始化列表,并且书写顺序应该和成员变量在类中声明的顺序相同。
- 对于无需做成单例的全局对象,应该将其优化为局部静态对象。