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
)。如果你对此感兴趣,确定自己能理解这样的语法和如此复杂声明的含义。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。