第20条:尽量使用“引用常量”传参代替传值

	默认情况C++以传值方式传递对象至函数(继承自C语言的特征)。除非另外指定,否则函数参数都是以实际实参的复件(副本)为初值,并且,函数调用者得到的也是函数返回值的一个副本。这些副本是由对象的拷贝构造函数创建。这使得“传值”成为昂贵的操作。考虑以下class继承体系:
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 的值 。同样显然的是, s 在 validateStudent 返回的时候将被销毁。所以这一函数中传参开销就是调用一次 Student 的拷贝构造函数和一次 Student 的析构函数
 
但上边的分析仅是冰山一角。一个 Student 对象包含两个 string 对象,所以每当你构造一个 Student 对象时,你都必须构造两个 string 对象。同时,由于 Student 类是从 Person 类继承而来,所以在每次构造 Student 对象时,你都必须再构造一个 Person 对象。一个 Person 对象又包含两个额外的 string 对象,所以每次对 Person 的构造还要进行额外的两次 string 的构造。最后的结果是,通过传值方式传递一个 Student 对象会引入以下几个操作:调用一次 Student 的拷贝构造函数,调用一次 Person 的拷贝构造函数,调用四次 string 的拷贝构造函数。在 Student 的这一副本被销毁时,构造函数调用都对应着相应析构函数。因此我们看到:通过传值方式传递一个 Student 对象总体的开销究竟有多大?调用六次构造函数和六次析构函数!
 
下面介绍正确的方法,这一方法才会使函数拥有期望的行为。毕竟期望的是所有对象以可靠的方式进行初始化和销毁。如果可以绕过所有这些构造函数和析构函数将是件很惬意的事情。那就是:通过引用常量传递参数(pass byreference-to-const):
bool validateStudent(const Student& s);

	这样做效率会提高很多:由于不会创建新的对象,所以就不会存在构造函数或析构函数的调用。改进的参数表中的 const 十分重要。由于之前版本的 validateStudent 通过传值方式接收 Student 参数:无论函数对于传入的 Student 对象进行什么样的操作,都不会对原对象造成任何影响, validateStudent 仅会对对象的副本进行修改。而改进版本中 Student 对象是以引用形式传入的,有必要将其声明为 const 的,否则,调用者就需要关心传入 validateStudent 的 Student 对象有可能会被修改。
 
	通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。这肯定不是你想要的。请看下边的示例,假设你正在使用一组类来实现一个图形视窗系统:
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 对象都有一个名字,可以通过 name 函数取得,所有的视窗可以被显示出来,通过调用 display 实现。 display 是虚函数,则说明简单基类 Window 的对象与派生出的 WindowWithScrollBars对象的显示方式是不一样的。(参见第 34 和 36 条)

现在,假设你期望编写一个函数来打印出当前窗口的名字然后显示这一窗口。下面是错误的实现方法:
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 迭代器函数对象类型适用于传值方式。对于所有其它的类型,都应该遵循本条款中的建议:使用引用常量传参代替传值。
 
需要记住的:
1、尽量以引用常量传参代替传值。因为传引用更高效,且可以避免“截断问题”(slicing problem)。
2、对于内建数据类型、 STL 迭代和函数对象类型,这一规则并不适用,对它们而言传值更恰当。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值