C++函数参数传递的一大利器——引用(左值)

来源:互联网 发布:孟非睡女嘉宾 知乎 编辑:程序博客网 时间:2024/05/22 03:57

前言:

本文只浅显地介绍了三种函数参数传递的方式及其优劣,主要篇幅是介绍引用及引用作为参数传递的优点。在另一篇博文中将会用本文中的主要代码反汇编为汇编代码,从编译器处理函数调用的层面更深入地探讨三种函数参数传递的区别。


C++函数参数传递方式:

一、按值传递
#include <iostream>using namespace std;int Add (int a, int b){    return a+b;}int main (void){    int x = 1, y = 2;    cout << Add(x,y) << endl;    return 0;}

像例子中主调函数直接把值(实参)传递给被调函数(形参)的形式就是按值传递,这是最为简单常见的函数参数传递方式。


但如果被调函数要修改主调函数传过去的实参,这种方式就不奏效了:

#include <iostream>using namespace std;void Swap (int a, int b){    int temp;    temp = a;    a = b;    b = temp;}int main (void){    int x = 10, y = 20;    Swap(x, y);    cout << "x = " << x << "," << "y = " << y << endl;    return 0;}



Swap函数的作用是交换xy的值,但调用后并没有交换。

为什么呢?我们添加一些代码看看:

#include <iostream>using namespace std;void Swap (int a, int b){    cout << "形参地址:\n";    cout << "a is at: " << &a << endl;    cout << "b is at: " << &b << endl;    int temp;    temp = a;    a = b;    b = temp;}int main (void){    int x = 10, y = 20;    cout << "实参地址:\n";    cout << "x is at: " << &x << endl;    cout << "y is at: " << &y << endl;    Swap(x, y);    cout << "x = " << x << "," << "y = " << y << endl;    return 0;}



从运行结果不难看出,Swap函数被调用时,会为形参分别创建新的名为ab的变量,并且将主调函数中实参xy的值分别赋给ab,这就相当于被调函数Swap为实参创建了一个副本(自然会为新变量分配内存,但这块内存与用于存储变量的普通内存不太一样)作为Swap的形参,当被调函数完成时副本即被销毁,因此对按值传递参数的方式并不会对主调函数传递过去的实参产生任何影响(Swap使用的是实参的副本,而不是原来的数据)。所以当我们需要被调函数修改实参的时候就需要别的函数参数传递方式。


二、按指针传递

顾名思义,按指针传递就是主调函数把指针传递给被调函数的形参,这种函数参数传递方式可以做到按值传递做不到的工作:让被调函数修改实参的值。

#include <iostream>using namespace std;void Swap (int* a, int* b){    int temp;    temp = *a;    *a = *b;    *b = temp;}int main (void){    int x = 10, y = 20;    int* p1 = &x;    int* p2 = &y;    Swap(p1, p2);    cout << "x = " << x << "," << "y = " << y << endl;    return 0;}


为什么按指针传递可以修改实参呢?看下一段代码:

#include <iostream>using namespace std;void Swap (int* a, int* b){    cout << "形参地址:\n";    cout << "a is at: " << a << endl;    cout << "b is at: " << b << endl;    int temp;    temp = *a;    *a = *b;    *b = temp;}int main (void){    int x = 10, y = 20;    int* p1 = &x;    int* p2 = &y;    cout << "x is at: " << &x << endl;    cout << "y is at: " << &y << endl;    cout << "实参地址:\n";    cout << "p1 is at: " << p1 << endl;    cout << "p2 is at: " << p2 << endl;    Swap(p1, p2);    cout << "x = " << x << "," << "y = " << y << endl;    return 0;}


从运行结果可以看到,形参ab的地址分别与实参p1p2的值相同。由于指针p1p2分别指向要交换数据的xy(即p1p2分别存放xy中存放数值的地址),因此ab也分别指向xy,因此形参通过获取到实参传递过来的目标地址可以访问xy,自然也就能修改其值了。实际上按指针传递并没有这么简单,这个在另一篇博文中会深入探讨。


乍一看前面两种函数参数传递方式已经能够满足我们对编写代码的需求,事实上也确实如此,C语言就只有这两种函数参数传递方式。对于例子中传递基本类型的参数还看不出什么,但如果是传递复合类型的参数如结构体、类等等呢?按值传递会创建副本,对于占用内存大的参数显然不是一个好的选择,那就只剩下按指针传递了,但指针是C/C++中最为强大也最难控制的一种类型,如果在某些情况下需要用到多级指针,那么程序的可读性和鲁棒性就会大打折扣,并且对不熟悉指针的程序员来说极易造成程序安全问题(比如内存泄漏)。C++为此提供了另一种强大实用、较易理解又相对安全的类型——引用,来传递参数。


三、按引用传递
#include <iostream>using namespace std;void Swap (int& a, int& b){    int temp;    cout << "a is at: " << &a << endl;    cout << "b is at: " << &b << endl;    temp = a;    a = b;    b = temp;}int main (void){    int x = 10, y = 20;    cout << "x is at: " << &x << endl;    cout << "y is at: " << &y << endl;    Swap(x, y);    cout << "交换后:x = " << x << ",y = " << y << endl;    cout << "x is at: " << &x << endl;    cout << "y is at: " << &y << endl;    return 0;}


从运行结果看到,实参和形参的地址都是相同的,这点与按指针传递相同,但不代表两者本质上一样。


左值引用

定义:

引用是已定义的变量的别名(另一个名称)。

特点:

1.必须在创建时初始化
int a = 10;int& b = a;

诸如int& b = 10; int& b;都是非法声明。其中,&不是地址运算符,而是类型标识符的一部分,就像int*指的是指向int的指针一样,int&指的是指向int的引用。

2.引用变量一旦与某一变量关联起来,就会从一而终,不会再与其它变量关联。
#include <iostream>using namespace std;int main (void){    int a = 10, b = 20;    int& x = a;    cout << "a = " << a << endl;    cout << "b = " << b << endl;    cout << "x = " << x << endl;    cout << "a is at: " << &a << endl;    cout << "b is at: " << &b << endl;    cout << "x is at: " << &x << endl;    cout << endl;    x = b;    cout << "执行x = b之后:" << endl;    cout << "a = " << a << endl;    cout << "b = " << b << endl;    cout << "x = " << x << endl;    cout << "a is at: " << &a << endl;    cout << "b is at: " << &b << endl;    cout << "x is at: " << &x << endl;    return 0;}


从程序运行结果可以看到,一旦x被声明为a的引用,就会一直与a关联(引用变量的整个生命周期都是如此),试图通过x = b来将x修改为b的引用也无法成功(即使通过指针来修改也无法成功),x的地址与a的地址一直相同。简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。

3.不能声明诸如数组的引用

这个特点的具体原因需要牵涉到数组与指针的关系以及计算机对数组的寻址问题,本文不深入探讨。

总结:

当我们写下int x = 10;这行代码时,系统将为变量x分配内存并将10存入这块内存,但本质上系统开辟内存是为了int型数据,x只不过是这块内存的名字,x本身并不占空间。而对于引用变量来说,它也只不过是变量名的一个别名,本身也不占内存。但与普通变量的区别在于引用变量具有依附性,所以在声明时就要进行初始化让其依附于一个变量,并在它的整个生命周期一直依附于这个变量,变量消亡引用也就没有了存在的意义,随之消亡。


引用与指针:

对指针有一定了解的人从引用的各个特点以及将引用作为参数传递的结果不难看出,引用与const指针在这方面非常相似。也就是说,int& x = a;实际上是int* const p = &a;这行代码的伪装表示。这并不代表两者完全相同,实际上在作为函数参数传递时引用的效率要比const指针更高,原因会在另一篇博文深入探讨。

为什么引用更为安全实用:

1.引用作为参数传递可以任意选择是否影响实参

如果我们需要形参影响实参,按照普通引用来定义实参即可;

如果我们不想引用修改是实参也可以,只需要声明常引用。

例如下面的函数就是非法的,编译器会报错:

void Swap (const int& a, const int& b){    int temp;    temp = a;    a = b;    b = a;}

2.临时变量、引用参数和const

如果实参与引用参数不匹配,C++将生成临时变量。仅当参数为const引用时,C++才允许这样做。

#include <iostream>using namespace std;double refcube (const double& ra){    cout << "ra is at: " << &ra << endl;    return ra*ra*ra;}int main (void){    double side = 3.0;    double *pd = &side;    double &rd = side;    long edge = 5L;    double lens[4] = {2.0, 5.0, 10.0, 12.0};    double c1 = refcube(side);    cout << "c1 = " << c1 << endl << "side is at: " << &side << endl;    double c2 = refcube(lens[2]);    cout << "c2 = " << c2 << endl << "lens[2] is at: " << &lens[2] << endl;    double c3 = refcube(rd);    cout << "c3 = " << c3 << endl << "rd is at: " << rd << endl;    double c4 = refcube(*pd);    cout << "c4 = " << c4 << endl << "pd is at: " << pd << endl;    double c5 = refcube(edge);    cout << "c5 = " << c5 << endl << "edge is at: " << &edge << endl;    double c6 = refcube(7.0);    cout << "c6 = " << c6 << endl;    double c7 = refcube(side+10.0);    cout << "c7 = " << c7 << endl;    return 0;}



参数sidelens[2]rd*pd都是有名称的、double类型的数据对象,因此可以为其创建引用,而不需临时变量。然而,edge虽然是变量,类型却不正确,double引用不能指向long。另一方面,参数7.0side+10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器就可以随意将其删除。虽然从程序中我们无法得到后两个函数调用中实参的值,但从ra的地址可以看出其存在于栈内存中,因此后两个函数中实参的值肯定与ra不同(原因在另一篇博文中会讲)。

但如果接受引用参数的函数的意图是修改作为参数传递的变量,编译器就会禁止创建临时变量。而refcube函数的目的只是使用传递的值,不是修改它们,因此临时变量不会造成任何不利影响,反而会使函数在可处理的参数种类方面更通用。因此,如果将引用指定为constC++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。


3.将引用用于结构和类

引用非常适合用于结构和类。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。

4.返回引用更为高效

传统返回机制与按值传递参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置(只是从概念上),而调用程序将使用这个值。如果需要返回的参数是结构体,那么传统返回机制将会把整个结构复制到一个临时位置,再由调用程序使用(其实就是复制)这个复制品,但返回引用的话就会直接将结构体复制给调用程序使用,效率更高。但要注意避免返回函数终止时不再存在的数据类型的引用。



























0 0
原创粉丝点击