C++对于初始化这件事是喜怒无常的,比方这行代码:
int x;
又时候C++编译器会将它初始化为0,但有时候却又不一定。如果编译器真的读取到一个为初始化的值时就会导致各种不明确也的伪白兔,情节较轻的话就会终止程序,情节较重的话,就会读取一些伴随及的值,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为。
事实上关于对象的初始化动作合是一定发生,何时不一定发生是由一些规则的,但是这些规则没必要去浪费时间记,既然你说未初始化有风险,那我就全部初始化好了,全初始化不就没有问题了吗?
内置类型可以给他初始化值,或者通过用户的输入来获取:
int x = 0;
const char 8text = "A C-Style string";
double d;
std::cin >> d;
内置类型以外的,其初始化就靠构造函数,其规则为:确保每一个成员都被初始化了:
class PhoneNumber {...};
class ABEntry
{
public:
ABEntry(const std::string &name, const std::string &address, const std::list &phone);
private:
std::string theName;
std::string theAddress;
std::list thePhones;
int numTimeConsulted;
};
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list &phone)
{ /*这些都是赋值而不是初始化*/
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
这是我们自己常用的写法,然鹅编译器是这么做的:在构造函数之前就发生了初始化,然后进入构造函数,最后再是构造函数里面的赋值。我们现在换一种写法:
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list &phone)
:theName(name),
theAdress(address),
thePhones(phones),
numTimesConsulted(0) //初始化
{ } //初始化过了,于是就不用再函数体内做任何事情了。
这个写法和上面结果相同,但效率高一些。由于编译器会为用户自定义类型的成员变量自动调用default构造函数,所以赋值的版本首先调用default构造函数为theName, theAdress, thePhones设初值,然后立刻再为它们赋新值,default构造函数为此做的一切因此就浪费了。成员初值列(第二种写法)就很好地避免了这一问题,因为初始值列中针对哥哥车官员变量二设的实参,被拿去作为各成员变量值构造函数的实参。theName以name为初值进行copy构造,以此类推。对于大多数类型而言,比起先调用default构造函数在调用copy assignment操作符,单只调用一次copy构造函数时比较搞笑的。但对于内置类型对象,如:numTimeConsulted,其初始化和赋值的成本是相同的,但为了一致性,我们最终还是选择了初值列。
我们为了避免去记那些成员需要被指定初值,因此我们呢立下一个规定:总在初值列中列出所有陈冠变量,可以没有初值,以防止因为遗漏导致没有初值而引发不明确行为。如下所示:
ABEntry::ABEntry(const std::string &name, const std::string &address, const std::list &phone)
:theName(), //调用default构造函数
theAdress(),
thePhones(),
numTimesConsulted(0) //将numTimesConsulted显示初始化为0
{ }
另外,即使遇到一个内置类型,我们也不可以用赋值的方法设置其初值,一定得用初值列!比方说成员变量是const或reference,他们是不可以被复制的。为了避免这种情况,这边建议您总是使用成员初值列呢!
如果想的多的话,会想到还有一个问题:加入class有多个构造函数,每个构造函数都有自己的成员初值列,多份成员初值列就会导致重复,有没有办法避免呢?答案是有的。我们可以在成员初值列中遗漏那些赋值与初始化成本相同的,改用他们呢的赋值 操作,并将那些赋值操作以往某个函数(通常是private),共所有构造函数调用。这种做法在“成员变量的初值系有文件或数据读入”时特别有用。
C++有着自己固定的“成员初始化次序”:base classes更早于其derived classes初始化,而class的成员总是以其声明次序被初始化为了避免迷惑以及一些可能的隐晦错误(例如:初始化一个数组需指定大小,因此得先初始化代表大小的成员),还是按照声明次序来条列初值列!事实上你不按声明顺序条列初值列也是可以的,但这并不会影响编译器是按声明次序被初始化的。
我们再看下一个话题之前先俩看几个概念:
1、static对象,其寿命从被构造出来知道程序结束位置,因此stack和heap-based对象都被排除。这种对象包括gloable对象,定义域namespace作用域内的对象,在classes内 ,在函数内,在file作用域中被声明为static的对象。在函数内的static对象称为local static对象(对于自己的函数是本地人),其他的static都是non-local的(其他的都是外人)。程序结束时static对象会被自动销毁(其析构函数会在main结束时被自动调用)
2、编译单元,指的是产出单一目标文件的那些源码。包括单一源码文件加上其所含如的头文件。
现在我们来看一句话:不同编译单元内定义的non-local static对象的初始化次序。
由于C++对于定义与不同编译单元内的non-local static对象的初始化次序并没有明确定义,假设我们现在有两个源码文件,每一个内含至少一个non-local static对象(该对象时global或者 定义域namespace作用域内抑或是在class内或file作用域内被声明为static)。我们在某个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static 对象,使用到的对象可能并没有被初始化。
来看个例子:
class FileSystem
{
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs; //extern会在之后说明
现在有个程序员建立了一个class用以 处理文件系统内的目录,用上FileSystem对象:
class Directory
{
public:
Directory(params);
...
};
Directory::Directory(params)
{
...
std::size_t disks = tfs.numDisks(); //使用tfs对象
...
}
现在创建一个Directory对象,用来放置临时文件:
Directory tempDir(params);
现在问题来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tmpDir是不同人在不同时间于不同源码文件中创建的,他们是定义于不同编译单元内的non-local static对象。你是无法确定他们的初始化顺序的,原因在于:决定他们呢的初始化次序相当困难,根本无解。那么就完全没有办法了吗?并不是,他不是不是local static对象吗?那我给他个户口,让他变成本地人不就好了。换句话说就是non-local static对象被local static对象给替换了。这就是Singleton模式 常用的一个手法,其精髓在于函数内的local static对象会在该函数被调用期间首次遇上该对象的定义式时被初始化。所以如果你用一个返回引用的函数(引用指向local static对象)替换直接访问non-local static对象,你就获得了保障。更棒的是,如果你从未调用non-local static对象的“仿真函数”就绝不会引发构造和析构成本!修改后如下:
class FileSystem
{
public:
...
std::size_t numDisks() const;
...
}; //同上面
FileSystem& tfs() //这个函数用来替换tfs对象;它在FileSystemclass中
{ //可能是一个static,定义并初始化一个local static对象
static FileSystem fs; //返回一个reference指向该对象
return fs;
}
class Directory
{
public:
Directory(params);
...
}; //同上面
Directory::Directory(params)
{
...
std::size_t disks = tfs().numDisks(); //使用tfs对象
...
}
Directory& tempDir() //替换tempDir对象
{ //它在Directory class中可能是个static
static Directory td; //定义并初始化local static对象
return td; //返回一个reference指向上述对象
}
我们用tfs()和tempDir()代替最初的tfs和tempDir,也就是说我们使用的是指向对象的static对象的reference而不再是static本身。
这些函数内含“static”对象的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论其是否为local还是non-local,在多线程环境下等待某事发生都会有麻烦,处理这一麻烦的做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的竞速形势。
总结一下,为避免在对象初始化之前过早地使用它们我们应该:
1、手工初始化内置型non-member对象。
2、使用成员初值列对付对象内的所有成员
3、在初始化次序不确定时,使用函数代替对象