高质量C++C编程指南(下).pdf

上传人:无*** 文档编号:90912992 上传时间:2023-05-18 格式:PDF 页数:46 大小:5.35MB
返回 下载 相关 举报
高质量C++C编程指南(下).pdf_第1页
第1页 / 共46页
高质量C++C编程指南(下).pdf_第2页
第2页 / 共46页
点击查看更多>>
资源描述

《高质量C++C编程指南(下).pdf》由会员分享,可在线阅读,更多相关《高质量C++C编程指南(下).pdf(46页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、第8章C+函数的高级特性对 比 于 C语言的函数,C+增加了重载(o v e r l o a d e d),内 联(i n l i n e)-.c o n s t 和 v i r t u a l四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,c o n s t 与v i r t u a l 机制仅用于类的成员函数。重载和内联肯定有其好处才会被C+语言采纳,但是不可以当成免费的午餐而滥用。本章将探究重载和内联的优点与局限性,说明什么情况下应该采用、不该采用以及要警惕错用。8.1 函数重载的概念8.1.1 重载的起源自然语言中,一个词可以有许多不同的含义,即该词被重载了。人们可

2、以通过上下文来判断该词到底是哪种含义。“词的重载”可以使语言更加简练。例 如“吃饭”的含义十分广泛,人们没有必要每次非得说清楚具体吃什么不可。别迂腐得象孔已己,说茴香豆的茴字有四种写法。在 C+程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这 是 C+语言采用重载机制的一个理由。例如示例 8-1-1 中的函数E a t B e e f,E a t F i s h,E a t C h i c k e n 可以用同一个函数名E a t 表示,用不同类型的参数加以区别。示 例8-1-1重载函数Eatv o i d E a t B e e f (

3、);/可以改为v o i d E a t (B e e f );v o i d E a t F i s h (,);/可以改为v o i d E a t (F i s h ,);v o i d E a t C h i c k e n ();/可以改为v o i d E a t (C h i c k e n );C+语言采用重载机制的另一个理由是:类的构造函数需要重载机制。因 为 C+规定构造函数与类同名(请参见第9章),构造函数只能有一个名字。如果想用几种不同的方法创建对象该怎么办?别无选择,只能用重载机制来实现。所以类可以有多个同名的构造函数。8.1.2重载是如何实现的?儿个同名的重我函数仍

4、然是不同的函数,它们是如何区分的呢?我们自然想到函数接口的两个要素:参数与返回值。如果同名函数的参数不同(包括类型、顺序不同),那么容易区别出它们是不同的函数。局质量C+/C编程指南,v 1.0如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。例如:voi d F u n c ti on(voi d);i n t F u n c ti on (voi d);上述两个函数,第一个没有返回值,第二个的返回值是i n t类型。如果这样调用函数:i n t x=F u n c ti on ();则可以判断出F u n c ti on是第二个函数。问题是在C+/C程序中,我们可以忽略函数的返回

5、值。在这种情况下,编译器和程序员都不知道哪个F u n c ti on函数被调用。所以只能靠参数而不能靠返回值类型的不同来区分重我函数。编译器根据参数为每个重载函数产生不同的内部标识符。例如编译器为示例8 7-1中的三个E a t函数产生象e a t b e e f、e a t f i sh、e a t c h i c ke n之类的内部标识符(不同的编译器可能产生不同风格的内部标识符)。如 果C+程序要调用已经被编译后的C函数,该怎么办?假设某个C函数的声明如下:voi d f oo(i n t x,i n t y);该 函 数 被C编译器编译后在库中的名字为一f oo,而C+编译器则会产生

6、像一f oo_ i n t_ i n t之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C+程序不能直 接 调 用C函数。C+提供了一个C连接交换指定符号e xte rn “C”来解决这个问题。例如:e xte rn C(voi d f oo(i n t x,i n t y);/其它函数)或者写成e xte rn C(ti i n c lu d e my h e a d e r.h-/其 它C头文件这 就 告 诉C+编译译器,函 数f oo是 个C连接,应该到库中找名字一f。而不是找_ f oo_ i n t_ i n to C+编译器开发商已经对C标准库的头文件作了 e xt

7、e rn “C”处理,所以我们可以用#i n c lu d e直接引用这些头文件。注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。例如:voi d P ri n t();/全局函数c la ss Avoi d P ri n t();成员函数2001Page 2 of 46局质量C+/C编程指南,v 1.0)不 论 两 个 P r i n t 函数的参数是否不同,如果类的某个成员函数要调用全局函数P ri n t,为了与成员函数P ri n t区别,全局函数被调用时应加:标志。如:P ri n t();/表 示 P ri n t是全局函数而非成

8、员函数8.1.3当心隐式类型转换导致重载函数产生二义性示 例 8-1-3 中,第 一 个 ou tpu t函数的参数是i n t类 型,第 二 个 ou tpu t函数的参数是 f loa t类型。由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语 句。u tpu t(0.5)将产生编译错误,因为编译器不知道该将0.5转换成i n t还 是 f loa t类型的参数。隐式类型转换在很多地方可以简化程序的书写,但是也可能留下隐患。#i n cl u d e v o i d o u t p u t (i n t x);/函数声明v o i d o u t p u t

9、(f l o a t x);/函数声明v o i d o u t p u t(i n t x)co u t o u t p u t i n t x e n d l ;)v o i d o u t p u t(f l o a t x)(co u t o u t p u t f l o a t x cl a s s B a s e(p u bl i c:v o i d f(i n t x)co u t B a s e:f(i n t)x e n d l;v o i d f(f l o a t x)co u t B a s e:f(f l o a t)x e n d l;v i r t u a l

10、v o i d g(v o i d)co u t B a s e:g(v o i d)“e n d l;);cl a s s D e r i v e d :p u bl i c B a s ep u bl i c:v i r t u a l v o i d g(v o i d)co u t Z/D e r i v e d:g(v o i d)f (42);/B a s e:f (i n t)422001Page 4 of 46局质量C+/C编程指南,v 1.0示 例 8-2-1成员函数的重载和覆盖p b-f(3.1 4f);/B a s e:f(f l o a t)3.1 4p b-g();)

11、/D e r i v e d:g (v o i d)8.2.2 令人迷惑的隐藏规则本来仅仅区别重载与覆盖并不算困难,但 是 C+的隐藏规则使问题复杂性陡然增加。这 里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不 论 有 无 v i r t u a l关键字,基类的函数将被隐藏(注意别与重载混淆)。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有v i r t u a l关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。示例程序8-2-2(a)中:(1)函数 D e r i v e d:f (

12、f l o a t)覆盖了 B a s e:f (f l o a t)。(2)函数 D e r i v e d:g(i n t)隐藏了 B a s e:g(f l o a t),而不是重载。(3)函数 D e r i v e d:h(f l o a t)隐藏了 B a s e:h(f l o a t),而不是覆盖。#i n cl u d e cl a s s B a s ep u bl i c:v i r t u a l v o i d f(f l o a t x)co u t B a s e:f(f l o a t)x e n d l;)v o i d g(f l o a t x)co u

13、t B a s e:g(f l o a t)x e n d l;)v o i d h (f l o a t x)co u t B a s e:h(f l o a t)x e n d l;);cl a s s D e r i v e d :p u bl i c B a s e(p u bl i c:v i r t u a l v o i d f(f l o a t x)co u t *D e r i v e d:f(f l o a t)x e n d l;v o i d g(i n t x)co u t D e r i v e d:g(i n t)x e n d l;v o i d h(f l

14、o a t x)co u t D e r i v e d:h(f l o a t)x f(3.1 4f);/D e r i v e d:f(f l o a t)3.1 4p d-f(3.1 4f);/D e r i v e d:f(f l o a t)3.1 4/B a d :be h a v i o r d e p e n d s o n t y p e o f t h e p o i n t e rp b-g (3.1 4f);/B a s e:g(f l o a t)3.1 4p d-g(3.1 4f);/D e r i v e d:g(i n t)3(s u r p r i s e!)

15、/B a d :be h a v i o r d e p e n d s o n t y p e o f t h e p o i n t e rp b-h(3.1 4f);/B a s e:h(f l o a t)3.1 4(s u r p r i s e!)p d-h(3.1 4f);/D e r i v e d:h(f l o a t)3.1 4示例8-2-2(b)重载、覆盖和隐藏的比较8.2.3 摆脱隐藏隐藏规则引起了不少麻烦。示 例8-2-3程序中,语 句p d-f(1 0)的本意是想调用函数 B a s e:f (i n t),但是 B a s e:f (i n t)不幸被 D e

16、r i v e d:f (ch a r *)隐藏了。由于数字 1 0不能被隐式地转化为字符串,所以在编译时出错。c l a s s B a s e(p u b l i c:v o i d f (i n t x);;c l a s s D e r i v e d :p u b l i c B a s ep u b l i c:v o i d f(c h a r *s t r);;v o i d T e s t(v o i d)(D e r i v e d *p d =n e w D e r i v e d;p d-f(1 0);/e r r o r示 例8-2-3由于隐藏而导致错误2001Pag

17、e 6 of 46局质量C+/C编程指南,v 1.0从示例8-2-3看来,隐藏规则似乎很愚蠢。但是隐藏规则至少有两个存在的理由:写 语 句p d-f(1 0)的人可能真的想调用D e r i v e d:f(c h a r *)函数,只是他误将参数写错了。有了隐藏规则,编译器就可以明确指出错误,这未必不是好事。否则,编译器会静悄悄地将错就错,程序员将很难发现这个错误,流卜祸根。假 如 类D e r i v e d有多个基类(多重继承),有时搞不清楚哪些基类定义了函数f。如果没有隐藏规则,那 么p d-f (1 0)可能会调用一个出乎意料的基类函数f。尽管隐藏规则看起来不怎么有道理,但它的确能消

18、灭这些意外。示 例8-2-3中,如 果 语 句p d-f (1 0)一 定 要 调 用 函 数B a s e:f (i n t),那么将类D e r i v e d修改为如下即可。c l a s s D e r i v e d :p u b l i c B a s e(p u b l i c:v o i d f(c h a r *s t r);v o i d f(i n t x)B a s e:f(x););8.3参数的缺省值有一些参数的值在每次函数调用忖都相同,书写这样的语句会使人厌烦。C+语言采用参数的缺省值使书写变得简洁(在编译时,缺省值由编译器自动插入)。参数缺省值的使用规则:【规 则

19、8-3-1 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。例如:v o i d F o o(i n t x=0,i n t y=0);/正确,缺省值出现在函数的声明中v o i d F o o(i n t x=0,i n t y=0)/错误,缺省值出现在函数的定义体中)为什么会这样?我想是有两个原因:一是函数的实现(定义)本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体中。二是参数的缺省值可能会改动,显然修改函数的声明比修改函数的定义要方便。【规 则8-3-2 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。正确的示例如卜:v o

20、i d F o o (i n t x,i n t y=0,i n t z=0);2001Page 7 of 46局质量C+/C编程指南,v 1.0错误的示例如下:v o i d F o o(i n t x=0,i n t y,i n t z=0);要注意,使用参数的缺省值并没有赋予函数新的功能,仅仅是使书写变得简洁一些。它可能会提高函数的易用性,但是也可能会降低函数的可理解性。所以我们只能适当地使用参数的缺省值,要防止使用不当产生负面效果。示 例 8-3-2 中,不合理地使用参数的缺省值将导致重载函数o u t p u t 产生二义性。#i n c l u d e v o i d o u t

21、p u t(i n t x);v o i d o u t p u t(i n t x,f l o a t y=0.0);v o i d o u t p u t(i n t x)(c o u t o u t p u t i n t X x e n d lv o i d o u t p u t(i n t x,f l o a t y)(c o u t o u t p u t i n t 7 7 x a n d f l o a t y 只能重载为成员函数+=-=/=*=&=1=%=(b)?(a):(b)语句r e s u l t =M A X(i,j)+2 ;将被预处理器解释为r e s u l t

22、 =(i)(j)?(i):(j)+2 ;由 于 运 算 符 +比 运 算 符 :的优先级高,所以上述语句并不等价于期望的r e s u l t =(i)(j)?(i):(j)+2 ;如果把宏代码改写为#d e f i n e M A X(a,b)(a)(b)?(a):(b)则可以解决由优先级引起的错误。但是即使使用修改后的宏代码也不是万无一失的,例如语句r e s u l t =M A X(i+,j);将被预处理器解释为r e s u l t =(i+)(j)?(i+):(j);对 于C+而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。让我们看看C+的“函数内联”是如何工作的。对于任

23、何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。假如内联函数是成员函数,对 象 的 地 址(t h i s)会被放在合适的地方,这也是预处理器办不到的。C+语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作

24、类的数据成员。所 以 在C+程序中,应该用内联函数取代所有宏代码,“断 言as s er t”恐怕是唯一的例外。as s er t是 仅 在D eb u g版本起作用的宏,它 用 于 检 查“不应该”发生的情况。为了不在程序的D eb u g版 本 和R el eas e版本引起差别,as s er t不应该产生任何副作用。如 果as s er t是函数,由于函数调用会引起内存、代码的变动,那么将导致D eb u g版 本 与R el eas e版本存在差异。所 以as s er t不是函数,而是宏。(参 见6.5节“使用断言”)8.5.2内联函数的编程风格关 键 字i nl i ne必须与函

25、数定义体放在一起才能使函数成为内联,仅 将i nl i ne放在函数声明前面不起任何作用。如下风格的函数F oo不能成为内联函数:i nl i ne v oi d F oo(i nt x,i nt y);/i ni i ne 仅与函数声明放在起2001Page 10 of 46局质量C+/C编程指南,v 1.0v oi d F oo(i nt x,i nt y)而如卜风格的函数F oo则成为内联函数:v oi d F oo(i nt x,i nt y);i nl i ne v oi d F oo(i nt x,i nt y)/i nl i ne 与函数定义体放在一起()所以说,i nl i n

26、e是 一 种“用于实现的关键字”,而 不 是 一 种“用于声明的关键字”。-般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了 i nl i ne关键字,但 我 认 为i nl i ne不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C+/C程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。定义在类声明之中的成员函数将自动地成为内联函数,例如cl as s A(pu b l i c:v oi d F oo(i nt x,i nt y)/自动地成为内联函数)将成员函数的定

27、义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程风格,上例应该改成:/头文件cl as s A(pu b l i c:v oi d F oo(i nt x,i nt y);)/定义文件i nl i ne v oi d A:F oo(i nt x,i nt y)(8.5.3慎用内联内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还 用 得 着“内联”这个关键字吗?内 联 是 以 代 码 膨 胀(复 制)为 代 价,仅仅省去了函数调用的开销,从而提高函数的2001Page 11 of 46局质量C+/C编程指南,v 1.0执行效率。如果执行

28、函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了

29、i n li n e不应该出现在函数的声明中)。8.6 一些心得体会C+语言中的重载、内联、缺省参数、隐式转换等机制展现了很多优点,但是这些优点的背后都隐藏着一些隐患。正圳人们的饮食,少食和暴食都不可取,应当恰到好处。我们要辨证地看待C+的新机制,应该恰如其分地使用它们。虽然这会使我们编程时多费一些心思,少了一些痛快,但这才是编程的艺术。2001Page 12 of 46局质量C+/C编程指南,v 1.0第 9 章 类 的 构 造 函 数、析构函数与赋值函数构造函数、析构函数与赋值函数是每个类最基本的函数。它们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。每个

30、类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。对 于 任 意 一 个 类A,如果不想编写上述函数,C+编译器将自动为A产生四个缺省的函数,如A(v o i d);/缺省的无参数构造函数A(co n s t A&a);/缺省的拷贝构造函数 A(v o i d);缺省的析构函数A&o p e r a t e =(co n s t A&a);/缺省的赋值函数这不禁让人疑惑,既然能自动生成函数,为什么还要程序员编写?原因如下:(1)如 果 使 用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C+发 明

31、人S t r o u s t r u p的好心好意白费了。(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均 采 用“位拷贝”而 非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。对于那些没有吃够苦头的C+程序员,如果他说编写构造函数、析构函数与赋值函数很容易,可以不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。本 章 以 类S t r i n g的设计与实现为例,深入阐述被很多教科书忽视了的道理。S t r i n g的结构如下:cl a s s S t r i n g(p u b l i c:S t r i n g (co n s t ch a r *s t r =

32、N U L L);/普通构造函数S t r i n g(co n s t S t r i n g&o t h e r);/拷贝构造函数 S t r i n g(v o i d);/析构函数S t r i n g&o p e r a t e =(co n s t S t r i n g&o t h e r);/赋值函数p r i v a t e:ch a r *m _ d a t a;/用于保存字符串);9.1 构造函数与析构函数的起源作 为 比C更先进的语言,C+提供了更好的机制来增强程序的安全性。C+编译器具有严格的类型安全检查功能,它几乎能找出程序中所有的语法问题,这的确帮了程序2001P

33、age 13 of 46局质量C+/C编程指南,v 1.0员的大忙。但是程序通过了编译检查并不表示错误已经不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。根据经验,不少难以察觉的程序错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。S t r o u s t r u p 在设计C+语言时充分考虑了这个问题并很好地予以解决:把 对 象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清

34、除工作。构造函数与析构函数的名字不能随便起,必须让编译器认得出才可以被自动执行。S t r o u s t r u p 的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就 加 前 缀 以 示 区 别。除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为v o i d 的函数不同。构造函数与析构函数的使命非常明确,就象出生与死亡,光溜溜地来光溜溜地去。如果它们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(以上典故参考了文献 Ee k e l,p 5 5-p 5 6 )9.2 构造函数的初始化表

35、构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却 在 函 数 体 )之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。构造函数初始化表的使用规则:如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。例如c l a s s AI.A(i n t x);/A 的构造函数);c l a s s B :p u b l i c A(.B(i n t x,i n t y);/B 的构造函数);B:B(i nt x,i nt y):A(x)/在初始化表里调用A的构造函数)类 的 c onst 常量只能在初始化表里被初始化,因为它不能

36、在函数体内用赋值的方式来 初 始 化(参 见 5.4节)。2001Page 14 of 46局质量C+/C编程指南,v 1.0类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如c lass A.A (v oi d);/无参数构造函数A (c onst A&ot h er);/拷贝构造函数A&op erat e=(c onst A&ot h er);/赋值函数);c lass B(p u bli c:B(c onst A&a);/B的构造函数p ri v at e:A m_a;/成员对象

37、);示 例9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。示 例9-2 (b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但 实 际 上B的构造函数干了两件事:先暗地里创建m_a对象(调用了 A的无参数构造函数),再调用类A的赋值函数,将 参 数a赋给示 例 9-2(a)成员对象在初始化表中被初始化 示例9-2(b)成员对象在函数体内被初始化B:B(c onst A&a)B:B(c onst A&a):m_a(a)(m_a=a;)对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但

38、后者的程序版式似乎更清晰些。若 类F的声明如下:c lass F(p u bli c:F(i nt x,i nt y);/构造函数p ri v at e:i nt m_x,m_y;i nt m_i,m_j;2001Page 15 of 46局质量C+/C编程指南,v 1.0)示 例9-2(c)中F的构造函数采用了第一种初始化方式,示 例9-2(d)中F的构造函数采用了第二种初始化方式。F:F(i nt x,i nt y)F:F(i nt x,i nt y):m_x(x),m_y(y)(m_x =x;m_i =0;m_y =y;m J =0;m_i =0;)m_j=0;示例9-2(c)数据成员在

39、初始化表中被初始化 示例9-2(d)数据成员在函数体内被初始化9.3 构造和析构的次序构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。一个有趣的现象是,成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。Eckel,p260-261J9.4 示例:类 String的构造函数

40、与析构函数/S t ri ng的普通构造函数String:S t ri ng(c onst c h ar*st r)(i f(st r=N U L L)(m_dat a=new c h ar 1 ;*m_dat a=O ;)else(i nt lengt h =st rlen(st r);m_dat a=new c h ar lengt h+1 ;st rc p y(mdat a,st r);2001Page 16 of 46局质量C+/C编程指南,v 1.0/S t ri ng的析构函数S t ri ng:S t ri ng(v oi d),idelet e m_dat a;/由于m_dat

41、 a是内部数据类型,也 可 以 写 成delet e m_dat a;9.5 不要轻视拷贝构造函数与赋值函数由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:本章开头讲过,如果不主动编写拷贝构造函数和赋值函数,编 译 器 将 以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。以 类Str i n g的两个对象a,b为例,假 设a.m _d a ta的 内 容 为“h e l l o”,b.m d a ta 的 内 容 为 w o r l d。现 将a赋 给b,缺省赋值函数的“位

42、拷贝”意味着执行b.m _d a ta =a.m _d a ta 这将造成三个错 误:一 是b.m d a ta原有的内存没被释放,造成内存泄露;二是b.m _d a ta和a.m _d a ta指向同一块内存,a或b任何方变动都会影响另一方;三是在对象被析构时,m d a ta被释放了两次。拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?Str i n g a (h e l l o );Str i n g b (w

43、o r l d );Str i n g c =a;/调用了拷贝构造函数,最 好 写 成c (a);c =b;/调用了赋值函数本例中第三个语句的风格较差,宜改写成Str i n g c(a)以区别于第四个语句。9.6 示例:类 String的拷贝构造函数与赋值函数/拷贝构造函数Str i n g:Str i n g(c o n s t Str i n g&o th e r)2001Page 17 of 46局质量C+/C编程指南,v 1.0/允许操作o th e r的私有成员m _d a tai n t l e n g th =s tr l e n(o th e r.m _d a ta);m d

44、 a ta =n e w c h a r l e n g th+1 ;s tr c p y(m d a ta,o th e r,m d a ta);/赋值函数Str i n g&Str i n g:o p e r a te =(c o n s t Str i n g&o th e r)(/(1)检查自赋值i f(th i s =f to th e r)r e tu r n *th i s;/(2)释放原有的内存资源d e l e te 口 m _d a ta;/(3)分配新的内存资源,并复制内容i n t l e n g th =s tr l e n(o th e r,m d a ta);m

45、_d a ta =n e w c h a r l e n g th+1 ;s tr c p y(m _d a ta,o th e r.m d a ta);/(4)返回本对象的引用r e tu r n *th i s;)类Str i n g拷贝构造函数与普通构造函数(参见9.4节)的 区 别 是:在函数入口处无需 与NULL进行比较,这 是 因 为“引用”不可能是NULL,而“指针”可以为NULL。类Str i n g的赋值函数比构造函数复杂得多,分四步实现:(1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出a二a这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如/

46、内容自赋值/地址自赋值b=a;b=&a;c=b;a =*b;a =c;也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”2001Page 18 of 46局质量C+/C编程指南,v 1.0他真的说错了。看看第二步的del et e,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的i f语句i f(t h i s =feo t h er)错写成为i f(*t h i s =o t h er)(2)第二步,用del et e释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。(3)第三步,分配新的内

47、存资源,并复制字符串。注意函数s t r l en返回的是有效字符串长度,不 包 含 结 束 符 0。函 数s t r cp y则 连 0 一起复制。(4)第四步,返回本对象的引用,目的是为了实现象a =b=c这样的链式表达。注意 不 要 将r et u r n *t h i s错 写 成r et u r n t h i s。那么能否写成r et u r n o t h er呢?效果不是一样吗?不可以!因为我们不知道参数。t h er的生命期。有 可 能o t h er是个临时对象,在赋值结束后它马上消失,那 么r et u r n o t h er返回的将是垃圾。9.7 偷懒的办法处理拷贝构

48、造函数与赋值函数如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。例如:cl a s s A.p r i v a t e:A(co n s t A&a);/私有的拷贝构造函数A&o p er a t e=(co n s t A&a);/私有的赋值函数);如果有人试图编写如下程序:A b(a);调 用了私有的拷贝构造函数b=a;调 用了私有的赋值函数编译器将指出错误,因为外界不可以操作A的私有函数。9.8 如何在派生类中实现类的基本函数基类的构造函数、析构函数、赋值函数都不能被派生类继

49、承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:派生类的构造函数应在其初始化表里调用基类的构造函数。2001Page 19 of 46局质量C+/C编程指南,v 1.0 基类与派生类的析构函数应该为虚(即 加v i r t u a l关键字)。例如#i n cl u de c l a s s B a s e(p u b l ic:v ir t u a l 、B a s e()c o u t B a s e”e n d l ;);c l a s s D e r iv e d :p u b l ic B a s e(p u b l ic:v ir t u a l D e r iv

50、e d()c o u t D e r iv e d”e n d l ;);v o id m a in(v o id)(B a s e *p B =n e w D e r iv e d;/u p c a s td e l e t e p B;)输出结果为:-D e r iv e dB a s e如果析构函数不为虚,那么输出结果为B a s e在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。例如:c l a s s B a s e(p u b l ic:B a s e&o p e r a t e =(c o n s t B a s e&o t h e r);类 B a s e 的赋

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 教案示例

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁