【复读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对象(传值调用)。

cpp 复制代码
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;
};

现在我们进行如下执行:

cpp 复制代码
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 引用传递

cpp 复制代码
bool validateStudent(const Student& s);

首先,这种方法是没有构造函数或者析构函数被调用,因为没有新的对象被创建,只有指针之间的传递;

其次,const修饰是必要的,因为值传递中的形参可是个拷贝出来的新对象,修改也不会影响原对象,其目的就是获取,而不涉及修改,因此要做到同等目标,const的修饰就能保证数据不被修改;

最后,这种方法也能处理 对象切片/截断问题

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

这里可以通过原书例子(我拓展了内容)理解:

有一个基类(Window)与一个派生类(WindowWithScrollBars)实现了图形化窗口系统;

cpp 复制代码
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";
    }
};

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

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

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

cpp 复制代码
WindowsWithScrollBars wwsb;
printNameAndDisplay(wwsb);  //WindowsWithScrollBars对象会被截断

其原因是:

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

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

cpp 复制代码
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的迭代器和函数对象。对它们而言,传值调用往往比较合适

相关推荐
一点媛艺12 分钟前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风16 分钟前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生1 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程2 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
UestcXiye3 小时前
《TCP/IP网络编程》学习笔记 | Chapter 3:地址族与数据序列
c++·计算机网络·ip·tcp
Chrikk3 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*3 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue3 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang