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

小编 2026-06-22 阅读:1447 评论:0
1. 按值传递参数会有效率问题默认情况下,C++向函数传入或者从函数传出对象都是按值传递(pas...

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

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

 1 class Person { 2  3 public: 4  5 Person(); // parameters omitted for simplicity 6  7 virtual ~Person(); // see Item 7 for why this is virtual 8  9 ...10 11 private:12 13 std::string name;14 15 std::string address;16 17 };18 19 class Student: public Person {20 21 public:22 23 Student(); // parameters again omitted24 25 virtual ~Student();26 27 ...28 29 private:30 31 std::string schoolName;32 33 std::string schoolAddress;34 35 };

 

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

1 bool validateStudent(Student s); // function taking a Student2 3 // by value4 5 Student plato; // Plato studied under Socrates6 7 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

1 bool validateStudent(const Student& s);

 

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

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

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

 1 class Window { 2  3 public: 4  5 ... 6  7 std::string name() const; // return name of window 8  9 virtual void display() const; // draw window and contents10 11 };12 13 class WindowWithScrollBars: public Window {14 15 public:16 17 ...18 19 virtual void display() const;20 21 };

 

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

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

1 void printNameAndDisplay(Window w) // incorrect! parameter2 3 { // may be sliced!4 5 std::cout << w.name();6 7 w.display();8 9 }

 

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

1 WindowWithScrollBars wwsb;2 3 printNameAndDisplay(wwsb);

 

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

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

1 void printNameAndDisplay(const Window& w) // fine, parameter won’t2 3 { // be sliced4 5 std::cout << w.name();6 7 w.display();8 9 }

 

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

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

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

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

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

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

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

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

6. 总结

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


作者:HarlanC

博客地址:http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表