1. 按值传递参数会有效率问题

默认情况下,C++向函数传入或者从函数传出对象都是按值传递(pass by value)(从C继承过来的典型特性)。除非你指定其他方式,函数参数会用实际参数值的拷贝进行初始化,函数调用者会获得函数返回值的一份拷贝。这些拷贝由对象的拷贝构造函数生成。这使得按值传递(pass-by-value)变成一项昂贵的操作。举个例子,考虑下面的类继承体系(Item 7):

 class Person {

 public:

 Person(); // parameters omitted for simplicity

 virtual ~Person(); // see Item 7 for why this is virtual

 ...

 private:

 std::string name;

 std::string address;

 };

 class Student: public Person {

 public:

 Student(); // parameters again omitted

 virtual ~Student();

 ...

 private:

 std::string schoolName;

 std::string schoolAddress;

 };

现在考虑下面的代码,在这里我们调用了一个函数,validateStudent,这个函数有一个Student参数(按值),返回值表示验证是否通过:

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

 // by value

 Student plato; // Plato studied under Socrates

 bool platoIsOK = validateStudent(plato); // call the function

当函数被调用时会发生什么?

很清楚,Student拷贝构造函数会被调用,用plato来初始化参数s。同样很清楚的是,当validateStudent函数返回后s会被销毁。所以这个函数参数传递的开销是分别调用了构造函数和析构函数。

但这不是所有的开销。一个Student对象中有两个string对象,所以每次你构建一个Student对象的时候你必须构造两个string对象。Student对象继承自Person对象,所以每次你构建一个Student对象你必须构造一个Person对象。一个Person对象中有两个额外的string对象,所以每个Person构造函数同样需要对两个额外的string进行构造。最后结果是按值传递一个Student对象导致对Student拷贝构造函数的一次调用,对Person拷贝构造函数的一次调用,对stirng拷贝构造函数的四次调用。当Student对象的拷贝被释放时,每个构造函数对应的析构函数要被调用,所以按值传递一个Student对象的总开销是6次构造和6次析构!!

2. 按const引用传递会更高效

这是正确的并且令人满意的行为。毕竟,你需要的是所有对象被可靠的初始化和销毁。并且,如果有一种方法能够绕过这些构造函数和析构函数就再好不过了。这种方法是存在的,就是:按const引用进行传递(pass by reference-to-const

 bool validateStudent(const Student& s);

这种用法更具效率:没有构造函数或者析构函数被调用,因为没有新的对象被创建。在修订后版本的参数声明中,const是很重要的。validataStudent的原始版本有一个按值传递的Studetn参数,调用者会知道对被传递进去的Student参数的任何可能的修改都会被屏蔽掉;validateStudent只是在修改它的一份拷贝。现在Student被按照引用进行传递,将其声明为const同样是必须的,否则调用者就会为传递进去的参数是否被修改而担心。

3. 按const引用传递能避免切片问题

按引用传递参数同样避免了切片(slicing)问题。当一个派生类对象被当作一个基类对象被传递时(按值传递),基类的拷贝构造函数会被调用,“使对象的行为看起来像派生类对象“这个特定的特性被“切掉”了。留给你的只剩下一个基类对象,因为是一个基类的构造函数创建了它。这是你永远不希望看到的。举个例子,假设你正在一些类上进行工作,这些类实现了图形化窗口系统:

 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;

 };

所有的窗口对象都有一个名字,你可以通过name函数来获取它,并且所有的窗口都能被显示出来,你可以通过触发display函数来实现。Display函数为虚函数的事实告诉你基类Windows对象的显示方式同WindowWithScrollBars对象的显示方式是不同的(Item 34和Item 36)。

现在假设你实现了一个函数,先打印窗口的名字然后让窗口显示出来。下面是实现这样一个函数的错误的方式:

 void printNameAndDisplay(Window w) // incorrect! parameter

 { // may be sliced!

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

 w.display();

 }

考虑当你使用一个WindowWithScrollBars对象作为参数调用这个函数会发生什么:

 WindowWithScrollBars wwsb;

 printNameAndDisplay(wwsb);

参数w将会被构造,它是按值传递的,所以w作为一个Window对象,所有让wwsb看起来像一个WIndowWithScrollBars对象的特定信息都会被切除。在printNameAndDispay内部,w的行为总是会像Window对象一样(因为他是一个Window类的对象),而不管传入函数的参数类型是什么。特别的,在printNameAndDisplay内部对display的调用总是会调用Window::display,永远不会调用WindowWithScrollBars::display。

解决切片问题的方法是将w按const引用传递进去(by reference-to-const):

 void printNameAndDisplay(const Window& w) // fine, parameter won’t

 { // be sliced

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

 w.display();

 }

现在w的行为会和传入参数的实际类型一致了。

4. 什么情况下按值传递是合理的

如果你偷看一下C++编译器的底层,你将会发现引用是按照指针来进行实现的,所以按引用传递一些东西就意味着传递一个指针。因此,如果你有一个内建类型的对象(例如int)按值传递比按引用传递效率更高。对于内建类型来说,当你在按值传递和按引用传递之间进行选择时,选择按值传递是合理的。这对于STL中的迭代器和函数对象同样适用,因为按照惯例,它们被设计成按值传递。迭代器和函数对象的设计者有责任留意下面两个问题:高效的拷贝和不用忍受切片问题。(这是一个规则如何被改变的例子,取决于你使用C++的哪一部分 见Item 1。)

5. 并不是对象小就应该按值传递

内建类型占用了很少的内存,所以一些人得出结论:所有这样的小的类型都是按值传递的候选者,即使它们是用户定义的类型。这个原因是靠不住的。因为一个对象占用内存少并不意味这调用它的拷贝构造函数不昂贵。许多对象——这些对象中的大多数STL容器——仅仅包含一个指针,但是拷贝这些对象会拷贝它们指向的所有东西。这可是非常昂贵的操作。

即使是当小对象的拷贝构造函数的调用开销很小时,也会有性能问题。一些编译器对于内建类型和用户自定义类型有不同的对待方式,即使它们有相同的底层表示(underlying representation)。举个例子,一些编译器拒绝将只含有一个double数值的对象放入缓存中,却很高兴的为一个赤裸裸的double这么做。当这类事情发生的时候,将这些对象按引用传递会更好,因为编译器会将指针(引用的实现)放入缓存中。

另外一个小的用户自定义类型不是按值传递的好的候选者的原因是,作为用户自定义类型,它们的大小会发生变化。一个类型现在可能很小但是在将来的发布中可能会变的更大,因为它的内部实现可能发生变化。当你切换到一个不同的C++实现时事情也有可能发生变化。举个例子,标准库的string类型的一些实现比其他实现大6倍。

一般情况下,你能够对“按值传递是不昂贵的”进行合理假设的唯一类型就是内建类型和STL迭代器以及函数对象。对于其它的任何类型,遵循这个条款的建议,优先使用按const引用传递而不是按值传递。

6. 总结

  • 优先使用按const-引用传递而不是按值传递。它更具效率并且能够避免切片问题。
  • 这个规则不适用于内建类型,STL迭代器和函数对象类型。对于它们来说,按值传递通常是合适的。

读书笔记 effctive c++ Item 20 优先使用按const-引用传递(by-reference-to-const)而不是按值传递(by value)的更多相关文章

  1. 读书笔记 effctive c++ Item 52 如果你实现了placement new,你也要实现placement delete

    1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面 ...

  2. 读书笔记 effective c++ Item 21 当你必须返回一个对象的时候,不要尝试返回引用

    1. 问题的提出:要求函数返回对象时,可以返回引用么? 一旦程序员理解了按值传递有可能存在效率问题之后(Item 20),许多人都成了十字军战士,决心清除所有隐藏的按值传递所引起的开销.对纯净的按引用 ...

  3. 读书笔记 effective c++ Item 24 如果函数的所有参数都需要类型转换,将其声明成非成员函数

    1. 将需要隐式类型转换的函数声明为成员函数会出现问题 使类支持隐式转换是一个坏的想法.当然也有例外的情况,最常见的一个例子就是数值类型.举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐 ...

  4. 读书笔记 effective c++ Item 31 把文件之间的编译依赖降到最低

    1. 牵一发而动全身 现在开始进入你的C++程序,你对你的类实现做了一个很小的改动.注意,不是接口,只是实现:一个私有的stuff.然后你需要rebuild你的程序,计算着这个build应该几秒钟就足 ...

  5. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  6. 读书笔记 effective c++ Item 46 如果想进行类型转换,在模板内部定义非成员函数

    1. 问题的引入——将operator*模板化 Item 24中解释了为什么对于所有参数的隐式类型转换,只有非成员函数是合格的,并且使用了一个为Rational 类创建的operator*函数作为实例 ...

  7. 读书笔记 effective c++ Item 28 不要返回指向对象内部数据(internals)的句柄(handles)

    假设你正在操作一个Rectangle类.每个矩形可以通过左上角的点和右下角的点来表示.为了保证一个Rectangle对象尽可能小,你可能决定不把定义矩形范围的点存储在Rectangle类中,而是把它放 ...

  8. 读书笔记 effective c++ Item 5 了解c++默认生成并调用的函数

    1 编译器会默认生成哪些函数  什么时候空类不再是一个空类?答案是用c++处理的空类.如果你自己不声明,编译器会为你声明它们自己版本的拷贝构造函数,拷贝赋值运算符和析构函数,如果你一个构造函数都没有声 ...

  9. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

随机推荐

  1. Grunt之watch详解

    Grunt 之 watch 和 livereload 现在 watch 中已经集成了 livereload ,所以把它们放在一起说明. watch 可以监控特定的文件,在添加文件.修改文件.或者删除文 ...

  2. PHP中文件包含的路径问题

    在程序中当前文件夹下文件路径可以表示为3种:1)绝对路径,2)相对路径,3)直接文件名 例如在/var/www下的a.php:1)/var/www/a.php 2)./a.php 3)a.php 在P ...

  3. C#子窗口与父窗口交互(使用委托和事件)

    目标:在子窗口Form2上单击按钮时向Form1传递一组自定义参数,并显示在父窗口Form1上. 方法:有很多方法,这里只介绍委托和事件的实现方式. 思路:Form2中定义事件,Form1创建Form ...

  4. WebStorm界面出现中文乱码(出现口口口)

    不少刚刚使用WebStorm软件的童鞋,发现在新建一个项目时,如果输入中文,会显示成口口口.这个问题要怎么解决呢... 点一下界面上那个扳手图标(settings),快捷键Ctrl+Alt+S. 2 ...

  5. jmeter接口测试实践

    一.什么是接口测试? 接口测试是测试系统组件间接口的一种测试.接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点.测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻 ...

  6. js架构设计模式——MVVM模式下,ViewModel和View,Model有什么区别

    MVVM模式下,ViewModel和View,Model有什么区别 Model:很简单,就是业务逻辑相关的数据对象,通常从数据库映射而来,我们可以说是与数据库对应的model. View:也很简单,就 ...

  7. js原生设计模式——6复杂对象的构建—Builder建造者模式

    <!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8&qu ...

  8. MonthCalendar控件

    MonthCalendar控件  功能,直接显示月历,

  9. ORACLE获取字符串中数字部分

    ') from dual; select regexp_replace('23456中国3-00=.,45','[^0-9]') from dual;标签:regexp_replace regexp ...

  10. V8编程入门

    本文档介绍了V8引擎的一些关键概念,并提供了例子hello world指引你入门. Hello World 让我们看一个Hello World的示例,它将一个字符串参数作为JavaScript语句,执 ...