Effective C++读书笔记之三

条款四:确定对象被使用前已先被初始化

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

         int a ;

        在某些语境下x保证被初始化(为0),但在其他语境中却不保证。如果你这么写:

class Point
{
public:
	int x,y ;
};
...
Point p ;

p的成员变量有时候被初始化(为0),有时候不会。

         读取未初始化的值会导致不明确的行为。在某些平台,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机”bits,污染了那些正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令人不愉快的调试过程。

           现在我们终于有了一些规则,描述“对象的初始化动作何时一定发生,何时不一定发生”。不幸的是这些规则很复杂,我认为对记忆力而言是太复杂了些。

           通常如果你使用C part of C++ 而且初始化可能招致运行期成本,那么就不保证发生初始化。一旦进入 non-C parts of C++,规则有些变化。这就很好地解释了为什么array(来自C part of C++)不保证其内容被初始化,而vector(来自STL part of C++)却有此保证。

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

                  int  x = 0 ; // 对int进行手工初始化

             const char*text = "A C-style string";// 对指针进行手工初始化

             double d ;

              std::cin >> d ;// 以读取 input stream 的方式完成初始化。

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

              这个规则很容易奉行,重要的是别混淆了赋值和初始化。考虑一个用来表现通讯薄的class,其构造函数如下:

class PhoneNumber{...};
class ABEntry
{
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 ;
	theAddress	= address ;// 这些都是赋值而非初始化
	thePhones	= phones ;
	numTimesConsulted	= 0 ;
}

               这会导致ABEntry对象带有你期望(你指定)的值,但不是最佳做法。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值。初始化发生时间更早,发生于这些成员的default构造函数被自动调用之时(比进入构造函数本体的时间更早)。但这对numTimesConsulted不为真,因为它属于内置类型,不保证一定在你所看到的那个赋值动作的时间点之前获得初值。

           注:某类如果想成为另一个类的成员变量必须具有默认的构造函数。

                ABEEntry构造函数的一个较佳写法是,使用所谓的member initialization list(成员初值列)替换赋值动作。

ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
	:theName(name),
	 theAddress(address),// 现在这些都是初始化
	 thePhones(phones),
	 numTimesConsulted(0)
{
	// 现在构造函数本体不必有任何动作
}

          这个构造函数和上一个的最终结果相同,但效率较高。基于赋值的那个版本(本例第一个版本)首先调用default构造函数为theName,theAddress和thePhones设初值,然后立刻再立刻对它们赋予新值。default构造函数的一切作为作为因此浪费了。成员初值列的做法(本例的第二个版本)避免了这一问题,因为初值列中针对各个成员变量而设的实参,被拿去作为各成员变量值构造函数的实参。本例中的theName以name为初值进行copy构造,theAddress以address为初值进行copy构造,thePhones以phones为初值进行copy构造。

          对大多数类型而言,比起先调用default构造函数然后再调用copy assignment操作符,单只调用一次copy构造函数是比较高效的,有时甚至高效得多。对于内置型对象如numTimesConsulted,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化。同样的道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列来初始化。同样道理,甚至当你想要default构造一个成员变量,你都可以使用成员初值列,只要指定无物作为初始化实参即可。假设ABEntry有一个无参构造函数,我们可将它实现如下:

ABEntry::ABEntry()

:theName(),// 调用theName的default构造函数

 theAddress(),

 thePhones(),

 numTimesConsulted(0)// 记得将numTimesConsulted显示初始化为0

{

}
          由于编译器会为用户自定义类型值成员变量自动调用default构造函数---如果那些成员变量在成员“成员初值列”中没有被指定初值的话,因而引发某些程序员过度夸张地采用以上写法。那是可以理解的,但请立下一个规则,规定总是在初值列中列出所有成员变量,以免还得记住哪些成员变量可以无需初值。举个例子,由于numTimesConsulted属于内置类型,如果成员初值列遗漏了它,它就没有初值,因而可能开启“不明确行为”的潘多拉盒子。

        有些情况下即使面对的成员变量属于内置类型(那么其初始化于赋值的成本相同),也一定得使用初值列。是的,如果成员变量时const或reference,它们就一定需要初值。为了避免需要记住成员变量何时必须在成员初值列表中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要。且又往往比赋值更高效。

        许多class拥有多个构造函数,每个构造函数有自己的成员初值列。如果这种class存在许多成员变量和/或base classes,多份成员初值列的存在就会导致不受欢迎的重复(在每个构造函数的初值列内都要初始化一次所有的成员变量)和无聊的工作(对程序员而言)。在这种情况下可以合理地在初值列中遗漏那些“赋值表现想初始化一样好”的成员变量,改用他们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有的构造函数调用。这种做法在“成员变量的初值系由文件或数据库读入”时特别有用(如配置文件)。然而,比起经由赋值操作完成的“伪初始化”,通过成员初值列完成的“真正初始化”通常更加可取。
        C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classs被初始化看,而class的成员变量总是以其声明的次序被初始化。回头看看ABEntry,其theName成员永远被最先初始化,然后是theAddress,再来就是thePhones,最后是numTimesConsulted。即使他们在成员初值列中以不同的次序出现(很不幸那时合法的),也不会有任何影响。为避免你或你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序。

         注:上述所谓晦涩错误,指的是两个成员变量的初始化带有次序性。例如初始化array时需要指定大小,因此代表大小的那个成员变量必须先有初值。

         一旦你已经很小心地将“内置型成员变量”明确地加以初始化,而且你也确保你的构造函数运用“成员初值列”初始化base classes和成员变量,那就只剩下唯一的一件事情你需要操心,就是......(⊙o⊙)......深呼吸......“不同编译单元内定义之non-local static对象”的初始化次序。

           让我们一点一点地探钻这一长串词组。

           所谓static对象,其寿命从被构造出来知道程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象,定义域namespace作用域内的对象、在class内、在函数内,以及在file作用域内被声明为static的对象。函数内德static对象被称为local static对象(因为它对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被销毁,也即是它们的析构函数会在main()结束时被自动调用。

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

            现在,我们关心的问题涉及至少两个源码文件,每一个内含有至少一个non-local static对象(也就是说该对象是global或位于namespace作用域内,抑或在class内或file作用域内被声明为static)。真正的问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local对象”的出沙湖次序并无明确定义。

            实例可以帮助我们理解。假设你有一个FileSystem class,它让互联网上的文件看起来好像位于本机。有英语这个class使世界看起来像个单一文件系统,你可能会产生一个特殊对象,位于global或namespace作用域内,象征单一文件系统:class FileSystem //来自你的程序库

{

...

public:

std::size_t numDisks() const ;// 众多成员函数之一

...

} ;

extern FileSystem tfs ;// 预备给客户使用的对象;tfs代表“the file system”

           FileSystem对象绝对不是一个稀松平常的无关痛痒(trivial)的对象,因此你的客户如果在theFileSystem对象构造完成之前就使用它,会得到惨重的灾情。

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

class Directory//由程序库客户端建立

{

public:

Directory(params) ;

...

} ;

Directory::Directory(params)

{

...

std::size_t disks = tfs.numDisks() ;// 使用tfs对象

...

}

          进一步假设,这些客户决定创建一个Directory对象,用来放置临时文件:

        Directory tempDir(params) ;// 为临时文件而做出的目录

        现在,初始化次序的重要性显现出来了:除非tfs在tempDir之前被初始化,否则tempDir的构造函数会用到尚未初始化的tfs。但tfs和tempDir是不同的人在不同的时间于不同的源代码五年间建立起来的,它们是定义于不同编译单元内的non-local对象。如何能够确定tfs会在tempDir之前先被初始化?

          喔,你无法确定。再说一次,C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困哪,非常困难,根本无解。在其最常见形式,也即是多个编译单元内的non-local static对象经由“模板隐式具现化”形成(而后者自己可能也是经由“隐式模板具现化”形成),不但不能决定正确的初始化次序,甚至往往不值得寻找“可决定正确次序”的特殊情况。

             幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些杉树,而不直接指涉这些对象。换句话说,non-local对象被local static对象替换了。Design Patterns迷们想必认出来了,这是Singleton模式的常见实的现手法。

              这个手法的基础在于:C++保证,函数内的local static对象会在“函数被调用期间”“首次遇到该对象之定义式”时被初始化。所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”你就获得了保证,保证你所获得的那个reference将指向一个经历初始化的对象。更棒的是,如果你从未调用non-local static对象 的“仿真函数”,就绝对不会引发构造和析构成本;真正的non-local static对象可美这等便宜!

           以此技术实行于tfs和tempDir身上,结果如下:

class FileSystem{...} ;

FileSystem& tfs()// 这个函数用来替换tfs对象;它在FileSystem class中可能是个static。定义并初始化一个local static对象

{                                                      // 返回一个reference指向上述对象。

static FileSystem fs ;

return fs ;

}

class Directory{...}
Directory::Directory(params)

{

...

std::size_t disks = tfs().numDisks() 

...

}


Directory& tempDir// 这个函数用来替换tempDir对象;他在Dirctory class中可能是个static。定义并初始化local static对象

,返回一个reference指向上述对象。

{

static Directory td ;

return td ;

}

           这么修改之后,这个系统程序的客户完全像以前一样的用它,唯一不同的是他们现在使用tfs()和tempDir()而不再是tfs和tempDir。也即是说他们使用函数返回的“指向static对象”的references,而不再使用static对象自身。

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

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

           既然这样,为避免在对象初始化之前过早地使用它们,你需要做三件事。第一,手工初始化内置型non-member对象。第二,使用成员初值列对付对象的所有成分。最后,在“初始化次序不确定性”(这是对不同编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。

请记住:

           * 为内置型对象进行手工初始化,因为C++不保证初始化它们。

           * 构造函数使用成员初始列,而不要在构造函数本体内使用复制操作。初始列列出的成员吧i按量,其排列次序应该和他们在class中的声明次序相同。

           * 为了在“跨编译单元之初始化次序”问题,请以local static对象non-local替换non-local static 对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《Effective C++ 中文第三版》是针对C++程序设计语言的一本重要的专业书籍,它将C++语言的各种语法特性、设计模式及编程技巧进行了深度分析和系统总结。 本书分为55条编程指南,从C++语言的核心概念(如RAII、异常安全等)到编程技巧的细节(如拷贝控制、继承、模板等)都进行了详细介绍。每个指南都包含了对应的问题、建议和说明,通过实际例子和对比分析,让读者能够更好地理解和掌握相关知识。 相比其他的C++规范书籍,《Effective C++ 中文第三版》更加实用和直观,它的重点在于介绍如何写出正确、高效、健壮的C++代码。同时,书中还对C++11和C++14的新特性进行了简单介绍,为读者扩展了视野,帮助读者更好地应对日益复杂的编程需求。 总的来说,《Effective C++ 中文第三版》是一本适合C++程序员的入门和进阶教材,通过系统性的介绍和实例讲解,能够帮助读者逐步掌握C++语言的精髓和技巧,写出更加高效、健壮和易维护的程序。 ### 回答2: 《Effective C++ 中文第三版》是一本介绍C++编程技巧的经典书籍。该书作者Scott Meyers是一位C++专家,他精心编写了该书的内容,用通俗易懂的语言阐述了C++编程的许多细节问题。通过学习这本书,读者可以更好地理解C++的语言特性,掌握C++编程的技巧和方法,以提高程序的质量和效率。 该书涵盖了37个条款,主要分为四个部分。第一部分介绍了C++语言的基础知识,包括构造函数和析构函数、赋值操作、拷贝构造函数等;第二部分介绍了C++的设计和实现,包括类设计、模板使用和异常处理等;第三部分介绍了C++的继承和多态,包括虚函数、抽象类、多重继承、虚继承等;第四部分介绍了C++的高级语言特性,包括模板元编程、异常安全、性能优化和智能指针等。 通过学习这本书,读者可以获得以下几个方面的收获。首先,掌握C++编程的基本技能和知识,能够写出高质量的、健壮的C++程序;其次,了解C++语言的设计和实现原理,能够更好地理解C++程序的内部机制;最后,学会了高效的C++编程技巧和方法,可以提高程序的性能和效率,避免常见的、容易犯的C++编程错误。 总之,《Effective C++ 中文第三版》是一本非常优秀的C++编程书籍,对于想要成为一名优秀的C++程序员的读者来说,是一本不可多得的好书。 ### 回答3: Effective C++是一本非常经典的C++编程技巧指南,被誉为C++编程者必读的参考书之一。作者Scott Meyers深入浅出的将自己多年的实际经验和对C++各个方面的深入理解融合到了书中,为读者提供了各种实用技巧和解决方案。本书被分成了50个小节,每个小节都介绍一个C++编程中的技巧,如何避免陷阱以及如何让代码更加清晰可读。 Effective C++中文第三版在前两版的基础上做了一些更新和补充,和当前主流的C++版本兼容,增加了对多线程编程方面的内容和对垃圾回收的讲解等等。此外,本书还提供了大量的实际例子和细节解释,让读者能够更好地理解和运用这些技巧。不仅适合初学者,对于已经上手C++编程的程序员也是一本非常有价值的参考书,可以帮助他们更好的掌握C++语言,并写出高效、可维护的代码。 总的来说,Effective C++C++编程界的经典书籍之一,具有极高的实用价值和指导意义。不同阶段的程序员都可以从中获益,提高自己的编程能力。因此,对于想要成为一名优秀的C++程序员的人来说,这本书是绝不能缺少的。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值