重读经典-《Effective C++》Item4:确定对象被使用前已先被初始化

晨曦之光 发布于 2012/03/09 14:13
阅读 205
收藏 0

本博客(http://blog.csdn.net/livelylittlefish )贴出作者(三二一@小鱼)相关研究、学习内容所做的笔记,欢迎广大朋友指正!

 

 

1. 永远在使用对象之前先将它初始化

 

(1) 对于无任何成员的内置类型,须手工进行初始化。

如:

int x = 0;

Const char* text = "A C-style string";

 

Double d;

Std::cin >> d;  //以读取input stream的方式完成初始化

 

(2) 对于内置类型意外的任何其他东西,初始化由构造函数函数完成。

 

规则1确保每一个构造函数都将对象的每一个成员初始化。

注意:别混淆了赋值(assignment)和初始化(initialization)

 

如:

class PhoneNumber{...};

class ABEntry  //Address Book Entry
{
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)
{
//all these are assignments, not initializations
theName = name;
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}

 

C++规定:对象的成员变量的初始化动作发生在进入构造函数本体之前。

初始化的发生时间是这些成员的default构造函数被自动调用之时。

 

以上例子中,ABEntry的构造函数内,ABEntry类的成员变量都不是被初始化,而是被赋值。他们的初始化在进入ABEntry构造函数本体之前。

对于内置类型的numTimesConsulted不为真,内置类型不保证一定在我们看到的那个赋值动作之前获得初值。

 

推荐使用成员初始化列表(member initialization list)

 

ABEntry::ABEntry(const std::string& name, const std::string& address,

                 const std::list<PhoneNumber>& phones)

    :theName(name),          //now, all these are initializations

     theAddress(address),

     thePhones(phones),

     numTimesConsulted(0)

{                               //now, constructor do nothing

}

 

该版本和上一个赋值的版本效果相同,但该版本的效率高。原因如下:

  • 基于赋值的版本首先调用default构造函数为成员变量设初值,然后立刻在对他们赋予新值。
  • default构造函数的一切作为因此浪费了。
  • 成员初始化列表避免了这一问题。

 

若想要default构造一个成员变量,也可以使用成员初始值列表,只要制定nothing做为初始化实参即可。如:

 

ABEntry::ABEntry()

    :theName(),           //call default constructor of theName

     theAddress(),        //do the same thing for theAddress

     thePhones(),         //do the same thing for thePhones

     numTimesConsulted(0) explicit initialize the build-in type member 0

{

}

 

规则2在成员初始化列表中列出所有成员变量。

规则3总是使用成员初始化列表。

 

有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定要使用初始化列表。

例如,如果成员变量是const或者references,他们就一定需要出示,不能被赋值。

 

为避免需要记住成员变量何时必须在成员初始化列表中初始化,何时不需要,最简单的做法就是使用规则3

 

2. 成员初始化次序

 

成员初始化次序:

  • Base class
  • Derived class

class的成员变量总是以其声明次序被初始化。即使他们在成员初始化列表中以不同的次序出现,也不会有任何影响。

 

static对象包括:

  • global对象
  • 定义于namespace作用域内的对象
  • class内、函数内、在file作用域内被声明为static的对象

 

  • Local static对象:函数内的static对象
  • Non-local static对象,即其他static对象,如
    • global对象
    • namespace作用域内的对象
    • class内或file作用域内被声明为static的对象。

 

程序结束时static对象会被自动销毁,即他们的析构函数会在main()结束时被自动调用。

 

编译单元(translation unit):是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)

 

C++对“定义与不同编译单元内的non-local static对象”的初始化顺序并无明确定义。

 

如:在一个编译单元中定义FileSystem

class FileStystem

{

public:

    ...

    std::size_t numDisks() const;

    ...

};

 

extern FileSystem tfs;  //the object reserved for users to use

 

在另一个编译单元中定义Directory

class Directory

{

Public:

    Directory(params);

    ...

};

 

Directory::Directory(params)

{

    ...

    std::size_t disks = tfs.numDisks();  //use tfs object

    ...

}

 

Directory tempDir(params);  //create temporary directory

 

在该例子中,tfs必须在调用Directory的构造函数之前初始化,否则tempDir的构造函数会用到尚未初始化的tfs

tfstempDir是不同的人在不同的时间于不同的源码文件建立起来的,他们是定义于不同编译单元内的non-local static对象。那么,如何确定tfs会在tempDir之前先被初始化?

 

答案是无法确定。

 

可通过如下设计——Singleton模式——解决这个问题:

  • 将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。
  • 然后用户调用这些函数,而不直接指涉这些对象。
  • non-local static对象被local static对象替换了。

 

该方法的基础:C++保证,函数内的local static对象会在该函数被调用期间首次遇上该对象之定义式时被初始化。

 

使用该方法解决上述例子,如下。

class FileStystem

{

public:

    ...

    std::size_t numDisks() const;

    ...

};

 

//this function used to replace tfs object, it can be static in FileSystem

FileSystem& tfs()

{

    static FileSystem fs;  //define and initialize a local static object

    return fs;             //return a reference to this object

}

 

class Directory

{

Public:

    Directory(params);

    ...

};

 

Directory::Directory(params)  //now, reference to tfs is replaced with tfs()

{

    ...

    std::size_t disks = tfs().numDisks();  //use tfs function

    ...

}

 

 

Directory& tempDir()       //this function used to replace tempDir object

{

    static Directory td;   //define and initialize a local static object

    return td;             //return a reference to this object

}

 

修改后,这个系统程序的客户完全像以前一样使用它,唯一不同的是使用tfs()tempDir()代替tfstempDir对象。即使用函数返回的指向static对象的reference,而不再使用static对象本身。

 

任何一种non-const static对象,不论他是local还是non-local,在多线程环境下等待某事发生都会有麻烦。

处理该麻烦的做法:在程序的单线程启动阶段(single-threaded startup portion)手工调用所有reference-returing函数,可消除与初始化有关的race conditions(具体参考其他资料)

 

总结:为避免在对象初始化之前过早地使用他们,要做

  • 手工初始化内置型non-member对象
  • 使用成员初始化列表(member initialization lists)对付对象的所有成分
  • 在初始化次序不确定性(这对不同编译单元所定义的non-local static对象是一种折磨)情况下加强你的设计

 

Rember

  • 为内置型对象进行手工初始化,因为C++不保证初始化他们。
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作(assignment)。初始化列表中列出的成员变量,其排列次序应该和他们在class中的声明次序相同。
  • 为免除跨编译单元之初始化次序问题,以local static对象替换non-local static对象。

原文链接:http://blog.csdn.net/livelylittlefish/article/details/5744603
加载中
返回顶部
顶部