Effective C++条款20:设计与声明——宁以pass-by-reference-to-const替换pass-by-value

一、传值调用会造成代码执行效率低

  • 传值调用为什么会造成代码执行效率低?因为传值调用的时候,是传递对象的一个副本,因此是调用对象的拷贝构造函数将对象复制一份,然后传递给函数。效率比较低

  • 下面来看一个例子:有一个基类(Person)与一个派生类(Student),并且定义一个函数(validateStudent),参数接受一个Student对象(传值调用)
 
  1. class Person

  2. {

  3. public:

  4. Person();

  5. virtual ~Person();

  6. private:

  7. std::string name;

  8. std::string address;

  9. };

  10.  
  11. class Student :public Person

  12. {

  13. public:

  14. Student();

  15. ~Student();

  16. private:

  17. std::string schoolName;

  18. std::string schoolAddress;

  19. };

  20.  
  21. bool validateStudent(Student s);

  • 现在我们有如下的代码:
 
  1. int main()

  2. {

  3. Student plato; //定义Student对象

  4. validateStudent(plato); //传值调用

  5. return 0;

  6. }

  • 现在我们来考虑一下这个传值调用的过程一共发生了哪些事情:
    • ①执行6次构造函数:plato传入进函数的时候,需要调用1次Student的构造函数,Student构造函数执行前需要先构造Person ,因此还会调用1次Person类的构造函数。Person和Student两个类中共有4个string成员变量,因此还需要调用string的构造函数4次
    • ②执行6次析构函数:当函数执行完成之后,传入validateStudent函数中的plato副本还需要执行析构函数,那么执行析构函数的时候分别要对应执行6次析构函数(Student+Person+4个string成员变量)

替代方案:以const引用传递调用

  • 一种替代传值调用的方式是将对象以const引用的方式传递给函数:
    • const:如果不想对象在函数被修改,那么就以const修饰
    • 引用:引用在编译器底层为指针形式。因此使用引用把对象传递给函数,是直接将对象传递给函数,而不是将对象的副本传递给函数(避免了构造函数的调用以及析构函数的调用)
  • 现在我们修改上面的validateStudent函数:
 
  1. bool validateStudent(const Student& s);

  2.  
  3. int main()

  4. {

  5. Student plato; //定义Student对象

  6. validateStudent(plato); //const引用调用

  7. return 0;

  8. }

二、传值调用会造成对象切割/截断问题

  • 对象切割/截断:如果将对象直接以传值方式调用,会造成对象的切割/截断问题。这种现象一般发生在函数的参数为基类类型,但是却将派生类对象传递给函数
  • 关于对象截断的例子还可以参阅这篇文章中的标题七:h6ttps://blog.csdn.net/qq_41453285/article/details/10310043
  • 下面来看一个例子:有一个基类(Window)与一个派生类(WindowWithScrollBars),并且定义一个函数(printNameAndDisplay),参数接受一个Window对象(传值调用)
 
  1. class Window

  2. {

  3. public:

  4. std::string name()const;

  5. virtual void display()const;

  6. };

  7.  
  8. class WindowsWithScrollBars :public Window

  9. {

  10. public:

  11. virtual void display()const;

  12. };

  13.  
  14. void printNameAndDisplay(Window w)

  15. {

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

  17. w.display();

  18. }

  • 现在我们有如下的代码:
 
  1. int main()

  2. {

  3. WindowsWithScrollBars wwsb;

  4. printNameAndDisplay(wwsb); //WindowsWithScrollBars对象会被截断

  5. return 0;

  6. }

  • 现在我们来考虑一下这个传值调用的过程一共发生了哪些事情:
    • 因为printNameAndDisplay函数的参数为Window类型,所以即使我们传入的是WindowsWithScrollBars类型的对象,那么这个对象会被截断,只取WindowsWithScrollBars对象中属于基类(Window)的内容,然后传递给函数
    • 因此被截断之后,不论如何调用,printNameAndDisplay函数中调用的display函数总是Window的display虚函数,不会是WindowsWithScrollBars中的display虚函数。因为多态只会发生在基类指针/引用指向于派生类的情况下,此处没有指针/引用

替代方案:以const引用传递调用

  • 如果我们将上述函数的参数改为const引用方式的话,那么传入函数的对象将与传入的对象类型有关(此时没有拷贝构造的流程,调用的会是WindowsWithScrollBars的display函数
  • 现在我们修改上面的validateStudent函数:
 
  1. void printNameAndDisplay(const Window& w)

  2. {

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

  4. w.display();

  5. }

  6.  
  7. int main()

  8. {

  9. WindowsWithScrollBars wwsb;

  10. printNameAndDisplay(wwsb); //传入的就是WindowsWithScrollBars类型的对象

  11. return 0;

  12. }

三、内置类型建议传值调用

  • C++编译器的底层,是把引用以指针的形式实现,因此引用传递就是指针的传递
  • 如果你使用的对象属于内置类型(例如int),内置类型都相当小,传值调用往往比引用传递的效率高些
  • 这条规则在STL的迭代器和函数对象中都被广泛引用

四、总结

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,传值调用往往比较合适
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值