Item 20:传递常量引用比传值更好
传引用更加高效
缺省情况下,C++ 以传值方式将对象传入或传出函数。除非你特别指定其它方式,否则函数的参数就会以实际参数的拷贝进行初始化,而函数的调用者会收到函数返回值的一个拷贝。这个拷贝由对象的拷贝构造函数生成。这就使得传值成为一个代价不菲的操作。
例如,考虑下面这个类层级结构:
class Person {
public:
Person(); // parameters omitted for simplicity
virtual ~Person(); // see Item 7 for why this is virtual
...
private:
std::string name;
std::string address;
};
class Student: public Person {
public:
Student(); // parameters again omitted
virtual ~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
考虑以下代码,在此我们调用一个函数—— validateStudent,它得到一个 Student 参数(以传值的方式),并返回它是否验证有效的结果:
bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
在调用 validateStudent 时进行了6个函数调用:
- Person 的拷贝构造函数。
- Student的拷贝构造函数。
- name 的拷贝构造函数。
- address 的拷贝构造函数。
- schoolName 的拷贝构造函数。
- schoolAddress 的拷贝构造函数。
当 Student 对象的拷贝被销毁时,每一个构造函数的调用都对应一个析构函数的调用,所以以传值方式传递一个 Student 的全部代价是六个构造函数和六个析构函数!
解决办法便是传递常量引用:
bool validateStudent(const Student& s);
- 以引用的方式传递,不会构造新的对象,避免了上述例子中6个构造函数的调用。
- const 也是必须的:传值的方式保证了该函数调用不会改变原来的 Student, 而传递引用后为了达到同样的效果,需要使用 const 声明来声明这一点,让编译器去进行检查!
传值造成的截断问题
将传值改为传引用还可以有效地避免截断问题:当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象的行为像一个派生类对象的特殊特性被“切断”了。你只剩下一个纯粹的基类对象。
比如一个 Window 父类派生了子类 WindowWithScrollBars:
class Window {
public:
//@ draw window and contents
virtual void display() const { std::cout << "display Window" << "\n"; }
};
class WindowWithScrollBars : public Window {
public:
virtual void display() const { std::cout << "display WindowWithScrollBars" << "\n"; }
};
//@ 产生截断,调用 Window::display
void printNameAndDisplay(Window w) {
w.display();
}
//@ 调用 Window::WindowWithScrollBars
void printNameAndDisplay(Window& w) {
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
当调用 printNameAndDisplay 时参数类型从 WindowWithScrollBars 被隐式转换为 Window。 该转换过程通过调用 Window 的拷贝构造函数来进行。 导致的结果便是函数中的 w 事实上是一个 Window 对象, 并不会调用多态子类 WindowWithScrollBars 的 display()。
特殊情况
一般情况下相比于传递值,传递常量引用是更好的选择。但也有例外情况,比如内置类型和STL迭代器和函数对象。
- 内置类型传值更好是因为它们小,而一个引用通常需要32位或者64位的空间。
- 但对象小并不意味着拷贝构造的代价不高!比如STL容器通常很小,只包含一些动态内存的指针。然而它的拷贝构造函数中, 必然会分配并拷贝那些动态内存的部分。
- STL 迭代器和函数对象也应当被传值,这是因为它们在 STL 中确实是被这样设计的,同时它们的拷贝构造函数代价并不高。
总结
- 用传引用给 const 取代传值。通常情况下它更高效而且可以避免类截断问题。
- 这条规则并不适用于内建类型及 STL 中的迭代器和函数对象类型。对于它们,传值通常更合适。