C++中多用引用传递方式替换值传递方式

简介: C++中多用引用传递方式替换值传递方式

1.传值方式


默认情况下,C++以传值方式(by value)传递对象到(或来自)函数。除非你另行指定,否则函数参数都是以实参的副本为初值,而调用端所获得的也是函数返回值的一个副本。这些副本是由对象的拷贝构造函数产生的(原因请参见前面的文章C++中的拷贝构造函数),这可能使得传值方式成为更昂贵的操作。为什么会这样说,请看下面的例子。


1class Person{
 2public:
 3    Person();
 4    virtual ~Person();
 5private:
 6    std::string name;
 7    std::string address;
 8};
 9
10class Student: public Person{
11public:
12    Student();
13    ~Student();
14private:
15    std::string schoolName;
16    std::string schoolAddress;
17};


现在考虑以下代码段,其中调用函数validateStudent(),该函数需要一个Student实参(以传值方式)并返回它是否有效:


1bool validateStudent(Student s);  // 函数声明
2
3Student CurryCoder;
4
5bool platoIsOK = validateStudent(CurryCoder);  // 调用函数,函数以传值方式接收参数


当调用函数validateStudent()时,究竟发生了什么?毫无疑问,Student的拷贝构造函数会被调用,以CurryCoder为蓝本将s进行初始化。同样地,当函数validateStudent()返回具体值时,s会被销毁。因此,对该函数来说,参数的传递成本是一次Student拷贝构造函数的调用,加上一次Student析构函数的调用


但是,请不要忘记Student对象内还有两个string对象(schoolName和schoolAddress)。所以,每次构造一个Student对象也会构造两个string对象。此外,Student对象继承自Person对象,所以每次构造Student对象也会构造一个Person对象。一个Person对象又有两个string对象在其中,因此每一次Person构造动作又需要承担两个string构造动作。最终结果是:以传值方式传递一个Student对象会导致调用一次Student的拷贝构造函数、一次Person拷贝构造函数、四次string拷贝构造函数。同样地,当函数内的那个Student副本被销毁,每一个构造函数调用动作都需要一个对应的析构函数调用动作。因此,以传值方式传递一个Student对象,总体成本是六次构造函数和六次析构函数。


2.传常引用方式


以上的传值方式我们会发现所有对象都确实能被构造和析构,但是那些构造和拷贝动作太多,太麻烦了。可以使用传常引用方式(by reference to const)来回避所有那些构造和析构函数。


1bool validateStudent(const Student& s);  // 函数声明
2
3Student CurryCoder;
4
5bool platoIsOK = validateStudent(CurryCoder);  // 调用函数,函数以传常引用方式接收参数


传常引用方式具有两个优点:第一,这种传递方式效率更高。没有任何构造函数和析构函数被调用,因为没有任何新对象被创建。其中,将引用使用const进行修饰很重要。原先函数validateStudent()以传值方式接收一个Student参数,因此调用者知道它们受保护,所以函数内部不会对传入的Student作任何改变;函数validateStudent()只能对其副本进行修改。现在,Student以传引用方式传递,并将它声明为const是十分必要的,因为如果不这样做的话,调用者会担心函数validateStudent()会不会改变它们传入的那个Student。


第二,以传引用方式传递参数也可以避免对象切割(slicing)问题对象切割如果有一个函数的参数类型是基类,这时一个子类对象被值传递进这个函数时,参数初始化所调用的构造函数是基类的,子类所衍生的特性就全部被"切割"掉了,在这个函数里你就只剩下一个基类对象。如下例所示:


1// 基类
 2class Window{
 3public:
 4    // ... 
 5    std::string name() const;  // 返回窗口名称
 6    virtual void display() const;   // 显示窗口和内容
 7};
 8
 9// 子类
10class WindowWithScrollBars: public Window{
11public:
12    // ...
13    virtual void display() const;
14};


从上面的代码段中可以看出,所有Window对象都带有一个名称,可以通过函数name()获得它。所有窗口都可以显示,可以通过函数display()完成它。display()是一个虚函数,这意味着它的在基类Window对象的显示方式与子类WindowWithScrollBars对象的显示方式不同。现在,假设你希望写一个函数打印窗口名称,然后显示该窗口。下面的代码就是错误的,会产生对象切割问题


1void printNameAndDisplay(Window w){   // 传值方式,参数可能会被切割
2    std::cout << w.name();
3    w.display();
4}


当你调用上面的函数printNameAndDisplay()并传递给它一个子类对象(即WindowWithScrollBars对象),会发生什么呢?


1WindowWithScrollBars wwsb;
2printNameAndDisplay(wwsb);   // 子类对象的特化信息都会被切除


形参w会被构造成为一个基类对象(Window对象),因为参数是以传值方式传递的,从而造成wwsb作为一个子类对象(WindowWithScrollBars对象)的所有特化信息都会被切除。在函数printNameAndDisplay()内不论传递过来的对象原本是什么类型,参数w就像Window对象。因此,在函数printNameAndDisplay()内调用的总是Window::display(),绝不是WindowWithScrollBars::dispaly()。


解决对象切割问题的方法:以传常引用方式传递w,传进来的窗口是什么类型,w就会表现哪种类型。如下所示:


1void printNameAndDisplay(const Window& w){   // 传常引用方式,参数不会被切割
2    std::cout << w.name();
3    w.display();
4}


3.传常引用方式并不能全部替换传值方式



一般来说,C++编译器会把引用作为指针来实现,所以引用传递本质上其实就是指针传递。因此,对于像int这样的内置类型,直接用值传递还是比引用传递要高效。对于STL的迭代器和函数对象,值传递同样也比引用传递高效。
C++由不同的"子语言"组成,每种"子语言"都有自己的高效编程守则,那么在STL和传统的C中,值传递是传递参数更高效的方法。为了遵循这一惯例,STL中的迭代器和函数对象,它们都被设计成值传递更高效,也不用我们去担心对象切割问题。


4.使用值传递还是引用传递与类型大小无关



有些人可能会认为:第一,因为内置类型体积小,所以体积小的对象使用值传递会更高效。这句话并不准确,假如我们定义一个容器类只有一个指针,但这个指针装了许多的对象,那么拷贝这个类的对象就意味着也要拷贝它的指针包含的所有对象,成本也是非常高。


第二,在有些编译器中,内置类型和用户定义类型是被分开处理的。假如你定义一个类,里面只含有一个double数据成员,编译器可能不会把它放在寄存器中。但是,编译器会把同样大小的单个double放进寄存器里,这就会影响速度。如果你不确定编译器到底会怎么做,就使用引用传递。因为引用传递即指针传递,编译器一定会把指针放在寄存器里。


最后,只有对于内置类型、STL的迭代器和函数对象,你才可以认为值传递更高效。对于其它的一切,还是要遵循本文的建议:多用常引用传递,少用值传递


5.总结


(1) 多用常引用传递,少用值传递。引用传递通常更高效,也能避免对象切割问题。

(2) 以上规则不适用于内置类型、STL的迭代器和函数对象。对它们而言,传值方式往往比较适合。

相关文章
|
5月前
|
安全 C++ 开发者
C++一分钟之-函数参数传递:值传递与引用传递
【6月更文挑战第19天】C++中函数参数传递涉及值传递和引用传递。值传递传递实参副本,安全但可能效率低,适合不变对象;引用传递传递实参引用,允许修改,用于高效修改或返回多值。值传递示例显示交换不生效,而引用传递示例实现交换。常量引用则防止意外修改。选择传递方式需考虑效率与安全性。
48 2
|
6月前
|
C++
C++ 默认参数与引用传递:语法、用法及示例
C++ 允许函数参数具有默认值,简化调用。例如,`void myFunction(string country = &quot;Norway&quot;)` 中`country`默认为&quot;Norway&quot;。默认参数仅适用于函数参数,不包括返回值。引用传递是另一种传递方式,函数直接访问变量内存,允许修改原值,提高效率。`void swapNums(int &x, int &y)` 中`x`和`y`为引用参数。了解这些特性可提升代码可读性和性能。
118 0
|
6月前
|
C++
c++关于值传递,指针传递,引用传递这几个方面还会存在误区
c++关于值传递,指针传递,引用传递这几个方面还会存在误区
36 0
|
6月前
|
C++
C++函数与值传递
C++函数与值传递
31 0
|
C++ 计算机视觉 数据格式
C/C++主调函数从被调函数中获取(各种类型)数据内容方式的梳理归纳
C/C++主调函数从被调函数中获取(各种类型)数据内容方式的梳理归纳
215 1
C/C++主调函数从被调函数中获取(各种类型)数据内容方式的梳理归纳
|
存储 程序员 编译器
C与C++的最常用输入输出方式对比
C与C++的最常用输入输出方式对比,IO,scanf,printf,cin,cout,占位符。使用方法,函数声明,代码实例。区别,优缺点。
296 1
C与C++的最常用输入输出方式对比
|
API C语言 C++
C++文件操作的5种方式
C++文件操作的5种方式
155 1
|
C++
C++函数与值传递
C++函数与值传递
74 0
|
编译器 C++
C++函数参数传递的三种方式
C++函数参数传递的三种方式
194 0