Effective C++读书笔记之条款04:确定在使用对象前已将它初始化。

条款4:确定在使用对象前已将它初始化。(Make sure that objects are initialized before they’re used)

1.永远在使用对象之前将它初始化。
1.1 对于内置类型,手动完成初始化。
什么叫做内置类型呢?就是在C++里面的这些基本数据类型:
① 整数 int、short和long
② 字符型 char和wchar_t
③ 布尔型 bool
④ 浮点型 float、double和long double
举例子: int =0;
1.2 除了内置类型之外的类型,由构造函数进行初始化。【确保每个构造函数都将对象的每一个成员进行初始化】
2.区分类成员的赋值和初始化,总是使用成员初值列。
2.1 Eg:

class Test
{
public:
	Test(const int& n);
	~Test();

private:
	int m_nNumber;
};

Test::Test(const int& n)//赋值并非初始化,调用默认构造和赋值运算符
{
	m_nNumber = n;
}
Test::~Test()
{
}
class Test
{
public:
	Test(const int& n);
	~Test();

private:
	int m_nNumber;
};

Test::Test(const int& n)//初始化并非赋值,调用拷贝构造函数,构造函数本体内不必有操作。
: m_nNumber(n)
{
}

Test::~Test()
{
}

2.2 总结。
2.2.1、基于赋值的那个版本首先调用默认构造函数为m_nNumber设初值,然后立刻再对它赋予新值。默认构造函数的一切作为因此被浪费了。成员初始化列表的做法避免了这一问题,因为初值列表中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参,m_nNumber以n为初始值进行拷贝构造。
2.2.2、C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在Test构造函数体内,m_nNumber不是被初始化,而是被赋值。初始化发生时间更早,发生于这些成员的默认构造函数被自动调用之时(比进入Test构造函数本体的时间更早)
2.2.3、对大多数类型而言,比起先调用默认构造函数然后再调用赋值操作符,仅仅调用一次copy构造函数是比较高效的,有时甚至高效的多。

2.3 特殊的。
constreference 就必须需要初值,而不能赋值,必须初始化。
编译错误:
在这里插入图片描述
那就必须使用成员初值列初始化。

class Test
{
public:
Test(int& n, string& str);
~Test();

private:
const int m_nNumber;
string& m_strString;
};

Test::Test(int& n, string& str)
:m_nNumber(n),
m_strString(str)
{
}

Test::~Test()
{
}

3. 中规中矩的成员初值列顺序。
/*
初始化顺序:base classes总是先于derived classes被初始化,类的成员变量总是以其声明顺序被初始化。
即使它们在成员初始化列表中以不同的次序出现(编译器会报出警告),也不会有任何影响(只是报出警告)。
为了避免代码阅读者的疑惑,或者必须一些晦涩错误(两个成员变量的初始化带有次序性,例如初始化数组时需要指定大小.
因此代表大小的那个成员变量必须先有初值),当你在成员初始化列表中列出各个成员时,最好总是以其声明次序为次序。
*/

class Person
{
public:
	Person(string& strName,
		string& strSex,
		int nAge,
		int nID,
		string& strPhoneNum);
	~Person();

private:
	const string m_strName;
	const string m_strSex;
	int m_nAge;
	int m_nID;
	string m_strPhoneNum;
};

Person::Person(string& strName, string& strSex, int nAge, int nID, string& strPhoneNum)
:m_strName(strName),
m_strSex(strSex),
m_nAge(nAge),
m_nID(nID),
m_strPhoneNum(strPhoneNum)
{
}

Person::~Person()
{
}

4. 不同编译单元内定义的non-local static对象的初始化次序。
4.1所谓static对象。
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。static对象包括global对象,定义于namespace作用域内的对象,在classes内、在函数内、以及在file作用域内被声明为static的对象。
static对象又分local static对象(函数内的static对象)和non-local static对象(非函数内的static对象)。
程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
4.2 所谓编译单元。
所谓编译单元是指产出单一目标文件的那些源码。基本上它是单一源码文件加上其所含入的头文件。

现在,我们关心的问题涉及至少两个源码文件,每一个内含至少一个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 {            // from your library’s header file
public:
    ...
    std::size_t numDisks() const; // one of many member functions
    ...
};
extern FileSystem tfs;  // declare object for clients to use
                        // (“tfs” = “the file system” );
                        // definition is in some .cpp file in your library

现在假设某些客户建立了一个class用以处理文件系统内的目录。很自然它们的class会用上tfs对象。

class Directory {       // created by library client
public:
    Directory( params );
    ...
};
Directory::Directory( params )
{
    ...
    std::size_t disks = tfs.numDisks(); // use the tfs object
    ...
}

进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:
Directory tempDir( params ); //directory for temporary files
现在初始化次序的重要性显现出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源码文件建立起来的,它们是定义于不同编译单元内的non-local static对象。如何能够确定tfs会在tempDir之前先被初始化?
4.3 解决:以 local static 替换 non-local static
唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这也就是我们常见的Singleton Mode的一种手法。
这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本;真正的non-local static对象可没这等便宜!

class FileSystem { ... };   // as before
FileSystem& tfs()           // this replaces the tfs object; it could be
{                           // static in the FileSystem class
    static FileSystem fs;   // define and initialize a local static object
    return fs;              // return a reference to it
}
class Directory { ... };        // as before
Directory::Directory( params )  // as before, except references to tfs are
{                               // now to tfs()
    ...
    std::size_t disks = tfs().numDisks();
    ...
}
Directory& tempDir()                // this replaces the tempDir object; it
{                                   // could be static in the Directory class
    static Directory td( params );  // define/initialize local static object
    return td;                      // return reference to it
}

这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。这样的单纯性使它们称为绝佳的inline候选人,尤其如果它们被频繁调用的话。但是从另一个角度看,这些函数“内含static对象”的事实使它们在多线程系统中带有不确定性。任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的“竞速形势”。

运用reference-returning函数防止“初始化次序问题”,前提是其中有着一个对对象而言合理的初始化次序。如果你有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却又受制于B是否已初始化,这时候你就有麻烦了。坦白说你自作自受。只要避开如此病态的境况,此处描述的办法应该可以提供你良好的服务,至少在单线程程序中。

5. 敲黑板,记重点。
5.1 为内置型对象进行手工初始化,因为C++不保证初始化它们。
5.2 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表中列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
5.3 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值