《C++面向对象高效编程(第2版)》——3.12 参数传递模式——客户的角度

3.12 参数传递模式——客户的角度

C++面向对象高效编程(第2版)
在设计类的接口时,要声明类的成员函数,并指定它们的参数。类的客户调用成员函数时,提供实参(如果有的话)。

每种方法都应清楚地指明参数的传递模式,参数可以按值、按引用或按指针传递。与const联合使用,参数会更加安全可靠。函数的原型用于向客户传达这些信息。

每个参数的传递模式都给客户传达特定的含义。再者,有时还需遵循一些经长时间验证确实行之有效的规则。因此,为参数选择合适的类型非常重要。在接下来的内容中,我们将介绍参数传递的不同样式和它们的含义。

注意:
在接下来的示例中,术语主调函数(caller)指的是g()函数(或者main程序),它调用另一个函数f()。在这种情况下,f()就是被调函数(calle e),即g()所调用的函数。换言之,主调函数是发起转移控制权的函数,被调函数是接受控制权的函数。
在以下所列例子中,将使用T类和X类,以及X类的成员函数f()。无需考虑T类和X类中所具体包含的内容。

(1)void X::f(T arg) // 第一例,按值传递(pass by value)。
被调函数可以对arg(原始对象的副本)进行读取和写入。在f()内改动arg不会影响f()的主调函数,因为主调函数已提供原始对象的副本。这也许是参数传递最佳和最安全的模式,主调函数和被调函数互相保护。但是,这种模式也存在缺点:要调用复制构造函数复制原始对象,再将原始对象的副本传递给f(),而且在退出f()时,通常还必须通过arg调用析构函数。必须记住,每次调用构造函数后,迟早都要调用析构函数销毁对象。构造函数和析构函数的开销很大。再者,有时复制对象操作仅限于特权客户或被完全禁止。在这种情况下,就不能使用按值传递,用按引用传递参数会更好。另外,复制大型对象非常耗时,此时,按值传递参数通常不是首选的方案。f()不应该在对象(该对象调用f())的数据成员中储存arg的地址(使用this指针),因为一旦退出函数,arg即被销毁。

(2)void X::f(const T arg)  // 第二例,按值传递。
该例和上例非常相似,仍然是按值传递,且它有普通按值传递所有的优缺点。但是,在该例中,被调函数只能对arg进行读取,不能写入,因为arg被声明为const对象。通常,主调函数对这样的参数传递样式都视而不见。实际上,主调函数并不关心被调函数如何操作它的副本。因为那只是个副本,并不是真正的对象。const仅是被调函数对原始对象副本施加的额外限制。

(3)void X::f(T& arg)  // 第一例,按引用传递(pass by reference)。
除非有其他的说明,否则,该例意味着被调函数可对arg进行读出和写入。换言之,arg是一个输入输出形参(in-out parameter)。被调函数可以修改真正的对象,也就是说,f()可以在需要时从arg中读取输入形参,然后再将结果写回arg中。如果确实打算这样操作,要在注释中清楚地说明。注意,arg属于主调函数,f()不会销毁它。

另一方面,我们可能打算把arg作为只输出形参(out-only parameter)使用(即被调函数可将结果值写入arg中,但不能从中读取值)。编译器无法强制执行这个规则。通常,在这种情况下,arg是一个未初始化的对象,仅用于返回值(只是一个输出形参)。主调函数创建一个空对象(可能使用默认构造函数),并将其传递给f(),f()把返回值写入arg中。需要更详细的文档才能清楚地说明该意图。如果打算把arg作为输出形参使用,那么,最好让这样的函数都遵循一种不同的命名约定(如函数名前缀Copy)。在主调函数已选定储存格式,被调函数只负责填充原始对象的情况下,使用引用作为输出形参是不错的方案。通常这种情况要用到继承层次,我们将在后续章节中介绍。记住,arg属于主调函数。按引用传递参数保证了参数是活的对象,它不像空指针那样。这也保证了引用的对象在f()调用的生存期内一直存在。不要在真正需要使用对象的地方,使用指向T的指针。

警告:
通常认为,无论何时传递引用参数,被调函数都不应该保存arg的地址。因为在退出函数后,无法保证arg还存在。(3)说明,f()应假设arg的生存期受限于f()的作用域内。想象一下,如果f()将arg的地址保存在它的对象(该对象调用f())的一个数据成员中,稍后试图通过已保存的地址使用arg。在此之前,arg可能已经在主调函数的作用域内被销毁了。主调函数不会保证arg的生存期!进行任何类似的操作一定会导致程序崩溃(或引起无法预料的行为和难以追踪的潜在程序错误)。以上的分析并不是说f()不应该获取arg的地址,获取地址没错,我们在赋值操作符中就要获取地址。但是,不要在任何数据成员中保存该地址。
   

线程安全
在多线程环境中,主调函数必须保证arg在f()函数的整个生存期内都存在。如果某线程调用一个带引用形参的函数,在f()调用完成之前,如果其他的线程被调度执行,且该进程销毁了传递给f()的对象,情况会变得非常糟糕。调试这样的代码简直是一场噩梦。
hand 绝不要对只输入形参(in-only parameter)使用按引用传递(无const限定符)模式。

(4)void X::f(const T& arg)  // 第二例,按引用传递。
此例优于(3),被调函数对arg只能读取不能写入。因为arg是对const对象的引用,它是一个只输入形参(in-only parameter)。在传递大型对象时,此传递样式为高效之道,强烈推荐使用。在按值传递不可用时,尽可能地使用该样式。和(3)一样,该例也意味着被调函数不能储存arg的地址,因为无法保证arg在函数返回后仍存在。在(3)和(4)中,f()函数可能也想制作一份arg的副本,但主调函数不允许这样做。要在文档中清楚地说明主调函数的意图。

(5)void X::f(T* argp)   // 第一例,按指针传递。
C程序员钟爱指针。大多数时候,他们在C中使用指针合情合理,因为C并没有引用的概念。然而,在C++中,如果不能清楚地理解指针的意图,会让情况变得很糟。无论何时,使用指针的好处是:可以用一个特别的值—— 0(也称为NULL),区别合法指针和非法指针。引用无此特点,无法区别合法引用和非法引用。实际上,正确使用引用时,绝不会出现对不存在对象的引用。

这种情况有些含糊不清,而且有潜在的不安全隐患。如果被调函数仅对argp所指向的对象写入,那么,它只是一个输出形参,argp的名称前应缀有out。甚至,如果此函数也能遵循不同的命名约定会更好(例如,函数名前缀Copy)。主调函数必须把可修改对象的地址传递给f()。传递NULL指针非常容易(有意或无意地)。这意味着,被调函数不能假定argp所指向对象存在。指针argp本身按值传递,这表明f()既不能创建新对象也不能储存argp中的地址,这样,主调函数才能检索到该地址。如果确实希望让f()改变argp所持的地址,应传递对argp指针的引用1。argp也可以被当做输入输出形参,在这种情况下,被调函数能读取argp所指向的对象,还能为其填入返回值。如果传入的是一个空指针就不能这样做。

如果确实想使用这种模式,最好是让argp带默认值0,如果将接口改变为:

void X::f(T* argp = 0)
就相当于明确地告知客户,即使将零指针(zero point)传递给这个函数,也不会引起任何无法预料的行为。

1该声明应该是f(T* & argp)。如果你对此感兴趣,确定自己能理解这样的语法和如此复杂声明的含义。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。

时间: 2024-12-24 02:27:25

《C++面向对象高效编程(第2版)》——3.12 参数传递模式——客户的角度的相关文章

《C++面向对象高效编程(第2版)》——导读

前言 C++面向对象高效编程(第2版) 面向对象软件开发已逐渐成为开发软件的首选.优秀的面向对象软件开发人员.设计人员.系统架构师对其需求与日俱增.要想成为一名成功的面向对象编程(OOP)人员必须忘却(摈弃)多年来面向程序编程的习惯,从新的角度分析问题. 面向对象编程要求程序员和设计者非常熟悉一些基本范式或概念.理解这些范式是在面向对象软件领域打下牢固基础的基本要求.支持OOP的语言都必须支持这些基本范式.换言之,学习OOP,简单地说,就是学习许多语言(如C++,Eiffel,SmallTalk

《C++面向对象高效编程(第2版)》——4.2 无用单元收集问题

4.2 无用单元收集问题 C++面向对象高效编程(第2版) 在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference). 4.2.1 无用单元 所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用.按照C++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源.以下是一个示例: main() { char* p =

《C++面向对象高效编程(第2版)》——4.6 对象赋值的语义

4.6 对象赋值的语义 C++面向对象高效编程(第2版) 赋值与复制的操作非常类似.在C++中,绝大多数的复制操作都由语言隐式调用(当对象按值传递或按值返回时).当通过现有对象创建新对象时,也进行了复制操作(但不是很频繁).与复制相反的是,赋值是必须由程序员显式调用的操作.然而,在Eiffel和Smalltalk中,赋值和复制操作都由程序员显式调用.这也是基于值的语言与基于引用的语言之间的区别. 在C++中,对于对象和基本类型赋值都具有相同的含义.把基本类型变量赋值给另一个(兼容的)基本类型变量

《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

2.21 确保抽象的可靠性--类不变式和断言 C++面向对象高效编程(第2版) 任何抽象都必须与客户履行它的契约(contract).当客户使用类时,他希望类的对象像其发布描述的那样运行正常.另一方面,类的实现者必须千方百计地确保对象运行正常.但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责.例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer).只有满足此前提条件,成员函数才能保证它的行为.因此,客户必须履行一些义务.换言之,如果客户履行了她那部分契约,

《C++面向对象高效编程(第2版)》——2.30 has-a关系的重要性

2.30 has-a关系的重要性 C++面向对象高效编程(第2版) "has-a"关系(也称为关联.聚集.包含.组合)是在OOD(面向对象设计)中频繁使用的重要关系.但是,许多设计者和程序员都没有很好地理解其相关性,从而导致复杂僵化的设计和不必要的继承. 在OOD阶段,软件的复用主要通过两种方式完成:继承和包含.它们各有优缺点.优秀的设计者了解它们的局限性.优点和代价,可以灵活自如地应用它们.继承将在第5章.第6章以及第二部分的第12章中详细讨论. 包含是一项强大的设计技术,它比继承更

《C++面向对象高效编程(第2版)》——2.27 关联

2.27 关联 C++面向对象高效编程(第2版) 关联表示对象与不同类之间的结构关系(structual relationship),大多数关联都是二元关系(binary relation).类之间的多重关联(multiple association)和类本身的自关联(self association)都是合法的(见图2-19). 关联可以有一个名称,表明阅读方向的箭头为可选.注意,方向箭头为可选,但关联名必须显示.关联在不同的方向可以有不同的名称,但是,大多数情况下,没必要注明(特别是在已标出

《C++面向对象高效编程(第2版)》——3.2 类要素的细节

3.2 类要素的细节 C++面向对象高效编程(第2版) 3.2.1 访问区域 客户可以访问在类的public区域中声明的任何成员.我们可以把该区域看做是通用公共(general public)的接口,它没有任何保护,是类限制最少的区域.一个设计良好的类绝不会将数据成员包含在public区域,该区域只能包含成员函数.如果在public区域包含数据成员,那么无需类的实现者,仅通过编译器即可访问这些数据成员.这违反了数据抽象和封装原则.这也是我们为什么总将数据成员放在private或protected

《C++面向对象高效编程(第2版)》——第2章 什么是数据抽象

第2章 什么是数据抽象 C++面向对象高效编程(第2版) 面向对象编程的一项基本任务是创建带有适当功能的类,并隐藏不必要的细节(即抽象数据).下面,我们将用一个现实生活中的例子来解释数据抽象的概念. 绝大多数人都见过影碟播放机(laser disc player)(或LD播放机).现在,提出一个简单的问题:设计一个影碟播放机,要求易于使用和修改,可后续添加更多有用的功能. 注意: 如果难以理解影碟播放机,可以用CD播放机代替LD播放机,其设计原理类似.实际上,影碟播放机的功能是CD播放机功能的超

《C++面向对象高效编程(第2版)》——1.5 什么可以作为类

1.5 什么可以作为类 C++面向对象高效编程(第2版) 用简单的例子详细讨论类和对象非常容易,但是难点在于如何为给定的问题找出合适的类.我们必须理解类代表什么,何时将问题中的某些部分转化为类,而非数据,反之亦然.根据我们的定义,类拥有一组对象的共同属性(或者特性).怎样的共同才是共同?何时说这是一个类,而不是另一个类的对象?这些都是我们在学习OOP时会遇到的,和真正关心的问题. 当我们决定创建一个类时,第一个问题就是"是否确实需要这个类的多个实例?",如果答案为"是&quo