深入理解C++引用
深入理解C++引用
本文深入探讨C++引用的各个方面,包括其概念、使用方法、const引用的特性、与指针的关系以及底层实现原理。通过详细的代码示例和解释,帮助读者全面理解C++引用的特性和应用场景。
一、引用的概念和定义
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风",实际都是指同一个人。
引用的使用格式如下:
类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用C语言的一些符号,这里引用和取地址使用了同一个符号&
,大家注意从使用方法角度区分就可以。
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// 引用:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
// 这里取地址,我们看到取到的是同一个地址
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
++a;//只是对a进行自增操作,通过调试却发现a、b、c和d都自增1
return 0;
}
补充(引用的特性):
- (1)引用在定义时必须初始化
- (2)一个变量可以有多个引用,在上述示例中:变量a就有b、c、d三个引用,还可以添加更多
- (3)引用一旦引用一个实体,再不能引用其他实体(引用定义后不能改变指向)
二、引用的使用
引用在实践中主要是用于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
示例一(引用传参)
#include<iostream>
using namespace std;
void Swap(int& rx, int& ry)//减少拷贝提高效率
{ //当对比较大结构体进行引用传参时,能更深刻体会到这一点
int tmp = rx;
rx = ry;
ry = tmp;//改变引用对象时同时改变被引用对象
}
int main()
{
int x = 0, y = 1;
cout << x << ' ' << y << endl;
Swap(x, y);
cout << x << ' ' << y << endl;
return 0;
}
示例二(引用做返回值)
#include <stdlib.h>
struct Zh
{
int top;
int* arr;
};
int& test(struct Zh& ra)//返回值类型为int&
{ //以下代码修改只针对test函数
return ra.arr[ra.top - 1];
}
int main()
{
struct Zh a;
a.top = 0;
a.arr = (int*)malloc(10 * 4);
for (; a.top < 5; a.top++)
{
a.arr[a.top] = a.top;
}
test(a)++;//以下修改后代码报警告和报错的点
return 0;
}
对示例二代码进行修改后报错,分析错误原因(仅展示修改部分的代码):
int test(struct Zh& ra)//将返回值类型修改成int
{
return ra.arr[ra.top - 1];
}
对示例二代码进行修改后报警告,分析报警告的原因(仅展示修改部分的代码):
int& test(struct Zh& ra)//返回值类型为int&
{
int b = ra.arr[ra.top - 1];//创建了一个局部变量b
return b;
}
补充:
引用传参跟指针传参功能是类似的,引用传参相对更方便一些。引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
三、const引用
1.const引用的权限问题
可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
错误示例(对象的访问权限在引用过程中不能放大):
int main()
{
const int a = 10;//const对象,权限比普通对象小
int& ra = a;// 这里的引用是对a访问权限的放大
// 编译报错:“初始化”: 无法从“const int”转换为“int&”
return 0;
}
正确示例(对象的访问权限在引用过程中可以平移和缩小):
int main()
{
const int a = 10;//const对象
int b = 20;//普通对象,权限比const对象大
// 这里的引用是对a和b访问权限的平移
const int& ra = a;
int& rb1 = b;
// 这里的引用是对b访问权限的缩小
const int& rb2 = b;
return 0;
}
2.const引用可以存储常属性对象
错误示例(普通引用不能存储常属性对象):
int main()
{
int a = 10;
double d = 12.34;
int& ra = 30;
// int& 类型存不了常量
int& rb = a * 3;
// a * 3 的结果保存在一个临时对象中,临时对象具有常属性
// int& 类型存不了常属性的对象
int& rd = d;
//d要先转换成int类型,在类型转换中会产生临时对象存储中间值
return 0;
}
上述代码中类似 int& rb = a * 3; double d = 12.34; int& rd = d;
这样的场景下,a * 3的结果保存在一个临时对象中, int& rd = d
也是类似,在类型转换中会产生临时对象存储中间值,也就是说,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用(const引用)才可以。
所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
正确使用(使用const引用存储常属性对象):
int main()
{
int a = 10;
double d = 12.34;
const int& ra = 30;
const int& rb = a * 3;
const int& rd = d;
return 0;
}
补充(临时对象产生的情形):
- (1)产生运算的表达式结果存储在临时对象
- (2)在类型转换过程中会产生临时对象存储中间值
- (3)除返回值类型为指针类型或引用类型之外,其它返回类型(void除外)在返回过程中都会创建临时对象
四、指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
- (1)语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
- (2)引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- (3)引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
- (4)引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
- (5)sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
- (6)指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
五、引用的底层原理
int main()
{
int a = 5;
int& ra = a;
int b = 10;
int* pb = &b;
return 0;
}
这段代码的汇编指令层:
lea rax,[a]
mov qword ptr [ra],rax
这段汇编层指令的大概意思就是把先把a的地址存到寄存器rax中,再把rax中存放的地址存到ra中。我们可以发现引用和指针在汇编指令层的实现过程是一模一样的。
于是得出结论:在上层语法层面,引用和指针是存在区别的;但在汇编指令层(底层),根本就没有引用的概念,引用的实现跟指针一模一样。