Item20–宁以 pass-by-reference-to-const 替换 pass-by-value

1. 性能代价:为什么要避免 Pass-by-Value?

在 C++ 中,默认情况下函数参数是按值传递(pass-by-value)的。这意味着当你在函数中传递一个对象时,编译器会调用该对象的拷贝构造函数(Copy Constructor)来制作一个副本。

场景模拟

假设你有以下类继承结构:

class Person {
public:
    Person();           // 1. Person 构造
    virtual ~Person();  // 2. Person 析构
    // ... private string name, string address ...
};

class Student : public Person {
public:
    Student();          // 3. Student 构造
    ~Student();         // 4. Student 析构
    // ... private string schoolName, string schoolAddress ...
};

现在有一个函数用于验证学生信息,错误(低效)的写法如下:

bool validateStudent(Student s); // 参数是 pass-by-value

// 调用
Student plato;
bool platoIsOK = validateStudent(plato);

发生了什么?

validateStudent(plato) 被调用时,为了创建参数 s,系统发生了巨大的开销:

  1. 调用 Student 的 Copy Constructor:将 plato 复制给 s

  2. 调用 Person 的 Copy Constructor:作为基类部分。

  3. 成员对象的复制PersonStudent 内部可能有 std::string 对象(如 name, address 等),每一个 string 都要调用拷贝构造。

  4. 析构函数:函数结束时,s 被销毁,上述所有对象的析构函数会被再次调用。

总代价 = 6 次构造函数调用 + 6 次析构函数调用(假设每个类有2个 string)。而这一切仅仅是为了让函数“看一眼”这个对象。

解决方案:Pass-by-Reference-to-Const

// 正确写法
bool validateStudent(const Student& s);
  • const: 保证函数内部不会修改原来的对象(调用者放心)。

  • & (引用): 实际上只是传递了一个指针(地址)。

  • 开销: 没有构造函数,没有析构函数。无论对象多大,代价几乎为零。


2. 正确性问题:对象切割 (The Slicing Problem)

这是 pass-by-value 最危险的陷阱。当一个派生类对象值传递的方式传入一个接受基类对象的函数时,派生类的特有部分会被“切掉”(Slicing)。

场景模拟

class Window {
public:
    string name() const; // 返回窗口名称
    virtual void display() const; // 虚函数,用于显示
};

class WindowWithScrollBars : public Window {
public:
    virtual void display() const override; //以此实现带有滚动条的显示
};

错误的函数写法

// 这里的参数是 Window (By Value)
void printNameAndDisplay(Window w) {
    std::cout << w.name();
    w.display();
}

// 调用
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

发生了什么?

  1. 参数 w 是一个 Window 对象。

  2. 当你传入 wwsb 时,编译器调用 Window 的拷贝构造函数。

  3. 它只复制了 wwsb 中属于 Window 的那部分数据。

  4. 结果:函数内部的 w.display() 永远只会调用 Window::display(),而不是 WindowWithScrollBars::display()。多态失效了。

解决方案

// 正确写法:接受 const 引用
void printNameAndDisplay(const Window& w) {
    std::cout << w.name();
    w.display();
}

因为传递的是引用(在底层通常是指针),w 指向了完整的 WindowWithScrollBars 对象。调用 w.display() 时,虚函数机制(vptr/vtable)能正常工作,正确调用派生类的版本。


3. 特例:什么时候该用 Pass-by-Value?

Item 20 并不是说永远都要用引用。对于某些小型的、类似内置类型的对象,Pass-by-value 更加高效。

适用 Pass-by-Value 的情况:

  1. 内置类型int, double, char, bool 等。

    • 理由:把一个 int 放入寄存器的速度比通过引用(指针解引用)访问内存要快得多。

  2. STL 迭代器 (Iterators)函数对象 (Function Objects)

    • 理由:习惯上它们就被设计为通过值传递。因此我们在设计迭代器时,要确保它们的拷贝代价很小且没有切割问题。


总结 (Summary)

特性 Pass-by-Value Pass-by-Reference-to-Const
效率 低(大型对象需拷贝构造+析构) 极高(无新对象生成)
多态 失效(发生对象切割) 正常(支持多态)
适用场景 内置类型 (int, double), STL迭代器 用户自定义类 (Classes, Structs)

核心法则:

对于用户自定义的类型(User-defined types),请总是首选 pass-by-reference-to-const

发表评论