洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目20] 传参时要多用“引用常量”,少用传值

默认情况下,C++为函数传入和传出对象是采用传值方式的(这是由C语言继承而来的特征)。除非你明确使用其他方法,函数的形式参数总会通过复制实在参数的副本来创建,并且,函数的调用者得到的也是函数返回值的一个副本。这些副本是由对象的拷贝构造函数创建的。这使得“传值”成为一项代价十分昂贵的操作。请观察下边的示例中类的层次结构:

class Person {

public:

  Person();                        // 省略参数表以简化代码

  virtual ~Person();               // 条目7解释了它为什么是虚函数

  ...

 

private:

  std::string name;

  std::string address;

};

 

class Student: public Person {

public:

  Student();                       // 再次省略参数表

  virtual ~Student();

  ...

 

private:

  std::string schoolName;

  std::string schoolAddress;

};

请观察下面的代码,这里我们调用一个名为validateStudent的函数,通过为这一函数传进一个Student类型的参数(传值方式),它将返回这一学生的身份是否合法:

bool validateStudent(Student s);         // 通过传值方式接受一个Student对象

 

Student plato;                           // 柏拉图是苏格拉底的学生

 

 

bool platoIsOK = validateStudent(plato); // 调用这一函数

在这个函数被调用时将会发生些什么呢?

很显然地,在这一时刻,通过调用Student的拷贝构造函数,可以将这一函数的s参数初始化为plato的值。同样显然的是,svalidateStudent返回的时候将被销毁。所以这一函数中传参的开销就是调用一次Student的拷贝构造函数和一次Student的析构函数。

但是上边的分析仅仅是冰山一角。一个Student对象包含两个string对象,所以每当你构造一个Student对象时,你都必须构造两个string对象。同时,由于Student类是从Person类继承而来,所以在每次构造Student对象时,你都必须再构造一个Person对象。一个Person对象又包含两个额外的string对象,所以每次对Person的构造还要进行额外的两次string的构造。最后的结果是,通过传值方式传递一个Student对象会引入以下几个操作:调用一次Student的拷贝构造函数,调用一次Person的拷贝构造函数,调用四次string的拷贝构造函数。在Student的这一副本被销毁时,相应的每次构造函数调用都对应着一次析构函数的调用。因此我们看到:通过传值方式传递一个Student对象总体的开销究竟有多大?竟达到了六次构造函数和六次析构函数的调用!

下面向你介绍正确的方法,这一方法才会使函数拥有期望的行为。毕竟你期望的是所有对象以可靠的方式进行初始化和销毁。与此同时,如果可以绕过所有这些构造和析构操作将是件很惬意的事情。这个方法就是:通过引用常量传递参数:

bool validateStudent(const Student& s);

这样做效率会提高很多:由于不会创建新的对象,所以就不会存在构造函数或析构函数的调用。改进的参数表中的const是十分重要的。由于早先版本的validateStudent通过传值方式接收Student参数,所以调用者了解:无论函数对于传入的Student对象进行什么样的操作,都不会对原对象造成任何影响,validateStudent仅仅会对对象的副本进行修改。而改进版本中Student对象是以引用形式传入的,有必要将其声明为const的,因为如果不这样,调用者就需要关心传入validateStudentStudent对象有可能会被修改。

通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。这肯定不是你想要的。请看下边的示例,假设你正在使用一组类来实现一个图形窗口系统:

class Window {

public:

  ...

  std::string name() const;        // 返回窗口的名字

  virtual void display() const;    // 绘制窗口和内容

};

 

class WindowWithScrollBars: public Window {

public:

  ...

  virtual void display() const;

};

所有的Window对象都有一个名字,可以通过name函数取得。所有的窗口都可以被显示出来,可以通过调用display实现。display是虚函数,这一事实告诉我们,简单基类Window的对象与派生出的WindowWithScrollBars对象的显示方式是不一样的。(参见条目3436

现在,假设你期望编写一个函数来打印出当前窗口的名字然后显示这一窗口。下面是错误的实现方法:

void printNameAndDisplay(Window w) // 错误! 参数传递的对象将被截断!

{

  std::cout << w.name();

  w.display();

}

考虑一下当你将一个WindowWithScrollBars对象传入这个函数时将会发生些什么:

WindowWithScrollBars wwsb;

 

printNameAndDisplay(wwsb);

参数w将被构造为一个Window对象——还记得么?它是通过传值方式传入的。这里,使wwsb具体化的独有信息将被截掉。无论传入函数的对象的具体类型是什么,在printNameAndDisplay的内部,w将总保有一个Window类的对象的身份(因为它本身就是一个Window的对象)。特别地,在printNameAndDisplay内部对display的调用总是Window::display,而永远不会是WindowWithScrollBars::display

解决截断问题的方法是:通过引用常量传参:

void printNameAndDisplay(const Window& w)

{                                  // 工作正常,参数将不会被截断。

  std::cout << w.name();

  w.display();

}

现在w的类型就是传入窗口对象的精确类型。

揭开C++编译器的面纱,你将会发现引用通常情况下是以指针的形式实现的,所以通过引用传递通常意味着实际上是在传递一个指针。因此,如果传递一个内建数据类型的对象(比如int),传值会被传递引用更为高效。那么,对于内建数据类型,当你在传值和传递常量引用之间徘徊时,传值方式不失为一个更好的选择。迭代器和STL中的函数对象也是如此,这是因为它们设计的初衷就是能够更适于传值,这是C++的惯例。迭代器和函数对象的设计人员有责任考虑复制时的效率问题和截断问题。(这也是一个“使用哪种规则,取决于当前使用哪一部份的C++”的例子,参见条目1

内建数据类型体积较小,所以一些人得出这样的结论:所有体积较小的类型都适合使用传值,即使它们是用户自定义的。这是一个不可靠的推理。仅仅通过一个对象体积小并不能判定调用它的拷贝构造函数的代价就很低。许多对象——包括大多数STL容器——其中仅仅包含一个指针和很少量的其它内容,但是复制此类对象的同时,它所指向的所有内容都需要复制。这将付出十分高昂的代价。

即使体积较小的对象的拷贝构造函数不会带来巨大的开销,它也会引入性能问题。一些编译器对内建数据类型和用户自定义数据类型是分别对待的,即使它们的表示方式完全相同。比如说一些编译器很乐意将一个单纯的double值放入寄存器中,这是语言的常规;但将一个仅包含一个double值的对象放入寄存器时,编译器就会报错了。当你遇到这种事情时,你可以使用引用传递这类对象,因为编译器此时一定会将指针(引用的具体实现)放入寄存器中。

对于“小型的用户自定义数据类型不适用于传值方式”还有一个理由,那就是:作为用户自定义类型,它们的大小可能会改变。现在很小的类型在未来的版本中可能会变得很大,这是因为它的内部实现方式可能会改变。即使是你更改了C++语言的具体实现都可能会影响到类型的大小。比如,在我编写上面的示例的时候,一些对标准库实现中string的大小竟然达到了另一些的七倍。

总体上讲,只有内建数据类型、STL迭代器和函数对象类型适用于传值方式。对于所有其它的类型,都应该遵循本条款中的建议:尽量使用引用常量传参,而不是传值。

时刻牢记

尽量使用引用常量传参,而不是传值方式。因为一般情况下传引用更高效,而且可以避免“截断问题”。

对于内建数据类型、STL迭代和函数对象类型,这一规则就不适用了,对它们来说通常传值方式更实用。

posted on 2007-06-01 18:12 ★ROY★ 阅读(1405) 评论(3)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【翻译】[Effective C++第三版•中文版][第20条]尽量使用“引用常量”传参,而不是传值  回复  更多评论   

好漂亮的程序啊!
2007-06-02 09:16 | 深蓝色的音符

# re: 【翻译】[Effective C++第三版•中文版][第20条]尽量使用“引用常量”传参,而不是传值  回复  更多评论   

给你做了个链接,希望以后能跟你多多交流.
因为我现在也在开始学习C++,不过好难啊!
2007-06-02 13:04 | 深蓝色的音符

# re: 【翻译】[Effective C++第三版•中文版][第20条]尽量使用“引用常量”传参,而不是传值  回复  更多评论   

说的很对
2007-06-04 14:02 | picasa

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理