在确保对象在使用前已先被初始化这一条款的编码实践中,作者为我们总结了三条经验,它们分别是:
-------------------------------------------------------------------------------------------------------
ⅰ. 手工初始化内置类型对象
ⅱ. 构造函数最好使用成员初值列,而不要在构造函数内使用赋值操作,其排列次序应和他们在类中声明的次序相同
ⅲ. 用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're used)

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

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

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

随机推荐

  1. HDU Senior's Gun (水题)

    题意: 给n把枪,m个怪兽,每把枪可消灭1怪兽,并获得能量=枪的攻击力-怪兽的防御力.求如何射杀能获得最多能量?(不必杀光) 思路: 用最大攻击力的枪杀防御力最小的怪兽明显可获得最大能量.如果每把枪都 ...

  2. [转] windows7 IIS管理器 在计算机“.”上没有找到WAS服务

    原文地址:windows7 IIS管理器 在计算机"."上没有找到WAS服务作者:云中的风 OS:windows7旗舰版 产生问题原因:运行金蝶K3-HR时客户端提示中间层服务器不 ...

  3. 转载-KMP算法前缀数组优雅实现

    转自:http://www.cnblogs.com/10jschen/archive/2012/08/21/2648451.html 我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见 ...

  4. HDU 3085 Nightmare Ⅱ 双向BFS

    题意:很好理解,然后注意几点,男的可以一秒走三步,也就是三步以内的都可以,鬼可以穿墙,但是人不可以,鬼是一次走两步 分析:我刚开始男女,鬼BFS三遍,然后最后处理答案,严重超时,然后上网看题解,发现是 ...

  5. NOIP2012 同余方程

    1同余方程 题目描述 求关于 x 的同余方程 ax ≡ 1 (mod b)的最小正整数解. 输入输出格式 输入格式: 输入只有一行,包含两个正整数 a, b,用一个空格隔开. 输出格式: 输出只有一行 ...

  6. 2016CCPC 中南地区邀请赛 A 矩阵快速幂

    A A^n=A^(n%2016)%7; #include <iostream> #include <cstdio> #include <cstring> #incl ...

  7. 容斥原理——uva 10325 The Lottery

    首先推荐一篇介绍容斥原理很好的博客http://www.cppblog.com/vici/archive/2011/09/05/155103.html 题意:求1~n中不能被给定m个数中任意一个数整除 ...

  8. JDBC 的基本步骤

    JDBC 的基本步骤: 一.导入mysql-connector-java-x.x.x-bin.jar后: 二.代码 1. 注册驱动(三种方式)2. 创建一个连接对象(三种方式) 3. 创建一个sql语 ...

  9. HDU-4675 GCD of Sequence 数学

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4675 题意:给一个大小为N的数列a[i],然后一个数M以及一个数K,要你求得一个数列b[i],其中b[ ...

  10. Eclipse hangs on loading workbench, eclipse停在启动界面的处理办法

    http://stackoverflow.com/questions/8972034/eclipse-hangs-on-loading-workbench 解答一: In most cases, rm ...