确定对象被使用前已先被初始化(Effective C++之04)

C++中的默认值

在C++中,编译器不会默认检查对象是否被初始化,这就会导致对象在不初始化的情况下被使用,造成不可预计的后果。因此,C++程序员必须保证自己所使用的变量已经有了一个明确的初始值。

一般情况下,在C++中,如果一个变量为全局代码段,则一般编译器会给一个初始的定值,如int类型会赋0。而定义为局部变量时,则默认值为随机数。但是在vs调试时,DEBUG版本和Release版本版本还有区别: http://www.dewen.org/q/8450/C%2B%2B+%E4%B8%AD+bool+%E5%8F%98%E9%87%8F%E7%9A%84%E9%BB%98%E8%AE%A4%E5%80%BC%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F

C++提供的标准模板库,会保证变量会有一个默认的初始值(非随机值),int初始化为0,string初始化为空串等。

确保对象使用前被初始化

尽管程序员对C++的默认行为很了解,但是默认的值和机器,编译器等都有关系,不确定性太多。因此最好的做法是,在使用对象之前,永远手动给对象初始化。如:

int x = 0;
const char* text = "ABC";
double d;
std::cin >> d;

如果是程序员自定义的类型,则满足以下规则:确保每一个构造函数都将对象的每一个成员初始化。但是要注意一点,“赋值”和“初始化”这两个操作在构造函数中很容易被混淆。看下面的例子:

class A {
public:
    A(const std::string& name, const std::list<std::string>& namelist);
private:
    std::string name;
    std::list<std::string> namelist;
    int nameCount;
};

A::A(const std::string& n, const std::list<std::string>& nl)
{
    name = n;                   //这些都是赋值,不是初始化。
    namelist = nl;
    nameCount = 0;
}

 在上面的代码中,name,namelist在构造函数中的动作都是赋值而不是初始化。初始化的动作发生在进入构造函数本体之前。 但是nameCount属于基本类型,不一定会在本构造函数之前经过初始化。

对于构造函数一个比较好的写法是,采用C++定义的成员初值列(member initialization)来执行:

A::A(const std::string& n, const std::list<std::string>& nl)
        :name(n), namelist(nl), nameCount(0)    
{
}

现在,这些都是初始化操作,name, namelist都调用了其copy构造函数进行初始化,且比赋值操作的构造函数效率较高,因为赋值的构造函数在赋值之前已经会对一些成员调用了默认的构造方法进行初始化。对大多数类型来说,直接调用copy构造函数比先调用默认构造再调用copy赋值都是高效的。

使用初值列还有一个好处就是:如果成员变量的类型是const或references,那么它们就不能在构造函数中进行赋值,而必须进行初始化。因此简单的做法是:永远使用初值列。而且在初值列中列出所有成员变量,即使有些不需要进行初始化,但是这样做的话利于维护和代码的阅读。

需要注意一点:C++的成员初始化的次序是固定的,它总是按照其声明的顺序进行初始化。在上面的例子中,初始化的顺序永远是:name, namelist, nameCount,而且这个顺序和初值列中写的顺序无关。(为了避免混淆,初值列中的顺序最好和声明的顺序相同)

“不同编译单元内定义之non-local static对象”的初始化

static对象是指:global对象,定义在namespace作用域的对象,在class,函数,文件作用域中的static对象。其中,函数内的static对象是local static,其它的static对象是non-local static对象。static对象在程序结束时被销毁。不同编译单元,是指产生的目标文件不同,基本上一个编译单元是指其源码文件加上其头文件。

我们遇到的问题是:如果在一个编译单元内的某个non-local static对象初始化用到了另一编译单元内的non-local对象,但是它所用到的这个对象尚未被初始化,就会出现错误。C++对这一初始化顺序并未进行定义。

如下面的例子(effective C++原书的例子):

class FileSystem {
public:
    ...
    std::size_t numDisks() const;
    ...
};
extern FileSystem tfs;

在另一个编译单元中有如下代码:

class Directory {
public:
    Directory(params);
    ...
};
Directory::Directory(params) 
{
    ...
    std::size_t disks = tfs.numDisk();
    ...
}
Directory tempDir( params );

那么,上述代码就必须要求tfs在tempDir之前进行初始化。但是C++编译器对这一顺序并无明确定义,因为要明确分析这一顺序是非常困难的。然而我们可以做一个设计来解决这一问题:将每个non-local static对象搬到自己的专属函数内,然后返回该对象的引用。这一设计类似Singleton模式。因为C++会保证:函数内的local static对象会在该函数被首次调用时被初始化。而且还有一个好处,如果从来没有调用过这个函数,这个对象将不会被初始化,减小析构成本,对于上述例子的non-local static对象不会做到这种效果。

用这个设计修改上述示例:

class FileSystem {};                 //同前
FileSystem& tfs()
{
    static FileSystem fs;
    return fs;
}
class Directory {};                    //同前
Directory::Directory(params) 
{
    ...
    std::size_t disks = tfs().numDisk();
    ...
}
Directory& tempDir()
{
    static Directory td;
    return td;
}

上述三点:

1. 为内置类型对象进行手工初始化,因为C++不保证初始化它们。

2. 构造函数最好使用成员初值列(member initialization list)。初值列的成员变量,排列次序应与在class中声明的次序相同。

3. 以local static对象替换non-local static对象。

posted @ 2013-01-06 01:43  xd_xiaoxin  阅读(1107)  评论(0)    收藏  举报