确定对象使用前已先被初始化
1. 初始化的烦恼
读取未初始化的变量,会导致不明确的行为。 在某些情况下,仅仅是读取未初始化的值,就会让你的程序崩溃。也有可能,读入一些“半随机”的bits,导致你的程序执行不可预测的路线。最终使得自己陷入不愉快的Debug过程。
2. 保证初始化
先谈谈最佳的解决方法:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你得手动的完成这件事。 以下是例子
int x=100;
const char* y="HelloWorld";
double z;
std::cin>>z;
至于内置类型之外的数据类型,初始化的责任落在构造函数身上。 规则很简单:确保每一个构造函数都将对象的每一个成员初始化。
class AInit:public AActor
{
public:
int TestInt=100;
double TestDouble=100.0;
public:
AInit():
TestInt(1),
TestDouble(99.99){
TestInt=1;
TestDouble=99.9;
}
}
-
我们可以看到有三种赋初值的方式,它们执行的顺序是先定义(初始化),再进入构造函数时的成员初始化列表(初始化),再构造函数内的语句(此时是赋值)。=-= 其中,比较推荐的是在初始化列表里对成员变量进行初始化,且规定总是在初值列列出所有成员变量,避免遗漏造成不明确的行为。 对于未在初始列指定初值的成员,编译器会自动调用default构造函数(也就是说会先调用默认构造函数,在执行构造函数内的语句 )。 对于内置类型,有时候也需要使用初值列,如果成员变量是const或者references,他们就一定需要初值。 为了避免记住那些成员需要初始化,最简单的做法就是:总是使用初值列。 在Classes中,可能具有很多构造函数,每个函数都有自己的初值列。当成员过多的时候,会导致不受欢迎的重复和无聊的工作。 这种情况下可以合理的省略那些“赋值表现的像初始化一样好”的成员变量,改用它们的赋值操作。当需要从文件或者数据库读入时这种方式很有用。当然,使用成员初值列完成真正的初始化通常更加可取。
3. 初始化的次序
C++有着十分固定的“成员初始化次序“。 基类比其派生类更早初始化。 而类的成员变量总是以其声明的次序被初始化。(与在成员初值列的次序无关,为了不引起疑惑,在初值列的次序应该以声明次序作为参考) 如果变量初始化具有依赖关系的话,那么在代码中的次序需要被调整。
4. 不同编译单元间的non-local static对象的初始化
我们一步一步理解。 所谓static对象,其生命周期从被构造出来到程序结束为止(作者的意思不只是用static关键字修饰的对象,广义的static?,这点容易不理解)。所以不包括stack对象和heap-based对象。主要指global对象,定义于namespace的对象,在classes内,在函数内,以及在file作用域被声明static的对象。函数内的static对象被称为local static对象(因为它们相对函数而言是local),其他static对象被称为non-static对象。 程序结束时static对象会自动销毁,也就是它们的析构函数会在main函数结束时被调用。
所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码,基本上就是单一的源码文件加上其所含的头文件。
然后现在这里有一个问题,如果某编译单元某个non-local static对象的初始化使用了另一个编译单元的non-local static对象,但是用到的这个对象可能尚未被初始化。因为C++对于定义在”不同编译单元内的non-local static对象”的初始化次序并无规定。(因为决定它们的初始化次序相当困难,甚至无解)
A.h
class ClassA{
public:
int number=100;
};
ClassA a;
B.h
#include "A.h"
extern ClassA a;
class ClassB{
public:
ClassB{
std::cout<<a.number;
}
};
然后我们怎么去解决呢? 幸运的是,一个小小的设计就可以了,我们唯一要做的是:将每一个non-local static对象搬到自己类的专属函数中去。(该对象在此函数中被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接涉及这些对象。换句话说,non-local static对象被local static对象替代了。 对于那些熟悉设计模式的伙伴们,一定认出来这就是大名鼎鼎的单例模式 (Singleton)一个常见的实现手法。 这个手法的基础在于:C++保证,函数内的local static对象会在该函数首次调用被初始化。
class ASingleton:public AActor
{
public:
int TestInt=100;
public:
static ASingleton& StaticClass()
在 {
static ASingleton Singleton;
return Singleton;
}
};
但是注意,在多线程的情况下,可能会出现一些初始化竞争问题。 处理这个麻烦的一种做法:在程序的单线程启动阶段手工调用所有reference-returning函数。
当然,运用reference-returning函数 可防止“初始化次序问题”,前提是对象有一个合理的初始化次序。如果对象A必须在对象B之前初始化,但A的初始化是否成功却又受制于B是否已初始化,这时候你就有麻烦了。如果避开如此病态的境况,此处描述的办法应该可以提供良好的服务,至少在单线程的程序中。
总结
- 为内置型对象进行手工初始化,因为C++不保证初始化它们。
- 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。
|