在确保对象在使用前已先被初始化这一条款的编码实践中,作者为我们总结了三条经验,它们分别是:
-------------------------------------------------------------------------------------------------------
ⅰ. 手工初始化内置类型对象
ⅱ. 构造函数最好使用成员初值列,而不要在构造函数内使用赋值操作,其排列次序应和他们在类中声明的次序相同
ⅲ. 用local static对象替换non-local static对象,以避免“跨编译单元的初始化次序”问题
-------------------------------------------------------------------------------------------------------
下面我们就一条一条的来解读作者的这三条经验。

□第一节. 手工初始化内置类型对象

在解读这条经验之前,我们先来看一段简单的程序让大家对“对象初始化先行”的重要性有一个认识。
-------------------------------------
1| #include <iostream>
2| using namespace std;
3|
4| int main(){
5|
6| int x;
7| cout<<x<<endl;
8|
9| system("pause");
10| return 0;
11| }
-------------------------------------
问题:上面的代码输出什么?
-------------------------------------
1| Windows: 不定值
2| Linux: 不定值
3| Unix: 0
-------------------------------------
看,多么危险的一件事。什么?看不出危险,好吧!试想,假如变量x里面是你的银行存款。1. 银行采用Windows做服务器,假如x的值是198812282,你会因掠夺银行财富而进某某监狱;2. 银行用了Unix系统,你会忍气吞声吗?当然,这只是一个玩笑,不过透过这个玩笑,让我们对变量的初始化先行的重要性有了更深的认识。
为什么变量要初始化呢?初始化和不初始化有什么区别呢?一切高级语言的本质都是汇编码,我们究其本质,来看下面的代码片段:
-------------------------------------------------------------------------------------------------------------------------
1| int x; | int x = 0;
2| cout<<x<<endl; | cout<<x<<endl;
-|-----------------------------------------------------------------------------------------------------------------------
1| mov eax,dword ptr [__imp_std::endl (10D2040h)] | mov eax,dword ptr [__imp_std::endl (0CC2040
2| mov ecx,dword ptr [esp] | mov ecx,dword ptr [__imp_std::cout (0CC2044
⇒3| push eax | push eax
⇒4| push ecx | push 0
5| mov ecx,dword ptr [__imp_std::cout (10D2044h)] |
6| call dword ptr [__imp_std::basic_ostream<char,std... | call dword ptr [__imp_std::basic_ostream<cha
7| mov ecx,eax | mov ecx,eax
8| call dword ptr [__imp_std::basic_ostream<char,std... | call dword ptr [__imp_std::basic_ostream<cha
-------------------------------------------------------------------------------------------------------------------------
从汇编码中,我们可以看出:
----------------------------------------------------------------------------------------------------------------------------------
▲.未初始化变量的程序会将栈顶指针esp指向的未知值放到寄存器ecx里,然后压栈供后面的cout使用;初始化变量的程序则将数值0压栈,供后面的cout使用。
----------------------------------------------------------------------------------------------------------------------------------
由于main也是一个函数,它内部的所有变量都是存储在栈里的,倘若栈里的值没有被初始化,那么就会得到一块"脏数据",运行结果就成了不可期待的了,这非常危险。因此,上面的内置类型对象x在声明之后,就应该被手动的初始化"x = 0"。

□第二节. 构造函数最好使用成员初值列,而不要在构造函数内使用赋值操作,其排列次序应和他们在类中声明的次序相同

内置类型的对象我们是可以手动初始化的,而其外的任何其他对象就需要构造函数出场了。那成员初值列是什么?我们仍然以一个示例程序作为开始,然后展开讨论。
-------------------------------------
1| #include <iostream>
2| #include <string>
3| using namespace std;
4|
5| class MyType{
6| };
7|
⇒ 8| class A{
⇒ 9| public:
⇒10| A(){
⇒11| this->m_IntValue = 0;
⇒12| this->m_StrValue = "";
⇒13| this->m_MyType = 0;
⇒14| }
15| private:
16| int m_IntValue;
17| string m_StrValue;
18| MyType *m_MyType;
19| };
20|
21| int main(){
22| A a;
23| return 0;
24| }
-------------------------------------
你是不是这样写构造函数的呢?我也是这样写构造函数的,至少在阅读这个条款之前。但是今天的条款04认为这样的写法欠妥,为什么呢?来看看作者的理由:
-----------------------------------------------------------------------
ⅰ. C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前
ⅱ. 上面写法是赋值,而不是初始化,对于对象,赋值操作的效率要比初始化低
-----------------------------------------------------------------------

1.1. C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前
这一点比较坑爹,它说了“C++规定”,我找了好多资料都没提到这一点。先不管这些,我们按照他说的来改造我们的构造函数,于是我们的类A变成了下面的样子:
-------------------------------------
1| class A{
2| public:
3| A():m_IntValue(0), m_StrValue(""),m_MyType(0){
4| }
5| private:
6| int m_IntValue;
7| string m_StrValue;
8| MyType *m_MyType;
9| };
-------------------------------------
关于上面的修改,以下内容援自《Effective C++》侯捷译第三版来进行说明:
-------------------------------------------------------------------------------------------------------------------------------
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本首先调用default构造函数为成员对象设初值,然后立刻再对它们赋予新值。default构造函数的一切作为因此浪费了。成员初值列(member initialization list)的做法避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。
--------------------------------------------------------------------------------------------------------------------------------
1.2. 上面写法是赋值,而不是初始化,对于对象,赋值操作的效率要比初始化低

我并不相信,所以写了一个测试程序,看看到底效率有没有差异,测试代码如下:
-------------------------------------------
1| int i = 0;
2| DWORD start_time = GetTickCount();
3| {
4| for(i = 10; i < 10000; i++){
5| A a;
6| }
7| }
8| DWORD end_time = GetTickCount();
9| cout<<end_time - start_time<<endl;
-------------------------------------------
==测试结果(Debug)==
-----------------------------------------------------
成员初值列初始化 手动赋值初始化
i=1000 : 0 0
i=10000 : 16 62
i=100000 : 201 256
-----------------------------------------------------
上面的结果或许能支撑作者的观点吧!好吧,数据面前说话,我们姑且认为作者是对的。

□第三节. 用local static对象替换non-local static对象,以避免“跨编译单元的初始化次序”问题

这个问题要稍微复杂一点了,至少作者用了很大的篇幅来描述他的问题和观点。而我觉得理解这一点的核心就三点:
-------------------------------------
ⅰ. 理解什么是local static和non-local static
ⅱ. 理解什么是跨编译单元和初始化次序
ⅲ. 为什么将non-local static变成local static就能避免初始化次序问题
-------------------------------------
3.1. 理解什么是local static和non-local static
理解二者最好的方法就是看一段代码:
-------------------------------------------
1| #include <iostream>
2| using namespace std;
3|
4| // 该静态变量不再函数中,因此是non-local static的
5| static int non_local_static_value = 10;
6|
7| void test(){
8| // 该静态变量在函数中,因此是local static的
9| static int local_static_value = 10;
10| }
11|
12| int main(){
13| return 0;
14| }
-------------------------------------------
这就不用细说了,不过需要提醒一下,local static的变量可以存在的位置有:class内、函数内、file作用域。non-local static的变量可以存在的位置:global、namespace内。

3.2. 理解什么是跨编译单元和初始化次序
所谓编译单元,是指单独生成目标文件的那些源码,比如我们在配置编译文件时,经常看到Makefile里有OBJ=XXX的字样,这就是目标文件。而跨编译单元的意思是我在A目标文件里使用B目标文件中的变量。要跨编译单元访问,我们有一个关键字extern。示例代码如下:
-------------------------------------------
1| class MyType{
2| };
3| extern MyType mt; // 这句代码告诉其他编译单元,mt这个全局变量你们是可以使用的
-------------------------------------------
所谓初始化序列,是指构造函数在为其成员变量初始化时的顺序。当然,这里并不仅仅指成员变量的初始化次序,还有全局的变量,静态变量等。这里关于次序有一个问题,试想,如果成员变量m_A的值需要由m_B计算而来,那么谁先初始化呢?当然是m_B了。类的构造函数是按照其成员变量声明次序来初始化的。但是跨编译单元的non-local static变量初始化是没有这样一个顺序的。那么我们就有了一个大问题了。
---------------------------------------------------------------------------------------------------------------------
★问题:因为没有严格的初始化次序规定,有极大的可能,A编译单元使用了还没有初始化的B编译单元里的non-local static变量。
---------------------------------------------------------------------------------------------------------------------
3.3. 为什么将non-local static变成local static就能避免初始化次序问题
在解释这一点之前,我们将作者给的解决方案的代码贴出来:
-------------------------------------------
1| class MyType{
2| };
3| // 这里定义了全局函数来将non-local static的变量变为local static
4| MyType& GetMT(){
5| static MyType mt;
6| return mt;
7| }
-------------------------------------------
这样做的根据是什么呢?那是因为C++有下面这样一条规定:
------------------------------------------------------------------------------------------------------
★C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。
------------------------------------------------------------------------------------------------------

■总结■

1. 对内置类型,如int、double、float、char等,声明变量后,要手工初始化对象
2. 对内置类型对象以外的对象,要用构造函数初始化,最好使用成员初值列,而不要在构造函数内使用赋值操作,其排列次序应和他们在类中声明的次序相同
3. 对跨编译单元的对象,要用全局函数将non-local static变为local static对象

[Effective C++ --009]确定对象被使用前已先被初始化的更多相关文章

  1. Effective C++(4) 确定对象被使用前已先被初始化

    危害:读取未初始化的值会导致不明确甚至是半随机化的行为. 最佳处理办法:永远在使用对象之前先将它初始化:确保每一个构造函数都将对象的每一个成员初始化. 1 注意区分赋值和初始化: 从初始化的角度而言, ...

  2. Effective C++ 之 Item 4:确定对象被使用前已先被初始化

    Effective C++ Chapter 1. 让自己习惯C++ (Accustoming Yourself to C++) Item 4. 确定对象被使用前已先被初始化 (Make sure th ...

  3. EC读书笔记系列之2:条款4 确定对象被使用前已先被初始化

    条款4:确定对象被使用前已先被初始化 记住: ★为内置对象进行手工初始化,因为C++不保证初始他们 ★构造函数最好使用初始化列表,而不要在构造函数本体内使用赋值操作.初始化列表列出的成员变量,其排列次 ...

  4. Effective C++学习笔记 条款04:确定对象被使用前已先被初始化

    一.为内置类型对象进行手工初始化,因为C++不保证初始化它们. 二.对象初始化数据成员是在进入构造函数用户编写代码前完成,要想对数据成员指定初始化值,那就必须使用初始化列表. class A { pu ...

  5. EffectiveC++条款04:确定对象被使用前已先被初始化

    不要混淆赋值和初始化,对于大多数类型而言,比起先调用默认构造函数然后调用赋值操作符,只调用一次拷贝构造函数是高效的 对于内置类型,也需要成员初值列(member initialization list ...

  6. [effictive c++] 条款04 确定对象被使用前已被初始化

    成员初始化 在c和c++ 中,使用为初始化的类型经常会引发不可预料的错误,从而使得我们要花费巨大的时间用于调试查找问题,所以确定对象被使用前已被初始化是个非常好的习惯. 永远在使用之前对对象进行初始化 ...

  7. 读书笔记 effective c++ Item4 确保对象被使用前进行初始化

    Item4 确保对象被使用前进行初始化 C++在对象的初始化上是变化无常的,例如看下面的例子: Int x; 在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证.看下面的例子: Cla ...

  8. 条款4:确定对象被使用前已被初始化(Make sure that objects are initialized before they&#39;re used)

    其实 无论学何种语言 ,还是觉得要养成先声明后使用,先初始化再使用. 1.永远在使用对象之前先将其初始化. 内置类型: 必须手工完成. 内置类型以外的:使用构造函数完成.确保每一个构造函数都将对象的一 ...

  9. 【Effective C++ 读书笔记】条款04:确定对象使用前已先被初始化

    永远在使用对象之前先将它初始化.对于无任何成员的内置类型,你必须手工完成此事. 至于内置类型以外的任何其他东西,初始化责任落在构造函数身上.规则很简单:确保每一个构造函数都将对象的每一个成员初始化. ...

随机推荐

  1. html、css杂记

    1:浮动 <div style="float: left"> 2:清除浮动,把父div撑起来 <div style="clear:both"& ...

  2. Linq之Linq to XML

    目录 写在前面 系列文章 linq to xml 总结 写在前面 在很多情况下,都可以见到使用xml的影子.例如,在 Web 上,在配置文件.Microsoft Office Word 文件(将wor ...

  3. 怎么样能让自己的虚拟机上网win7 for linux

    我的电脑是win7 32位 虚拟机是linux 我是通过无线连接互联网的 点无线网络连接 右键 点共享 按照我的方式设置 接下来进入 里面会自动的选择这个 如果没有可以自己按照这个设置 接下来进入虚拟 ...

  4. BZOJ 1005: [HNOI2008]明明的烦恼 Purfer序列 大数

    1005: [HNOI2008]明明的烦恼 Time Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://www.lydsy.com/JudgeOnline/ ...

  5. POJ2479 Maximum sum(dp)

    题目链接. 分析: 用 d1[i] 表示左向右从0到i的最大连续和,d2[i] 表示从右向左, 即从n-1到i 的最大连续和. ans = max(ans, d1[i]+d2[i+1]), i=0,1 ...

  6. codevs 1031 质数环

    题目描述 Description 一个大小为N(N<=17)的质数环是由1到N共N个自然数组成的一个数环,数环上每两个相邻的数字之和为质数.如下图是一个大小为6的质数环.为了方便描述,规定数环上 ...

  7. C++里消除Wunused

    编译程序时,有一大堆警告总是不爽的.别人的代码也就忍了,不好去改.自己的可没法忍.看看C++里怎么消除Wunused警告. 先来看下面的程序: #include <iostream> in ...

  8. RabbitMQ 消息队列 入门 第二章(交换类型fanout)

    1.安装完 RabbitMQ 之后,我们可以点击  http://localhost:15672/#/  默认账号:guest  密码: guest  在这上面我们可以查看执行情况.管理连接.管理队列 ...

  9. [转]PHP时区/MySql时区/Linux时区

    本文转自:https://blog.csdn.net/watermelonmk/article/details/82669062 问题背景:手头上有个国外的项目,为了所谓的国际化,得将时区修改至[美国 ...

  10. 文件-- 字节相互转换(word、图片、pdf...)

    方式一: /// <summary> /// word文件转换二进制数据(用于保存数据库) /// </summary> /// <param name="wo ...