前言:
相信大家在学习C语言的时候,最头疼的就是指针,经常会碰到一级指针、二级指针,这些指针使用起来,稍有不慎就会等导致程序崩溃,为了让广大程序员少掉点头发,C++中提出了 引用这一概念。当然,在C++的代码中,仍然可以兼容C语言的指针。
一、引用的概念
在语法上引用不是新定义一个变量,而是给已存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块空间。例如:鲁迅和周树人都是同一个人。
类型& 引用变量名(对象名)= 引用实体
int main() { int a = 0; int& b = a;//定义引用类型,b是a的引用 int& c = a; return 0;
通过汇编指令实现的角度看,引用底层是类似指针的方式实现的,如下图所示:
二、共用同一块空间验证
int main() { int a = 0; int& b = a; int& c = a; cout << &a << endl; cout << &b << endl; cout << &c << endl; return 0; }
通过上面打印的结果可以看出,引用变量的地址和引用实体的地址是一样的,说明引用其实就是给同一块内存空间上取别名。
int main() { int a = 10; int& b = a; int& c = a; b++; cout << a << endl; cout << b << endl; cout << c << endl; return 0; }
上面代码,对其中一个引用变量++,其他三个变量的值也发生了改变,这也能说明引用是给同一块内存空间取别名。
三、引用的特性
3.1 引用在定义时必须初始化
int main() { int a = 10; int& d;//不能这样 return 0; }
上面的代码是不被允许的,因为在定义引用变量b的时候,没有初始化。
3.2 一个变量可以有多个引用
int main() { int a = 10; int& b = a; int& c = a; return 0; }
上面代码中,b和c都是a变量的引用。这一点很容易理解,就像同一个人可以有多个外号一样,一个变量也可以同时有多个引用。
3.3 引用不能改变
引用一旦引用一个实体,不能再引用其它实体。
int main() { int a = 10; int num = 100; int& b = a; int& c = a; b = num;//这里是把num的值赋给a,并不是让b变成num的引用 cout << "变量a的地址:" << &a << endl; cout << "引用b的地址:" << &b << endl; cout << "变量num的地址:" << &num << endl; return 0; }
上面代码中的b = num,是把num变量的值赋值给b引用的实体,并不是让b变成num的引用。通过打印地址可以证明这一点,引用b的地址和变量a的地址一样,说明b任然是a的引用。这类似于终身制,引用一旦和实体绑定就不能再分离。
这里还需要注意一点:给引用b赋值,引用实体a也是会跟着发生改变的,就像周树人吃了午饭,那鲁迅必定也吃了,不可能出现周树人吃了午饭,而鲁迅还饿肚子的情况。
四、引用的使用场景
4.1 做参数
引用做参数有以下两个方面的意义:
- 做输出型参数,即要求形参的改变可以影响实参
- 提高效率,自定义类型传参,用引用可以避免拷贝构造,尤其是大对象和深拷贝对象(后续文章会讲到)
交换两个整型变量:
void Swap(int& num1, int& num2) { int tmp = num1; num1 = num2; num2 = tmp; } int main() { int a = 10; int b = 11; cout << "a:" << a << " " << "b:" << b << endl; Swap(a, b); cout << "a:" << a << " " << "b:" << b << endl; return 0; }
上面代码,利用引用做参数实现了两个数的交换,以前交换两个数的值,需要进行值传递,也就是实参去两个数的地址,形参用指针来接受。而用引用做参数后,实参无需再传递地址,形参num1是变量a的引用,和a表示同一块内存空间,形参num2是变量b的引用,和b表示同一块空间,因此在函数体内交换num1和num2实际上就是交换a和b。
交换两个指针变量:
void Swap(int*& p1, int*& p2) { int* tmp = p1; p1 = p2; p2 = tmp; } int main() { int a = 10; int b = 11; int* pa = &a; int* pb = &b; cout << "pa:" << pa << " " << "pb:" << pb << endl; Swap(pa, pb); cout << "pa:" << pa << " " << "pb:" << pb << endl; return 0; }
如果用C语言来实现交换两个指针变量,实参需要传递指针变量的地址,那形参就需要用二级指针来接收,这显然十分繁琐,且容易出错。有了引用之后,实参直接传递指针变量即可,形参用指针类型的引用(指针也是一种类型)。
链表新玩法:
之前用C语言实现的链表,在进行尾插、头插、头删等操作的时候,因为可能会涉及到对头节点的修改,因此在传参的时候,用的是指向头节点的指针的地址,也就是一个二级指针,如下:
SLTNode* node;//创建一个链表,其实只是定义了一个头节点 SLTPushBack(&node);//调用尾插函数,传递的是头指针的地址 void SLTPushBack(SLTNode** pphead,SLTDataType x)//函数原型
在有了引用之后,可以对代码做如下修改:
SLTNode* node;//创建一个链表,其实只是定义了一个头节点 SLTPushBack(node);//调用尾插函数,直接传递头指针 void SLTPushBack(SLTNode*& pphead,SLTDataType x)//函数原型
还可以对结构体指针类型进行重定义,像下面这样:
typedef struct SListNode { SLTDataType data;//数据域 struct SListNode* next;//指针域 }SLTNode, *PSLTNode;//对结构体指针类型进行重定义 PSLTNode node;//创建一个链表,其实只是定义了一个头节点 SLTPushBack(node);//调用尾插函数,直接传递头指针 void SLTPushBack(PSLTNode& pphead,SLTDataType x)//函数原型
很多数据结构书上都是采用第三种写法,导致很多新手朋友学起来比较蒙,其本质上就是利用了C++中引用这一概念。
4.2 做返回值
先来回顾下,普通的传值返回。
int add(int x, int y) { int sum = x + y; return sum; } int main() { int a = 5; int b = 4; int ret = add(a, b); return 0; }
上面代码中的add函数,实现了一个简单的两数求和,要将求和结果sum返回给调用它的地方,这里采用的是传值返回,由于sum是函数中的一个局部变量,存储在当前函数的栈帧中,随着函数调用结束栈帧销毁,sum也会随之灰飞烟灭。因此,对于这种传值返回,会生成一个临时的中间变量,用来存储返回值,在返回值比较小的情况下,这个临时的中间变量一般就是寄存器,下面通过调试来验证:
不仅函数中的普通局部变量在传值返回的时候会创建临时的中间变量,函数中static修饰的静态变量,虽然存储在内存中的静态区,不会随着函数调用结束而销毁,但是在传值返回的时候,同样会创建一个临时的中间变量,以下面的代码为例:
int Text() { static int a = 10; return a; } int main() { int ret = Text(); return 0; }