函数传递是 C/C++ 代码块重用和结构化设计的产物。当代码块封装成函数之后,就需要和“外界”交流,这就是信息传递(接收信息 和放出信息)。信息传递由函数参数和函数返回值负责。
接收信息由函数参数承担,放出信息由函数返回值负责。
为了不破坏封装性和最大限度保证信源的真实性,函数参数获取信息会首先选择信息拷贝(即按值传递),同时为了最大程度不干扰外界, 函数返回值也遵循按值传递(即信息拷贝),如此就把函数中做的细节工作对外界隐藏了,只保留了入口和出口两种接口, 同时这两种接口是信息拷贝,不影响信息发出者和接受者本身。从而保证了函数对数据加工的特性,同时达到了不污染信源的目的。
函数参数和返回值传递方式非常类似,在这里只详细讲解函数参数的传递方式。在 C++ 中它有3中方式:
1、按值传递
3、指针传递
3、引用传递
按值传递
按值传递在传递的时候,实参被复制了一份,然后在函数体内使用。拷贝之后就与实参失去了关联,它的改变不会返回给实参。 换句话说,函数体内修改参数变量时修改的是实参的一份拷贝,而实参本身是没有改变的,所以如果想在调用的函数中修改实参的值, 使用值传递是不能达到目的的,除非将这种改变通过返回值的形式再赋给实参(此法貌似只能改变一个实参)。
当函数运行结束之后,那份拷贝(称之为“形参”)就被释放了(当然函数的其他变量也被释放了)。形参的运算顺序符合逗号法则, 不过不建议使用表达式初始化形参,如int f(a++,a),从左边开始计算还是从右边开始?最好的办法是在函数外先计算好再赋给形参。
可见,按值传递实际上就是把实参的值赋给形参,然后形参在函数中起作用。说白了就是赋值运算,对于内部类型很快,但对于用户 类型赋值操作就会涉及到构造函数和析构函数了,效率大打折扣,而且不一定会享受编译器对内部类型的优化措施。
指针传递
指针传递也可以看作是按值传递。指针里面存的是指向变量的地址,即指针的“值”就是变量的地址,如果按照“按值传递”的思路, 实参传给形参的就是变量的地址。变量的地址被拷贝一份给形参,依照类型的匹配原则,该形参也应该是指针。这样在函数内部要使用 形参(只能使用形参,不能使用实参,实参已经被隐藏)找到指向的变量(就是实参指向的变量)是一种间接寻址,单从这方面就比 按值传递效率低了,如果这种操作很频繁,执行次数较多的话就会大大影响效率。
使用指针传递还有一个隐患:在函数体类对指针操作不当可能其指向发生变化,而失去了对原指向变量的改变作用(即使返回该指针 也是没有用的,因为指向改变了)。如果你不是为了改变它的指向,最好使用 const 限定该指针为常量指针(不能改变指向)。
如果你不想改变指针指向的变量,又是内部类型,应优先考虑按值传递,即安全(编译器对其检查比指针检查要求更严格)有高效!
引用传递
引用一旦(必须定义时)初始化之后就不能改变指向。调用函数时,编译器会为引用开辟空间并绑定实参初始化(该空间存储实参的地址), 从这种意义上讲,其效率和指针相当(都需要间接寻址),但其更像常量指针(不可改变指向),相对来说比一般指针更安全。 而且引用传递时,对形参的操作等同于对实参的操作,即传递的不会是实参的副本,而就是实参。不像指针需要解除地址符来操作实参, 引用传递没有这个步骤。
引用传递的效率如何?这个要看编译器具体实现,引用传递最显然的实现方式是使用指针,这种情况下与指针的效率是一样的, 而有些情况下编译器是可以优化的,采用直接寻址的方式,这种情况下,效率比传值(大量拷贝损失效率)调用和传址(地址拷贝和间接寻址) 调用都要快,与上面说的采用全局变量方式传递的效率相当。
综上,某些情况下引用传递可能被优化,总体效率稍高于传址调用。
符号表上对变量、指针、引用的不同处理
- 程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。
- 指针变量在符号表上对应的地址值为指针变量的地址值。
- 引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改。
因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
三种参数传递的比较
按值传递 | 指针传递 | 引用传递 | |
对实参作用 | 拷贝内容 | 拷贝地址间接操作 | 直接操作或同指针 |
改变实参 | 否 | (解除地址符)可以 | 可以 |
指向 | 可变 | 不可变 | |
类型安全 | 安全 | 不安全 | 安全 |
用户类型效率 | 最低 | 中等 | 最高 |
内部类型效率 | 最高 | 最低 | 中等 |
综上所述,得出以下原则:
- 内建的数据类型优先使用值传递,而对于自定义的数据类型,特别是传递较大的对象,那么请使用引用传递。
- 如果一个参数可能在函数中指向不同的对象,或者这个参数可能不指向任何对象,则必须使用指针参数。
- 引用参数的一个重要用法是,它允许我们在有效实现重载操作符的时,还能保证用法的直观性(因为不需要解除地址符)。
- 如果被返回的对象是被调用函数中的局部变量,则不应按应用方式返回它,因为在被调用函数执行完毕时,局部对象将调用析构函数。 当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。
- 对于用户类型尽量使用引用和指针,因为这两个视为“内部类型”,编译器会像内部类型一样对其尽可能优化(比如使用寄存器)。
实参和形参的关系
原则上实参和形参是单向信息传递的关系,只能实参传给形参,不能反向传递。引用和指针实质上也是这么回事,传递的是地址,也是 单向的,只不过形参可以通过这个地址改变实参(好像信息反向传递了,但是改变的内容而不是传过来的地址,所以仍然是单向传递)。
- 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。 函数调用结束返回主调用函数后则不能再使用该形参变量。
- 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值, 以便把这些值传送给形参。因此应预先用赋值,输入等办法使参数获得确定值。
- 实参和形参在数量上,类型上、顺序上应严格一致,否则就会发生类型不匹配的错误。
- 在一般传值调用的机制中只能把实参传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参值发生改变, 而实参中的值不会变化。而在引用调用的机制当中是将实参引用的地址传递给了形参, 所以任何发生在形参上的改变实际上也发生在实参变量上。
返回对象
当成员函数或独立的函数返回对象时,有几种返回方式可供选择:
1、返回指向对象的引用
2、返回指向对象的 const 引用
3、返回指向对象的 const 对象
返回指向 const 对象的引用
使用 const 引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。
- 如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率
- 如果函数要返回函数中创建的对象,则不能返回其引用,因为一旦函数运行结束该对象就被释放了。
- 函数返回引用的类型与被引用的对象必须一致,如 const 必须都为 const
返回指向非 const 对象的引用
两种常见的返回非 const 对象情形是:重载赋值运算符以及重载与 cout 一起使用的 « 运算符等。前者这样做旨在提高效率,而后者 必须这样做。
返回对象
如果被返回的对象是被调用函数中的局部变量,则不能按引用方式返回它,因为在被调用函数执行完时,局部对象将调用其析构函数。 因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。通常,被重载的算术运算符属于这一类。
返回 const 对象
返回 const 对象就限定该对象不能作为左值,有助于编译器发现 “==” 被写成 “=” 的输入错误(如果不限定为 const 的话讲不报错)。
总结:
- 返回对象
如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。
- 返回引用
如果方法或函数返回一个没有公有复制构造函数的类(如 ostream 类)的对象,它必须返回一个指向这种对象的引用。
- 尽量返回引用
有些方法或函数(如重载的赋值运算符)可以返回独享,也可以返回指向对象的引用,在这种情况下,应首选引用,因为其效率更高。
【注意】本文属于作者原创,欢迎转载!转载时请注明以下内容:
(转载自)ShengChangJian's Blog编程技术文章地址:
https://ShengChangJian.github.io/2016/09/cpp-parameter.html
主页地址:https://shengchangjian.github.io/