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

Item4 确保对象被使用前进行初始化

C++在对象的初始化上是变化无常的,例如看下面的例子:

Int x;

在一些上下文中,x保证会被初始化成0,在其他一些情况下却不能够保证。看下面的例子:

Class Point

{

Int x,y;

};

Point p;

P的数据成员有时候保证能够被初始化(成0),有时候却不能。如果你从不存在未初始化对象的语言中转到c++, 就需要注意了,因为这很重要。

使用未初始化对象的坏处

读取未初始化的值会产生未定义的行为。在一些平台中,仅仅读取未初始化的值就会让你的程序停止。更有可能读入一些半随机的bits,这会污染你的对象,最终导致不可思议的程序行为和很多不愉快的程序调试。

对于内建类型和非内建类型初始化的说明

现在,有一些规则描述了什么时候对象初始化保证能够发生,什么时候不能够保证。不幸运的是,这些规则太复杂了-复杂的不值得我们去记住它。一般来说,如果你使用c++中的c部分,初始化可能会招致运行期成本,因此不保证发生初始化。如果你进入了c++的非C部分,事情就会发生改变。这就解释了为什么数组(来自c++的c语言部分)不保证它的内容被初始化,但是vector会保证(来自c++的STL部分)。

如何保证内建类型进行初始化

处理这种看上去是不确定状态事务的最好方法是在你使用对象之前总是将它们进行初始化。对于内建类型的非成员对象,你需要手动初始化。举个例子:

int x = 0; // manual initialization of an int

const char * text = "A C-style string"; // manual initialization of a pointer (see also Item 3)

double d; // “initialization” by reading from

std::cin >> d; // an input stream

 

如何保证非内建类型进行初始化

对于内置类型之外的其他东西,初始化的责任落在了构造函数身上。规则非常简单:确保所有的构造函数初始化对象的所有东西。

这个规则很容易遵守,但是不要将赋值和初始化搞混,这很重要。考虑一个表示地址簿的类,其构造函数如下:

class PhoneNumber { ... };

class ABEntry { // 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 theAddress;

std::list<PhoneNumber> thePhones;

int numTimesConsulted;

};

ABEntry::ABEntry(const std::string& name, const std::string& address,

const std::list<PhoneNumber>& phones)

{

theName = name; // these are all assignments,

theAddress = address; // not initializations

thePhones = phones;

numTimesConsulted = 0;

}

构造函数中使用初始化列表比赋值更具效率

这将会产生你所需要的ABEntry对象,但这仍然不是最好的方法。C++的规则中规定:对象的数据成员在进入构造函数体之前被初始化。在ABEntry构造函数内部,theName,theAdress和thePhones并没有被初始化,它们是被赋值。初始化发生的更早,在进入ABEntry的构造函数体之前这些数据成员的默认构造函数会被自动调用。但是这并不适用于numTimesConsulted,因为它是内建类型。对于内建类型来说,不能够保证在赋值之前被初始化。

写出ABEntry构造函数的更好的方法是使用成员初始化列表,而不是赋值。

ABEntry::ABEntry(const std::string& name, const std::string& address,

const std::list<PhoneNumber>& phones)

: theName(name),

theAddress(address), // these are now all initializations

thePhones(phones),

numTimesConsulted(0)

{} // the ctor body is now empty

这个构造函数会和上面的构造函数产生同样 的结果。但是它会更有效率。基于赋值的版本首先会调用默认构造函数来初始化theName,theAddress和thePhones,然后迅速的在默认构造出来的成员基础之上再进行赋值。在默认构造函数中进行的所有工作因此被浪费了。成员初始化列表的使用避免了这个问题,因为成员初始化列表中的参数被用作不同数据成员的构造函数的参数。在这种情况下,theName会以name作为参数拷贝构造出来,theAddress会以address作为参数拷贝构造出来,theAddress会以phones为参数拷贝构造出来。对于大多数类型,比起先调用默认构造函数然后调用拷贝赋值运算符,调用一个单一拷贝构造函数是更有效率的,而且有时效率能够大大提高。

对于像numTImeConsulted这样的内建类型来说,初始化和赋值的开销是相同的,但是为了一致性,最好通过初始化列表对所有东西进行初始化。类似的,在你想使用默认构造函数构造数据成员的时候,你仍然可以使用成员初始化列表:初始化参数不要指定任何东西。举个例子,如果ABEntry有一个构造函数不带任何参数,可以像下面这样来实现:

ABEntry::ABEntry()

: theName(), // call theName’s default ctor;

theAddress(), // do the same for theAddress;

thePhones(), // and for thePhones;

numTimesConsulted(0) // but explicitly initialize

{} // numTimesConsulted to zero

 

在构造函数中使用初始化列表更不容易犯错

对于没有在成员初始化列表中列出来的用户自定义类型的数据成员,编译器会自动为其调用默认构造函数,对于这种想法是过多的考虑了。这种做法可以理解,但是如果有一个规则:总是在成员初始化列表中列出所有数据成员。这样我们就不必特地的记住哪些数据成员在被忽略的情况下不会被初始化。因为numTimesConsulted是一个内建类型,如果不将其放入成员初始化列表,它就不会被初始化,也就会打开未定义行为的大门。

 

有时候成员初始化列表必须被使用,甚至对于内建类型也是这样。举个例子,const数据成员或者引用数据成员必须被初始化而不能够被赋值(Item5)。为了避免需要记住什么时候数据成员必须被初始化什么时候是可选的,最简单的选择是总是使用初始化列表。有时候这样做是需要的,它比赋值更加有效率。

例外,什么时候用赋值会更好

许多类有多个构造函数,每个构造函数有自己的初始化列表。如果这些类有许多数据成员和(或者)基类,多个初始化列表的存在就会引入令人不愉快的重复(不同的构造函数初始化列表重复),程序员也会厌倦。在这种情况下,在成员初始化列表中省略那些赋值和真正进行初始化有相同效率的成员就是合理的,将这些赋值移动到一个单独的函数(private)中供所有的构造函数调用。如果数据成员的初始化值是从文件中或查找数据库得来的,这种方法特别有帮助。总之,真正的数据成员初始化(通过成员初始化列表)比“假的”通过赋值进行的初始化要更好。

对于类对象成员初始化顺序的说明

C++中一个不变的地方是对象的数据成员被初始化的顺序。这种顺序总是会相同的:基类在派生类初始化之前进行初始化(Item12),在类内部,数据成员根据其在类中声明的顺序进行初始化。举个例子,在ABEntry中,theName首先被初始化,theSecond其次,thePhones第三个被初始化,numTimesConsulted最后被初始化,即使这些数据成员在初始化列表中被列出的顺序不同(很不幸这是合法的),初始化顺序也是按照声明顺序。为了防止读代码的人产生迷惑,同时为了防止一些模糊不清的错误,最好成员初始化列表中列出的数据成员顺序和声明顺序一致。

对于non-local静态对象初始化顺序的说明

  • 问题描述

一旦你很小心的对内建类型的成员显示的进行初始化,并且你能够确保在构造函数中使用初始化列表对基类和数据成员进行初始化,那么只剩下一件你需要担心的事情。这件事情是,在不同编译单元中定义的非本地静态对象的初始化顺序。

我们一点一点的分析这句话。

 

静态对象的生存时间会从对象被构建开始直至程序结束。基于栈和堆的对象生存周期不在此列。在此列中的对象类型包括全局对象,命名空间范围内定义的对象,类内部的静态对象,函数内部声明的静态对象,文件范围内声明的静态对象。函数内部的静态对象被叫做本地静态对象(相对于函数来说是local的)其他类型的静态对象则是非本地的(non-local)对象。静态对象在程序退出时被销毁,例如:main函数执行完成时会调用析构函数。

 

一个编译单元是能够产生单一Obj文件的源码。基本上来说就是一个单一的源文件,加上所有#include进来的文件。

我们关心的问题涉及到至少两个单独编译的源文件,每个源文件至少包含一个非本地(non-local)静态对象(例如:命名空间范围内的全局对象或者类或文件范围内的static对象)。实际的问题是:如果一个编译单元中的非本地(non-local)静态对象的初始化使用了不同编译单元中的非本地静态对象,它使用的对象有可能没有被初始化,因为在不同编译单元中定义的非本地静态对象的初始化相对顺序是未定义的。

看下面例子。假设你有一个FileSystem类,它让互联网上文件看起来像在本地。既然你的类使世界看起来像一个单一的文件系统,你会创建一个全局的或者命名空间范围内的特殊对象来表示这个单一的文件系统。

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

一个FileSystem是很重要的对象,因此在初始化tfs对象之前使用tfs会是灾难性的。

现在假设在一个文件系统中客户端为文件夹创建了一个类。很自然的,这个类会使用到tfs对象:

class Directory { // created by library client

public:

Directory( params );

...

};

Directory::Directory( params )

{

...

std::size_t disks = tfs.numDisks(); // use the tfs object

...

}

进一步假设客户端决定为临时文件创建单一的文件夹对象:

Directory tempDir( params ); // directory for temporary files

 

现在初始化顺序的重要性就很明显了:除非在tempDIr初始化之前对tfs进行初始化,否则tempDir的构造函数会尝试使用未初始化的tfs.因为tfs和tempDir是由不同的人在不同的时间不同的源文件中被创建的,它们是被定义在不同编译单元中的非本地静态对象。怎么才能够保证tfs在tempDIr之前被初始化呢?

 

你不能够保证,因为在不同编译单元中定义的非本地静态对象的初始化相对顺序是未定义的。这是有原因的,决定非本地静态对象的“合适的”初始化顺序是很难的。其最常见的形式,在多个编译单元内存在着通过隐式模板具现化产生的非本地静态对象,在这种情况下,不仅不能够决定初始化的正确顺序,并且为可能能够决定初始化的正确顺序寻找特定的cases也是不值得的。

  • 问题如何解决?

 

幸运的是,一个小的设计改动能够消除整个问题。所有需要做的就是将每个非本地静态对象移动到一个函数中,并且在函数中将其声明成static.这些函数返回其包含的对象的引用。客户端就可以调用函数而不是直接引用对象了。换句话说,非本地静态对象被本地静态对象替换掉了。(设计模式的爱好者会发现这是单例模式的一般实现。)

C++保证在函数调用时首次碰到函数内定义的本地静态对象,这个对象会被初始化。因此,如果你通过调用以本地静态对象作为返回值的函数来代替直接访问非本地静态对象,就能够保证返回的对象引用指向的是被初始化的对象。还有一个好处,如果你从来没有调用替代非本地对象的这个函数,永远不会有构造函数和析构函数的开销,这个对于非本地对象来说就不会生效了。

 

将上面的技术应该在tfs和tempDir上:

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

}

 

这个修改过的系统程序客户端和修改之前是一样的,只不过现在使用tfs()和tempDir()而不是tfs和tempDir.也就是,现在使用以指向对象引用作为返回值的函数来替代使用对象本身。

  • 解决方法的局限性和使用场景 

这个规则描述的引用-返回函数通常是简单的:在第一行定义并且初始化一个本地对象,在第二行返回。这种简单性使得其成为内联函数的绝对候选人,特别是在它们被频繁调用的情况下(Item30)。另外,这些函数包含静态对象的事实使得其在多线程系统中使用会出现问题。再说一次,任何类型的non-const静态对象(不管是local的还是non-local的),在多线程环境下等待某事发生都会出现问题。解决这个麻烦的一个方法是在程序的单线程启动阶段手动触发所有引用-返回函数。这种方法可以消除初始化相关的竞速形式(race conditions)。

当然,使用引用-返回函数的方法可以防止初始化顺序问题,前提是需要进行初始化的对象首先要有一个合理的初始化顺序。如果在一个系统中对象A必须在对象B初始化之前进行初始化,而对象A的初始化依赖于对象B,这就会出现问题了。如果你能够避开这种病态的场景,这里描述的方法能够很好的为你服务,至少是在单线程应用中。 

总结

为了避免在对象初始化之前被使用,你需要做三件事情。

第一,  手动初始化内建对象。

第二,  使用成员初始化列表初始化一个对象的所有部分。

第三,  对初始化顺序不确定的场景进行重新设计。

转载请注明出处

时间: 2024-07-28 14:27:40

读书笔记 effective c++ Item4 确保对象被使用前进行初始化的相关文章

Effective Java 读书笔记(2创建和销毁对象)

第一章是引言,所以这里不做笔记,总结一下书中第一章的主要内容是向我们解释了这本书所做的事情:指导Java程序员如何编写出清晰.正确.可用.健壮.灵活和可维护的程序. 2.1考虑用静态工厂方法代替构造器 静态工厂方法与构造器相比有四大优势: (1)静态工厂方法有名称,具有适当名称的静态工厂方法易于使用.易于阅读: (2)不必每次在调用它们的时候都创建一个新的对象: (3)可以返回原返回类型的任何子类型的对象: (4)在创建参数化类型实例的时候,它们使代码变得更加简洁. 同时静态工厂方法也有两大缺点

Effective C++读书笔记之十三:以对象管理资源

Item 13:Use objects to manage resources 假设我们使用一个用来塑膜投资行为的程序库,其中各式各样的投资类型继承自一个root class: class Investment { ... };  //"投资类型"继承体系中的root class 进一步假设,这个程序系通过一个工厂函数(工厂函数会"返回一个base class指针,指向新生成的derived class 对象),供应我们某特定的Investment对象: Investment

条款47: 确保非局部静态对象在使用前被初始化

class FileSystem { ... }; // 这个类在你 // 的程序库中 FileSystem theFileSystem; // 程序库用户 // 和这个对象交互 ////////////////////////////////////////////////////////// class Directory { // 由程序库的用户创建 public: Directory(); ... }; Directory::Directory() { 通过调用theFileSystem

【Head First Java 读书笔记】(四)对象的行为

状态影响行为,行为影响状态 对象有状态和行为 类所描述的是对象知道什么和执行什么. 同一类型的每个对象能够有不同的方法行为吗? 任一类的每个实例都带有相同的方法,但是方法可以根据实例变量的值来表现不同的行为. 比如Song类有title实例变量,不同的实例都可以调用play()方法,但会根据title播放不同的歌曲. 方法的参数,你可以传值给方法 方法会运用形参,调用的一方会传入实参. 实参是传给方法的值,当它传入方法后就成了形参.参数跟局部变量一样,它有类型与名称,可以在方法内运行. 从方法中

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

一.为内置类型对象进行手工初始化,因为C++不保证初始化它们. 二.对象初始化数据成员是在进入构造函数用户编写代码前完成,要想对数据成员指定初始化值,那就必须使用初始化列表. 1 class A 2 { 3 public: 4 A(const string &str) 5 { 6 m_str = str; //m_str 的初始化是在进入构造函数中用户自定义编写代码前完成的,所以这里的m_str = str,执行的不是初始化,而是赋值 7 } 8 private: 9 string m_str;

effective java读书笔记1——创建和销毁对象

今天刚开始读effective java,中文版的读起来很拗口,但感觉收获很多. 另外,这本书的内容是针对Java 1.5和1.6的. 在这里整理一下第2章:创建和销毁对象 的内容. 第一条:考虑用静态工厂方法代替构造器 这一条针对的情景是要获得类的实例时.一般说来,想要获得类的实例,都是通过构造函数(书里叫做构造器). 最常见的构造函数是这样的,没有返回参数,名字和类名相同. public class A{ public A(int a){ //构造函数内容 ... } } 而所谓的静态工厂,

读书笔记 effective c++ Item 32 确保public继承建立“is-a”模型

1. 何为public继承的”is-a”关系 在C++面向对象准则中最重要的准则是:public继承意味着“is-a”.记住这个准则. 如果你实现一个类D(derived)public继承自类B(base),你在告诉c++编译器(也在告诉代码阅读者),每个类型D的对象也是一个类型B的对象,反过来说是不对的.你正在诉说B比D表示了一个更为一般的概念,而D比B表现了一个更为特殊的概念.你在主张:任何可以使用类型B的地方,也能使用类型D,因为每个类型D的对象都是类型B的对象:反过来却不对,也就是可以使

Effective Java 读书笔记之二 对于所有对象都通用的方法

尽管Object是一个具体的类,但设计它主要是为了扩展.它的所有非final方法都有明确的通用约定.任何一个类在override时,必须遵守这些通用约定. 一.覆盖equals时请遵守通用的约定 1.Object中默认的equals方法约定是:类的每个实例都只与它自身相等.当类有自己特有的“逻辑相等”的概念时,就应该覆盖equals方法. 2.Timestamp对Date进行了扩展,Timestamp的equals实现确实违反了对称性.如果Timestamp和Date混合一起使用,可能导致不正确

读书笔记 effective c++ Item 13 用对象来管理资源

1.不要手动释放从函数返回的堆资源 假设你正在处理一个模拟Investment的程序库,不同的Investmetn类型从Investment基类继承而来, 1 class Investment { ... }; // root class of hierarchy of 2 3 // investment types 进一步假设这个程序库通过一个工厂函数(Item 7)来给我们提供特定Investment对象: 1 Investment* createInvestment(); // retur