简介
- 尽量以常值引用传递(pass-by-reference to const)代替值传递(pass by value);前者比较高效并且可以避免对象切割问题
- 对内置类型、STL 迭代器和函数对象使用值传递
值传递函数的缺点
在 C++ 中默认的参数传递方式是值传递,即在传递参数过程中会调用需要传递传递的参数的构造函数(具体来说是 copy 构造函数)创建一份副本并传入函数中,这在某种情况下是比较昂贵的操作,考虑一下例子:
1 | class Person { |
接下来考虑运行以下代码:
1 | // 测试值传递参数的函数 |
在调用 validateStudent()
时,由于该函数采用值传递方法,因此在调用函数时会调用一次 Student
的拷贝构造函数。假如在 Student
, Person
中的构造函数和析构函数中添加输出语句方便观察,运行输出如下(实际代码见 ex1):
1 | Person constructed. |
可以发现,在运行过程,Person
和 Student
的构造函数各被调用两次,一次是默认构造函数另一次是拷贝构造函数。但除此之外还有考虑 Student
和 Person
中的成员变量,同样为了方便演示,我们在其中添加一个自定类 Age
并在其构造函数中添加相应输出(实际代码见 ex2):
1 | class Age { |
1 | Age constructed. |
可以发现 Person
中的 Age
对象同样通过拷贝构造函数产生了一个副本。因此,在调用 ValidateStudent()
中,包括 std::string
在内实际上调用了 7 次构造函数 (Person
一次, Student
一次, Age
一次, std::string
四次)以及 7 次析构函数。因此函数参数值传递是非常昂贵的操作。
常值引用传递 (pass by reference to const) 的优点
减少不必要的开销
在以上的例子中,实际上我们并不需要对传入参数进行修改操作(从函数名可以看出来),因此在这种情况下为了避免拷贝构造 Student
的开销,我们应该使用常值引用传递,在上面的例子中,只需要把 ValidateStudent()
改成:
1 | bool validateStudent(const Student& s); |
运行输出如下:
1 | Age constructed. |
可以发现只有初始化 plato
时调用了构造函数,调用函数没有产生任何额外开销,同时因为我们限定了传递的时候引用参数被声明为 const
,因此也不用担心函数会对外部变量进行修改。
避免对象切割问题
上述的用法只是说明了常值引用传递可以减少值传递的开销,他们实际的效果是一样的。但实际上值传递在有些场合可能会造成问题。考虑以下例子:
1 | class Window { |
上述例子中,WindowWIthScrolledBar
是 Window
的子类并且有一个 display()
虚函数。假如我们通过以下函数来进行对窗口的 display()
的操作:
1 | void printNameAndDisplay(Window w) { |
通常情况我们会希望 printNameAndDisplay()
可以随着我们传入参数的类型不同调用不同的 display()
函数(多态)。但是如果通过值传递的方法的话我们会得到如下输出(在 display()
中加入输出语句,具体代码见 ex4):
1 | Window |
可以发现 w
的 WindowWithScrolledBar
性质完全没体现出来,这是因为值传递的过程中由于我们声明的类型是 Window
所以实质上无论是 Window
的什么子类,拷贝过程是调用了一次 Window
的拷贝构造函数构造了一个 Window
对象,因此子类的性质就不存在了。解决的方法也很简单,通过常值引用传递即可:
1 | void printNameAndDisplay(const Window& w) { |
同样的调用方法输出结果:
1 | Window |
使用常值引用传递的场合
大部分 C++ 编译器底层对引用的实现是基于指针,因此传递引用实际上是传递指针,因此如果要传递的参数是内置类型(如 int
),那么值传递的效率可能会比引用传递的效率稍高一些。另外,STL 的迭代器和函数对象通常涉及为值传递。因此我们在使用时需要自己注意是否会出现上述的对象切割问题。
另外,有部分人认为使用值传递还是常值引用传递只跟对象尺寸大小有关,即对 double
对象使用值传递,对一个内部只包含一个 double
的自定义类对象也使用值传递。这个结论并不一定可靠,有以下几个论点:
- 有一些自定义类内部可能只含有指针(尺寸并不大)但是拷贝函数的开销并不低,因为同时还要拷贝指针指向的对象
- 编译器在处理内置类型和自定义类的处理会有所不同,哪怕他们尺寸可能是一样的
- 我们需要考虑到以后自定义类有可能会被扩充的情况
因此通常而言,只推荐对内置类型、STL 迭代器以及函数对象使用值传递,其他情况可能使用常值引用传递
结论
- 尽量以常值引用传递(pass-by-reference to const)代替值传递(pass by value);前者比较高效并且可以避免对象切割问题
- 对内置类型、STL 迭代器和函数对象使用值传递
其他
- 完整可运行代码地址:Effective-Cpp-Reading-Note
- Efftive C++ 阅读笔记:Effective C++