Item 20:传递常量引用比传值更好 Effective C++笔记

C++ 引用 常量

Item 20: Prefer pass-by-reference-to-const to pass-by-value

C++函数的参数和返回值默认采用传值的方式,这一特性是继承自C语言的。如果不特殊指定, 函数参数将会初始化为实参的拷贝,调用者得到的也是返回值的一个副本。 这些拷贝是通过调用对象的拷贝构造函数完成的,正是这一方法的调用使得拷贝的代价可能会很高。

通常来讲,传递常量引用比传值更好,同时避免了截断问题。但是内置类型和STL容器、迭代器,还是传值更加合适。

来个例子

一个典型的类的层级可能是这样的:

class Person {
    string name, address;
};
class Student: public Person {
    string schoolName, schoolAddress;
};

假如有这样一处函数调用:

bool validateStudent(Student s);           // function taking a Student by value

Student plato;                             // Plato studied under Socrates
bool platoIsOK = validateStudent(plato);   // call the function

在调用validateStudent()时进行了6个函数调用:

  1. Person的拷贝构造函数,为什么Student的拷贝构造一定要调用Person的拷贝构造请参见:Item 12:完整地拷贝对象
  2. Student的拷贝构造函数
  3. name, address, schoolName, schoolAddress的拷贝构造函数

解决办法便是传递常量引用:

bool validateStudent(const Student& s);

首先以引用的方式传递,不会构造新的对象,避免了上述例子中6个构造函数的调用。 同时const也是必须的:传值的方式保证了该函数调用不会改变原来的Student, 而传递引用后为了达到同样的效果,需要使用const声明来声明这一点,让编译器去进行检查!

截断问题

将传值改为传引用还可以有效地避免截断问题:由于类型限制,子类对象被传递时只有父类部分被传入函数。

比如一个Window父类派生了子类WindowWithScrollBars

class Window {
public:
  ...
  std::string name() const;           // return name of window
  virtual void display() const;       // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
  ...
  virtual void display() const;
};

有一个访问Window接口的函数,通过传值的方式来获取Window的实例:

// incorrect! parameter may be sliced!
void printNameAndDisplay(Window w){     
  std::cout << w.name();
  w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

当调用printNameAndDisplay时参数类型从WindowWithScrollBars被隐式转换为Window。 该转换过程通过调用Window的拷贝构造函数来进行。 导致的结果便是函数中的w事实上是一个Window对象, 并不会调用多态子类WindowWithScrollBarsdisplay()

// fine, parameter won't be sliced
void printNameAndDisplay(const Window& w){ 
  std::cout << w.name();
  w.display();
}

这就很好嘛,如果你曾深入过编译器你会发现引用是通过指针来实现的。

特殊情况

一般情况下相比于传递值,传递常量引用是更好的选择。但也有例外情况,比如内置类型STL迭代器和函数对象

内置类型传值更好是因为它们小,而一个引用通常需要32位或者64位的空间。可能你会认为小的对象也应当首选传值, 但对象小并不意味着拷贝构造的代价不高!比如STL容器通常很小,只包含一些动态内存的指针。然而它的拷贝构造函数中, 必然会分配并拷贝那些动态内存的部分。

即使拷贝构造函数代价很小,传值的方式仍然有性能问题。有些编译器会区别对待内置类型和用户定义类型, 即使它们有相同的底层表示。比如有些编译器虽然会把double放入寄存器,但是拒绝将只含一个double的对象放入寄存器。

一个只含double的对象大小为8,它和一个double具有相同的大小和底层表示。关于对象大小的计算,请参考:Item 7:将多态基类的析构函数声明为虚函数

从面向对象设计方面来讲,即使对象现在很小,但它作为用户定义类型是有可能变大的(如果你更改了内部实现)。 从长远来讲的性能考虑,也应当采取传引用的方式来设计使用它的函数。

STL迭代器和函数对象也应当被传值,这是因为它们在STL中确实是被这样设计的,同时它们的拷贝构造函数代价并不高。

Harttle

致力于简单的、一致的、高效的前端开发

看看这个?