传值、传指针和传引用
我们先从最简单的内置类型说起,结合汇编代码说说三者之间的联系和区别;然后我们再讲用户自定义类类型的三种传参方式的优劣。
内置类型
内置类型的传值是最简单的,也是其他传参形式的基础。此处的“其它传参形式”指所有传参方式,包括内置类型的传指针、传引用,也包括自定义类类型的传值、传指针和传引用。
传指针也是传值
内存地址(指针)也是一个数值,所以从汇编代码的层次看,传值和传指针其实是一样的(《Head First C》中也提到 C 语言所有的传参都是传值)。传值之后的操作更大程度上依赖于业务:
传值(不包括传指针的情形),被调用函数声明为
func(const int i)
或func(int i)
的形式,但后者在其多数的使用场景中其实并不规范,往往应该用const
修饰。在func()
的定义中一般不会对 i 进行取地址操作。这是最基础的使用情形。传值(特指传指针),被调用函数多声明为
func(int *pi)
的形式,也有变种func(int * const pi)
和func(const int *pi)
。传指针的应用场景又可以分为两种:- 需要解引用。一般使用场景为修改指针指向的值的内容,此时不可以声明为
func(const int *pi)
- 不需要解引用。只是单纯的对指针做一些操作,比如重新指向,或者加减指针操作,此时不可以声明为
func(int * const pi)
- 需要解引用。一般使用场景为修改指针指向的值的内容,此时不可以声明为
引用的实现往往基于指针
广义的传值是包括传指针的。传引用,个人认为是对传指针的一种包装,更易于使用,更少的出错可能性。实际上引用的实现往往基于指针,我们可以粗略的认为“传引用 = 传指针 + 解引用”。传引用也可以分为两种使用场景:
- 不会对引用进行取地址操作。这是最常用的情形。
- 会对引用进行取地址操作。这其实是存在性能浪费的,为什么不直接传指针呢?如果说你是回避指针,减少出错的可能性,但既然还是要取地址,那不如从一开始就面对;另一种可能性是函数定义中多数操作是非指针操作,取地址只是个小比例。
代码怎么写更好
上述列举了 5 种代码场景,我们通过业务场景再进行一次分类:
不改实参
我们不需要在被调用函数中修改实参
1 | func(const int i); |
虽然上述 3 种都可以实现目的,但哪一种更好呢?先说结论,在《Effective C++》里提到
对内置(C-like)类型在函数传参时pass by value比pass by reference更高效,当用OO的c++自定义类型(存在构造/析构等)pass by reference to const 更好,STL里的迭代器和函数对象是用C指针实现的,因此pass by value更好。
我们先不讨论自定义类类型。如果有特殊的业务要求,比如要根据指针获得下一个内存块,那么使用指针更合适,但最普通的使用场景下,传指针(包括传引用)比传值多一个解引用步骤(传指针需要显示解引用,传引用是隐式解引用),除此之外两者的开销其实是一样的,就像前面提到的,在汇编代码层次,传递指针和传递数值指令是一样的。
要改实参
我们有修改实参的需求
1 | func(int * const pi); |
哪一种更好呢?如果没有必须使用指针的理由,为了减少出现错误的可能性,个人认为还是选择引用更好一些。貌似 Google 的编码规范中统一接口形式,要求使用指针。
其他
- 对于内置类型,传值比传引用更高效。虽然我无法确定内置类型没有构造函数(我个人倾向于没有构造的观点),参考 Do built-in types have default constructors?
- 内置类型的初始化和赋值成本一样(参考《Effective C++》 第28页)
- 来看 《Effective Modern C++》第 50 页的一句话,重点看我加粗的这一句。可以看出大师对“内置类型初始化和赋值区别”的态度——对于使用者来说,两者的区别不值一提。由此推断,对使用者来说可以认为两者的成本是一样的。同大师在上一版本的作品中给出的结论一致。
The “confusing mess” lobby points out that the use of an equals sign for initialization(使用
=
的初始化) often misleads C++ newbies into thinking that an assignment is taking place(误认为是赋值操作), even though it’s not. For built-in types like int, the difference is academic(学术的,理论的), but for userdefined types, it’s important to distinguish initialization from assignment
类类型
我们之前讨论内置类型时,提到一点“内置类型的传值和传引用(传指针),在撇开解引用步骤之外开销是一样的”。因为内置类型传值是传递一个数值,传指针也是传递一个数值。
我们都知道类类型是对数据的封装——对内置类型或类类型的封装,继承体系中派生类对基类的封装。只关注首尾,类其实是对很多个内置类型的数据成员的封装。从自然语义上说,类类型的传值得包括其所有数据成员的传值,如此就意味着得传递很多个(内置类型)数值了。从严格的语法定义上,类类型传值需要付出类对象初始化(拷贝构造)和销毁(析构)成本,而传引用(指针)可以通过“传地址 + 解引用”来避免这些成本,这基本上是稳赚的——类封装起来的内置类型成员越多,代价越大;而传地址只是传递一个数值,解引用只是一步寻址操作,成本是固定的。只封装一个内置类型成员的类多见于测试代码,实际应用中基本不会出现。
用户自定义的类类型,函数调用时传常量引用比传值更高效,因为前者不会有类对象初始化(拷贝构造)和销毁(析构)成本。
附录
汇编代码
1 | // 所有的汇编指令都是对内存块的操作,其参数都是内存标记(寄存器或内存地址) |