C++ 引用
一、引用
1.1 引用是什么
想必大家都读过四大名著之一的《水浒传》,小说中每个英雄除了有自己本身的名字以外,根据人物外表性格还有许多绰号。比如:宋江又叫:及时雨、呼保义、孝义黑三郎、宋公明、宋押司。李逵又叫铁牛、黑旋风。C++引入了引用这个概念,引用不是新定义一个变量,而是给已存在变量取了一个别名。在语法层面:编译器不会为引用变量开辟内存空间,它和引用的变量共用同一块内存空间
1.2 引用的用法
用法:引用对象类型& 别名 = 引用对象;
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int& alias_x = x; // 引用对象x为int类型,所以引用类型也为int
cout << "&x = " << &x << endl;
cout << "&alias_x = " << &alias_x << endl;
alias_x = 20;
cout << "x= " << x << endl;
cout << "alias_x= " << alias_x << endl;
return 0;
}
输出:
&x = 009BFBBC
&alias_x = 009BFBBC
x= 20
alias_x= 20
我们可以看到x
与alias_x
的内存地址相同,修改x
就是修改alias_x
,修改alias_x
就是修改x
1.3 引用的特性
1.3.1 引用在定义时必须初始化
引用语法层面是某个对象的别名,所以在定义时必须初始化,不能引用一个未知的对象
int x = 10;
int& alias_x; // 错误,引用在定义时必须初始化,不能引用未知对象
int& alias_x = nullptr; // 错误,不能引用一个空对象
int& alias_x = x; // 正确
1.3.2 一个对象可以有多个引用
水浒传中好汉可以有多个绰号,一个对象可以有多个引用
int x = 10;
int& alias_x1 = x;
int& alias_x2 = x;
int& alias_x3 = x;
1.3.3 一旦引用一个对象,再不能改为引用其他对象
引用是一个非常专一的“人”,一旦引用了某个对象,就不能在改为引用其他对象了
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int y = 20;
int& alias_x = x;
cout << "&x = " << &x << endl;
cout << "&y = " << &y << endl;
cout << "&alias_x = " << &alias_x << endl;
cout << "alias = " << alias_x << endl;
alias_x = y; // 一旦引用一个对象,在不能引用其他对象,这里是将y的值赋值给alias_x,而不是引用y
cout << "&x = " << &x << endl;
cout << "&y = " << &y << endl;
cout << "&alias_x = " << &alias_x << endl;
cout << "alias = " << alias_x << endl;
return 0;
}
输出
&x = 0058FA08
&y = 0058F9FC
&alias_x = 0058FA08
alias = 10
&x = 0058FA08
&y = 0058F9FC
&alias_x = 0058FA08
alias = 20
可以发现alias_x地址并未发生改变,只是alias_x值变为20
1.3.4 引用类型必须和引用对象的类型相同
1.4 引用的权限
#include <iostream>
int main()
{
const int x = 10;
//int& alias_x = x; 权限放大,编译错误,x 为常变量,而alias_x 为变量
const int& alias_x = x; //权限不变可以,alias_x与y都为常变量 正确
int y = 10;
const int& alias_y = y; // 权限缩小,y为变量,而alias_y为常变量 正确
}
1.5 引用的本质
引用的本质就是指针,引用一个对象其实就是存储这个对象的内存地址,当要使用时编译器自动对指针解引用访问对应对象,所以使用引用会开辟内存空间存放对象地址,32位系统下占4字节,64位系统下占8字节
二、引用的应用场景
2.1 引用做函数形参
2.1.1 形参可以改变实参
当函数内形参的改变实参也改变时,可以用引用
void swap1(int x,int y)
{
int tmp = x;
x = y;
y = tmp;
}
void swap2(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void swap3(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
swap1中函数内x与y交换,函数外的实参并没有发生改变,因为形参只是实参一份临时拷贝
swap2中形参px、py存放的是实参的地址,对形参修改会改变外面的实参
swap3中使用了引用,引用的本质就是指针,等同于swap2
2.1.2 提高效率
当函数的形参变量类型很大时,直接传值调用,形参是实参的临时拷贝,效率很低。可以使用引用
#define MAXSIZE 10000 //数组大小
typedef int ElemType; //元素类型重定义,以后元素类型发生改变只用改这里
typedef struct SeqList
{
ElemType data[MAXSIZE];
int length; //顺序表当前长度(已存放元素个数)
}SeqList;
void Print1(SeqList s)
{
for (int i = 0; i < s.length; i++)
{
printf("%d ", s.data[i]);
}
}
void Print2(SeqList& s)
{
for (int i = 0; i < s.length; i++)
{
printf("%d ", s.data[i]);
}
}
调用函数Print1时,需要将SeqList类型的实参拷贝给形参,而SeqList类型大小为40004字节,开销很大,效率很低
调用函数Print2时,形参本质是指针
2.2 引用做函数返回值
当函数的返回值类型很大时,直接传值返回,实际返回的是返回值的临时拷贝,效率很低。可以使用引用
#define MAXSIZE 10000 //数组大小
typedef int ElemType; //元素类型重定义,以后元素类型发生改变只用改这里
typedef struct SeqList
{
ElemType data[MAXSIZE];
int length; //顺序表当前长度(已存放元素个数)
}SeqList;
SeqList s; // 全局变量
SeqList Test1()
{
return s;
}
SeqList& Test2(SeqList& s)
{
return s;
}
函数Test1返回时,先将返回值拷贝给临时变量,再将临时变量返回,而SeqList类型大小为40004字节,开销很大,效率很低
函数Test2返回时,返回引用,引用本质是指针,只需要将指针拷贝给临时变量,再将临时变量返回
三、引用的陷阱
引用做为函数返回值时,不要返回函数内的局部变量。局部变量出了函数作用域生命周期就结束了,虽然有的编译器不会将这部分空间清0,但是这部分空间已经不属于我们了,在尝试访问或修改这部分空间属于非法访问内存
int& Add(int x, int y)
{
int result = x + y;
return result;
}
int main()
{
int& ret = Add(5, 5);
cout << "ret = " << ret << endl;
Add(1, 1);
cout << "ret = " << ret << endl;
return 0;
}
输出
ret = 10
ret = 2
add函数内的result是局部变量,出了函数作用域生命周期结束,返回result并保存在ret中已经属于非法访问内存了。测试环境中编译器并没有将这部分空间清0,所以再次调用函数后,ret值变为了2
四、引用与指针对比
4.1 相同点
●引用的本质就是指针,只是编译器对引用做了封装而已
● 引用与指针所占空间大小一样
4.2 不同点
●引用在定义时必须初始化,指针没有要求
●一旦引用一个对象,再不能改为引用其他对象,而指针可以随时指向一个同类型对象
●不能引用NULL,但有NULL指针
● 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数
● 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
● 有多级指针,但是没有多级引用
●访问对象方式不同,指针需要显式解引用,引用编译器替我们解引用处理