自学内容网 自学内容网

【复读EffectiveC++20】条款20:宁以pass-by-reference-to-const替换pass-by-value

条款20:宁以pass-by-reference-to-const替换pass-by-value

这一条款在我的工作中倒是直接感受不深,其原因是我所涉及的技术栈基本为 com ,大量(几乎全部)使用指针,但读到这里也是有种拨开云雾的感觉;

一、名词解释与异同

1、pass-by-value

顾名思义,值传递,函数被调用时,实参的值会被复制一份并传递给对应的形参。
一般情况下,C++默认是以值传递的方式向函数传入或者从函数传出对象。除非另外指定,否则函数参数都会以实际参数值的复件(副本)为初值,这些副本都是由对象的拷贝构造函数产出。

值传递的优点包括数据安全性和简单清晰,但缺点是可能会增加额外的内存消耗,尤其是当处理大数据结构时(即 传递影响效率问题)。

2、pass-by-reference-to-const

引用传递,即是指在调用函数时,将实际参数的指针值传递进去。
也可以理解为同值传递,是把实参的指针当成实参传递进去,在里面使用的时候也是通过指针进行地址数据的获取,修改 (敲重点)等等。

引用传递的主要优点是可以避免在函数调用中不必要的数据复制,特别是在处理大型数据结构时,可以显著提高效率并减少内存使用。

二、存在的问题与解决方法

1、传递影响效率问题

此问题是书中的原例,其核心就是围绕值传递传入时发生的拷贝。
例如,现在有一个继承体系:一个基类(Person)与 一个派生类(Student),并且定义一个函数(validateStudent),参数接受一个Student对象(传值调用)。

class Person {
public:
    Person();
virtual ~Person();
private:
    std::string name;
    std::string address;
};
 
class Student :public Person {
public:
    Student();
    ~Student();
private:
    std::string schoolName;
    std::string schoolAddress;
};

现在我们进行如下执行:

bool validateStudent(Student s);

Student plato;          //定义Student对象
bool platoIsOK  = validateStudent(plato); //传值调用

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

(1)执行6次构造函数
  • plato传入进函数的时候,需要调用1次 Student 的构造函数;
  • Student构造函数执行前需要先构造 Person ,因此还会调用1次 Person 类的构造函数;
  • Person和Student两个类中共有4个 string 成员变量,因此还需要调用 string 的构造函数4次。
(2)执行6次析构函数
  • 当函数执行完成之后,传入 validateStudent 函数中的 plato 副本还需要执行析构函数;
  • 同样,执行析构函数的时候分别要对应执行6次析构函数( Student + Person + 4个 string 成员变量)。

这样的执行是肯定正确的,但不代表是最优的,毕竟,如果函数涉及多次调用,那我们又要执行多少次的构造呢?而且,构造函数如果相当复杂,又是一个巨大的开销。

因此,我们要如何绕过这个问题呢?

2、解决方法

原书也是给出了解决方案的,例如 const 引用传递

bool validateStudent(const Student& s);

首先,这种方法是没有构造函数或者析构函数被调用,因为没有新的对象被创建,只有指针之间的传递;
其次,const修饰是必要的,因为值传递中的形参可是个拷贝出来的新对象,修改也不会影响原对象,其目的就是获取,而不涉及修改,因此要做到同等目标,const的修饰就能保证数据不被修改;
最后,这种方法也能处理 对象切片/截断问题

对象切片/截断:如果将对象直接以传值方式调用,会造成对象的切片/截断问题。这种现象一般发生在函数的参数为基类类型,但是却将派生类对象传递给函数。

这里可以通过原书例子(我拓展了内容)理解:
有一个基类(Window)与一个派生类(WindowWithScrollBars)实现了图形化窗口系统;

class Window
{
public:
    std::string name()const
    {
        return "Window's name\n";
    }

    virtual void display()const
    {
        std::cout << "Window's display\n";
    }
};
 
class WindowsWithScrollBars :public Window
{
public:
    virtual void display()const
    {
        std::cout << "WindowsWithScrollBars's display\n";
    }
};

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

void printNameAndDisplay(Window w) //错误,参数可能会被切割
{
    std::cout << w.name();
    w.display();
}

当使用一个WindowWithScrollBars对象作为参数调用这个函数就会发生 对象切片/截断问题;

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

错误运行结果
其原因是:

  • 参数w将会被构造,它是按值传递的。
  • 所以w作为一个 Window 类实例化的对象,是不会包含派生类 WindowWithScrollBars 的特征的。
  • 在 printNameAndDispay 的调用中,w的行为总是会像 Window 对象一样(因为他是一个Window类的对象),而不管传入函数的参数类型是什么。
  • 特别的,在 printNameAndDisplay 内部对 display 的调用总是会调用 Window::display ,永远不会调用 WindowWithScrollBars::display 。

因此,其正确的方法为使用 const 引用:

void printNameAndDisplay(const Window& w) //很好,参数不会被切割
{
std::cout << w.name();
w.display();
}
 
int main()
{
    WindowsWithScrollBars wwsb;
    printNameAndDisplay(wwsb);  //传入的就是WindowsWithScrollBars类型的对象
    return 0;
}

正确运行结果
当然,说那么好,const 引用 方法也是有条件限制的,而这个界限就是 “ 按值传递是否昂贵的 ”。

举个例子,一些编译器拒绝将只含有一个double数值的对象放入缓存中,却很高兴的为一个赤裸裸的double这么做。当这类事情发生的时候,将这些对象按引用传递会更好,因为编译器会将指针(引用的实现)放入缓存中。

据此,请根据情况自行分析,毕竟这也是C++高性能特点的在不同使用者手中不一样的体现之一 。

三、总结

尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,传值调用往往比较合适


原文地址:https://blog.csdn.net/weixin_51122602/article/details/140579586

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!