归去来兮

 

Effective C++读书笔记之三 :确定对象被使用前已先被初始化

关于对象初始化,c++似乎反复无常,例如:
int x;
在某些语境条件下保证初始化为0,而在另外一些语境下却并没有这个保证。而下边这种情况:
class Point{
int x,y;
}
;

Point p;
p的成员函数有的时候被初始化(0),有的时候没有被初始化。
现在,已经有了一些规则,对象的初始化何时会发生,何时不会发生。但是这些规则过于复杂,对我们的记忆有一定的挑战,哈哈。
通常如果使用C part of C++而且初始化可能招致运行期成本,那就不保证发生初始化。一旦进入non-C parts of C++,规则有一些变化。这就是为啥array(C part of C++)不保证其内容被初始化,而vector(STL part of C++)却有此保证。
我们的最佳处理状态是,在使用任何对象之前先将它初始化。对于无任何成员的内置类型,必须手动完成此事:
int x = 0;   //对int进行初始化;
const char *text = " A C-style string";   //对指针进行初始化;
double d;
std::cin 
>> d;   //采用input stream的方式完成初始化;
至于内置类型以外的其他类型,初始化的责任落在了构造函数身上。规则很简单:确保构造函数都将对象的每一个成员初始化。
这个规则很容易奉行,值得注意的是别混淆了赋值(assignment)和初始化(initialization)。考虑一个表现通讯簿的class:
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 tehAddress;
std::list
<PhoneNumber> thePhones;
int numTimesConsulted;
}
;

ABEntry::ABEntry(
const std::string &name, const std::string &address, std::list<PhoneNumber> &phones)
{//以下全是赋值(assignment),不是初始化(initialization);
theName = name;
theAddress 
= address;
thePhones 
= phones;
numTimesConsulted 
= 0;
}
ABEntry对象会带给你期望的值,但不是最佳做法。C++规定:对象成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName, theAddress, thePhone都不是被初始化,而是被赋值。初始化的时间发生的更早,发生于这些成员函数的default构造函数被自动调用之时(比进入ABEntry构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为他属于内置类型,不保证在你看到的那个赋值动作的时间点之前获得初值。
ABEntry构造函数的一个较好写法是,使用所谓的member initializatin list(成员函数初始列)替换赋值动作:
ABEntry::ABEntry(const std::string &name, const std::string &address,
 
const std::list<PhoneNumber> &phones)
: theName(name),
 theAddress(address),
 thePhones(phones), 
numTimesConsulted(
0)
{}//构造函数本体不需要任何动作
这个构造函数和上一个的结果相同,但是通常效率较高。基于赋值的那个版本首先调用default构造函数为theName, theAddress,和thePhones设立初值,然后立刻再对他们赋值。default的一切作为此刻都浪费了。成员初始列(member initianlization list)的做法避免了这一问题,因为初始列中针对各个变量设的实参,被拿去作为各个成员函数的实参。本例中theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。
对于大多数类型而言,比起先调用default构造函数再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有事甚至高的多。对于内置类型,如numTimesConsulted,其初始化成本和赋值的成本相同,但为了一致性最好也通过成员初始列来进行初始化。同样道理,如果我们想default构造一个成员变量,也可以使用成员初始列,只要指定nothing作为初始化列就行了,假设ABEntry有一个无参构造构造函数,我们可以实现如下:
ABEntry::ABEntry()
:theName(),             
//调用theName的构造函数
theAddress(),           //调用theAddress的构造函数
thePhones(),            //调用thePhomes的构造函数
numTimesConsulted(0)    //记得将此内置类型显式定义为0
{}
此处记得一条规则,那就是在成员初始列中列出所有的成员变量,以免还得记住哪些成员变量可以无需初值。举个例子,如numTimesConsulted属于内置类型,如果(member initialization list)遗漏了他,他就没有初值,因而可能开启“不明确行为”的潘多拉盒子。
有些情况下,即使面对的是内置类型,也一定要使用初始列。如果成员变量是const或者reference,他们一定得需要赋初值,而不是赋值。为避免成员变量何时必须在成员初始列中初始化,何时不需要,最简单的做法就是:总是使用成员初始列。
C++有着十分固定的“成员初始化次序”。是的,次序总是相同的:base classes总是先于derived classes被初始化,而class的成员变量总是以其声明次序被初始化。让我们在看一下ABEntry,其theName永远先被初始化,然后是theAddress,之后thePhones,最后是numTimesConsulted,即使他们在member initialization list中以不同的顺序出现,也不会有任何影响。为了避免迷惑,你在member initialization list中条列各个成员时,最好总是以其声明次序为次序。
一旦我们已经 很小心的将内置型成员变量明确的加以初始化,而且也确保构造函数运用member initialization list初始化base classes和成员变量,那就剩下唯一的一件事情需要操心,就是:不同编译单元内定义之non-local static对象的初始化次序。
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap_based对象都被排除。这里所说的static对象包括global对象,定义于namespace对象内的对象,在class内,在函数内,以及在file内被声明为static的对象。函数内的static对象称为local static对象,其他则成为non-local static对象,程序结束时static对象会自动销毁,也就是他们的析构函数会在main()结束时被调用。
所谓编译单元(translation unit)是指产生单一目标文件(single object file)的那些源码。基本上是单一源码文件加上其所包含的头文件。
现在,上面所涉及到的问题至少包括两个源码文件,每一个内含至少一个non-local static对象(也就是说对象是global,或者位于namespace内,或者class内,或者file作用域内被声明为static)。问题是:如果某个编译单元内的某个non-local static对象的初始化动作使用了另外一个编译单元内的non-local static对象,他所使用的这个对象可能没有初始化,因为C++对“不同编译单元内的non-local static 对象”的初始化次序并没有明确定义。
来个例子:
 假设有一个FileSystem class,它让互联网上的文件看起来像是本机。由于这个class使世界看起来像个单一文件系统,可能会产生一个特殊对象,位于global或namespace作用域内,象征单一文件系统:
class FileSystem{
public:

std::size_t numDisks() 
const;

}
;
extern FileSystem tfs;//预备给客户使用的对象,tfs代表"the file system"
FileSystem对象绝不是一个无关痛痒的对象,因此客户如果在theFileSystem对象构造完成前就使用他,会得到惨重的代价:
现在假设建立了一个class用以处理文件系统内部的目录。很自然,他们会用上theFileSystem的对象:
class Directory{
public:
Directory();

}
;
Directory::Directory()
{

std::size_t disks 
= tfs.numDisks();

}
进一步假设,这些客户决定创建一个directory对象,用来放置临时文件:
Directory tempDir(params);//为临时文件而做出的目录
现在,初始化的重要作用显示出来了,除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但是tfs和tempDir是不同的人在不同的时间创建出来的,他们是定义于不同编译单元内地non-local static对象,如何能确认tfs在tempDir之前先被初始化?
哦,这是无法确认的。
一个小小的设计可以改变这种情形:将每一个non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference用来指向他所包含的对象。然后用户调用这些函数,而不是直接指涉这些对象。换句话就是non-local static对象被local static对象替换了。这个实现的基础在于:C++保证,函数内的non-local static 对象会在函数被调用期间首次遇上该对象之定义式时被初始化。所以如果以函数调用替换直接访问non-local static 对象,就获得了保证,保证所得的那个reference将指向一个经历初始化的对象。 此技术实现于上面的代码如下:
class FileSystem{};//同前
FileSystem &tfs()//这个函数用来替换tfs对象;他在FileSystem中可能是个static。
{
static FileSystem fs;
return fs;
}

class Directory{};
Directory::Directory()
{

std::size_t disks 
= tfs().numDisks();

}

Directory 
&tempDir()
{
static Directory td;
return td;
}
修改之后,我们只是使用的是tfs()和tempDir(),而不是tfs和tempDir。也就是使用的是指向static的reference而不是static对象自身。
这样的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回他。当然,他是绝佳的inline候选,特别是经常被调用的话。但是,另一方面,任何一种non-const static对象,不论是local或者non-local,在多线程下“等待某事发生都会有麻烦的。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有的reference-returning函数(??),这可消除与初始化有关的race condition。
为避免对象初始化之前过早的使用他们,你需要做的是三件事:手工初始化内置的non-member对象;使用member initialization list;在初始化次序不确定下加强你的设计。


Things to remember:
1.Manually initialize objects of built-in type, because C++only sometimes initializes them itself;
2.In a constructor, prefer to use the member initialization list to assigenment inside the body of the constructor. List data member in the initialization list in the same order they're declared  in the class.
3.Avoid initialization order problems across translation units by replacing non-local static objects with local static objects.

posted on 2008-12-30 11:06 Edmund 阅读(307) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(1)

随笔分类

随笔档案

搜索

最新评论

阅读排行榜

评论排行榜