《Effective C++ 》学习笔记(4th.确定对象被使用前已先被初始化)

关于对象初始化这件事,想必诸位一定在某种程度上了解过,即便不算通达谙练也肯定是略有耳闻,至少关于指针,我想不论哪本C/C++语言教材,肯定都三申五令地强调指针需要初始化之后再作使用,否则很有可能会发生一些“不可预料的、后果严重的”问题。如此可怖的描述是那么地让人胆颤,以至于即便没有试过那样的操作也会有种不明觉厉的感觉(大概?)。

总之,关于使用未初始化对象的严重性在这里就不再过多赘述,况且不单单是指针,即便是普通变量,在使用时未初始化可能都会招致一些不可预料的问题,下面就开始说说《Effective C++》中关于这一章的内容:



4.确定对象被使用前已先被初始化

关于对象初始化这件事,C++似乎反复无常。如果你这么写:

int x;

在某些情况下,x会被初始化为0,但不能保证一直都会如此。如果你这么写:

class pos
{
...//省略了部分代码
int x;
int y;
}
...
pos p;

p的成员遍历有时候会被初始化为0,有时候又不会。如果你是从非C/C++语言阵营而那里并不存在“无处值对象”,那么请小心,因为这颇为重要。

读取未初始化的值会导致不明确的行为。在某些编程平台下,仅是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入了些半随机的bits,污染了正进行读取的对象,最终导致不可测知的程序行为,以及许多令人十分不爽的debug过程(深有体会)。

那么现在,我们终于有了一些规则,描述“对象的初始化动作何时一定发生,何时不一定发生”,但非常不幸的是,就我们的记忆力而言还是太复杂了。

通常,如果你使用c part of c++(条款1中有提到)而初始化可能带来一些运行期的成本,那么就不保证发生初始化。一旦进入non-c part of c++,规则就有些变化。正如数组(array)不保证其内容被初始化,而vector(stl)却有这样的保证。

表面上来看,这似乎十个无法确定的状态,而最佳的处理办法就是:永远在使用对象之前先将它初始化。对于无任何成员的内置类型,你得手工完成这件事。

int x=0;//对int进行手工初始化
const char * text="A String";//对指针进行手工初始化
double d;
std::cin>>d;//以读取input stream完成舒适化。

至于内置类型以外的东西,初始化的责任就落实在构造函数身上了,规则也很简单:确保每一个构造函数都将对象的每一个成员初始化。

class pos
{
public:
    pos(int cur_x=0,int cur_y =0);
    pos(pos&);
private:
    int member_x;
    int member_y;
}
...
pos::pos(int cur_x,int cur_y)
           : member_x(cur_x),member_y(cur_y){}
           
pos::pos(pos & cur_p)
          :member_x(cur_p.member_x),member_y(cur_p.member_y){}

至于为什么用到初始化列表而不是直接在构造函数的函数体内些赋值语句(=),是因为对于大多数类型而言,编译器会首先调用default构造函数然后再调用=操作符,所以只调用一次copy构造函数的效率自然高得多。此外,还有一点:
总是在初始化列表中列出所有的成员变量,以免还要记住哪些成员变量无需初值

有些情况下,即便面对的成员变量属于内置类型,也一定得使用初值列:const或reference,它们一定得有初值,且不能被赋值(下一个条款我们会提到)。为了避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单得做法就是:总是使用初始化列表。这样做有时候绝对必要,且往往比赋值更加高效。

许多classes拥有多个构造函数,每个构造函数有自己的成员初值列。如果这中classes存在许多成员变量以及(或者)base classes,多份成员初值列的存在就会导致不受欢迎的重复(初值列内)和及其无聊的工作(对于编码的各位而言)。这种情况下可以合理地在初值列中遗漏那些赋值表现像初始化一样好的成员变量,改用他们的赋值操作,并将那些赋值操作放在某个函数(通常是private),以供所有的构造函数调用。这种做法在成员变量的初值系由文件或数据库读入时特别有用。然而,比起经由赋值操作完成的伪初始化,通过初始化列表完成的真正初始化通常更加可取。

C++的初始化次序是十分固定的,没错,base classes(基类)会更加早于derived classes(派生类) 被初始化,而class的成员变量总是以其被声明的次序初始化,关于这一点不得不说的是,即使它们在初始化列表中以不同的次序出现,也不会有任何影响,况且这是一个合法的操作(某些编译器会对这点有提示)。为了避免你或者你的代码检阅者迷惑,并避免某些可能存在的错误(两个成员变量的初始化带有次序性,例如初始化array的时候需要指定大小,因此,代表大小的那个成员变量必须先有初值),当你在成员初值列中条列各个成员时,最后总是以其声明的次序作为次序。

一旦你已经很小心地将内置成员变量明确地加以初始化,而且你也确保构造函数运用初始化列表来初始化base classes和成员变量,那就只剩唯一一件事需要操心:不同编译单元内定义之non-local static 对象的初始化次序。

好吧,这一大串看起来确实有些惹人生厌了。我们来一点点钻探这串词组。

所谓static对象,其寿命会从被构造出来到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象(全局变量)、定义于namespace作用域内的对象、在classes内、函数内、以及在file作用域内被声明为static的对象。函数内的static对象被称为local static对象,其他的static对象就是non-local static对象了。

程序结束时,static对象会自动销毁,也就是它们的析构函数会在main函数结束时被自动调用。

所谓编译单元,是指产出单一目标文件的那些源码。基本上它是单一cpp文件加上其所含入的头文件。

现在,我们所关心的问题涉及至少两个源码文件,每一个内至少含有一个non-local static对象。真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义。

不过我们可以用一个很简单的设计来消除这个问题。唯一需要的做的就是将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个引用(reference)指向它所含的对象。然后用户调用这些函数,而不是直接染指这些对象。换句话说,non-local static对象是被local static对象替换了。

这个方法的基础在于,C++保证,函数内的local static对象会在该函数被调用期间首次遇上该对象的定义式时被初始化。所以如果你以函数调用替换直接访问non-local static对象,你就获得了这个保证,即你所获得的那个引用将指向一个历经初始化的对象。

当然,运用返回一个引用(reference-returning)函数来防止初始化次序问题是由一个前提的:其中有一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却受制于B是否已经初始化,这时候就有麻烦了。

既然如此,为了避免在对象初始化之前过早地使用它们,你需要做三件事:一、手工初始化内置的非成员(non-member)对象。二、使用成员初值列对付对象的所有成分。最后,在初始化次序不确定性氛围下加强你的设计。

总结一下:

  • 为内置型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用初值列,而不要在构造函数内使用赋值操作。初值列列出的成员变量,其顺序应该与在class内中的声明一致。
  • 为免除跨编译单元初始化次序的问题,请以local static对象替换non-local static对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值