代码改变世界

Effective C++ 学习笔记(15)

2011-08-04 12:03  Daniel Zheng  阅读(217)  评论(0编辑  收藏  举报

尽量使用“传引用”而不是“传值”


  C 语言中,什么都是通过传值来实现的,C++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参总是通过“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。

  “通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。例如,看下面这个(只是假想的)类的结构:

  

class Person
{
public:
Person();
~Person();

...

private:
string name, address;
};

class Student:public Person
{
public:
Student();
~Student();

...
private:
string schoolName, schoolAddress;
};

Student returnStudent(Student s)
{
return s;
}

Student plato;

returnStudent(plato);

  这个看起来无关痛痒的函数调用过程,其内部究竟发生了些什么呢?

  简单地说就是:首先,调用了 Student 的拷贝构造函数用以将s 初始化为plato;然后再次调用Student 的拷贝构造函数用以将函数返回值对象初始化为s;接着,s 的析构函数被调用;最后,returnStudent 返回值对象的析构函数被调用。所以,这个什么也没做的函数的成本是两个Student 的拷贝构造函数加上两个Student 析构函数。

  但没完,还有!Student 对象中有两个string 对象,所以每次构造一个Student对象时必须也要构造两个string 对象。Student 对象还是从Person 对象继承而来的,所以每次构造一个Student 对象时也必须构造一个Person 对象。一个Person 对象内部有另外两个string 对象,所以每个Person 的构造也必然伴随另两个string 的构造。所以,通过值来传递一个Student 对象最终导致调用了一个Student 拷贝构造函数,一个Person 拷贝构造函数,四个string 拷贝构造函数。当Student 对象被摧毁时,每个构造函数对应一个析构函数的调用。所以,通过值来传递一个Student 对象的最终开销是六个构造函数和六个析构函数。因为returnStudent 函数使用了两次传值(一次对参数,一次对返回值),这个函数总共调用了十二个构造函数和十二个析构函数!

  为避免这种潜在的昂贵的开销,就不要通过值来传递对象,而要通过引用:

  

const Student& returnStudent(const Student& s)
{
return s; }

  这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建。

  通过引用来传递参数还有另外一个优点:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的。例如,假设设计这么一套实现图形窗口系统的类:

  

class Window 
{
public:
string name() const; // 返回窗口名
virtual void display() const; // 绘制窗口内容
};
class WindowWithScrollBars: public Window
{
public:
virtual void display() const;
};

  每个 Window 对象都有一个名字,可以通过name 函数得到;每个窗口都可以被显示,着可以通过调用display 函数实现。display 声明为virtual 意味着一个简单的Window 基类对象被显示的方式往往和价格昂贵的WindowWithScrollBars 对象被显示的方式不同(见条款36,37,M33)。

  现在假设写一个函数来打印窗口的名字然后显示这个窗口。下面是一个用错误的方法写出来的函数:

// 一个受“切割问题”困扰的函数
void printNameAndDisplay(Window w)
{
cout
<< w.name();
w.display();
}

  想象当用一个WindowWithScrollBars 对象来调用这个函数时将发生什么:

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

  参数w 将会作为一个Windows 对象而被创建(它是通过值来传递的,记得吗?),所有wwsb 所具有的作为WindowWithScrollBars 对象的行为特性都被“切割”掉了。printNameAndDisplay 内部,w 的行为就象是一个类Window的对象(因为它本身就是一个Window 的对象),而不管当初传到函数的对象类型是什么。尤其是, printNameAndDisplay 内部对display 的调用总是Window::display,而不是WindowWithScrollBars::display。

  解决切割问题的方法是通过引用来传递w:

  

// 一个不受“切割问题”困扰的函数
void printNameAndDisplay(const Window& w)
{
cout
<< w.name();
w.display();
}

  现在 w 的行为就和传到函数的真实类型一致了。为了强调w 虽然通过引用传递但在函数内部不能修改,就要采纳条款21 的建议将它声明为const。

  传递引用是个很好的做法,但它会导致自身的复杂性,最大的一个问题就是别名问题,这在条款17 进行了讨论。另外,更重要的是,有时不能用引用

来传递对象,参见条款23。最后要说的是,,引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——

例如int— — 传值实际上会比传引用更高效。