C++引用深度详解
- 前言
- [1. 引用的本质与核心特性](#1. 引用的本质与核心特性)
-
- [1.1 引用概念](#1.1 引用概念)
- [1.2 核心特性](#1.2 核心特性)
- [2. 常引用与权限控制](#2. 常引用与权限控制)
-
- [2.1 权限传递规则](#2.1 权限传递规则)
- [2.2 常量引用](#2.2 常量引用)
- [2.3 临时变量保护](#2.3 临时变量保护)
-
- [1. 样例](#1. 样例)
- [2. 样例](#2. 样例)
- [3. 测试](#3. 测试)
- 三、引用使用场景分析
- [4. 性能对比实验](#4. 性能对比实验)
-
- [4.1 参数传递效率](#4.1 参数传递效率)
- [4.2 返回效率对比](#4.2 返回效率对比)
- [5. 引用与指针的终极对比](#5. 引用与指针的终极对比)
-
- [5.1 底层实现](#5.1 底层实现)
- [5.2 特性对比表](#5.2 特性对比表)
- [6. 高级应用技巧](#6. 高级应用技巧)
-
- [6.1 链式操作](#6.1 链式操作)
- [7. 总结引用要点](#7. 总结引用要点)
- [8. 最佳实践指南](#8. 最佳实践指南)
前言
本文深度探索引用的各种用法和特性。介绍引用的语法,核心特性,引用的权限控制,常引用以及引用的各种使用场景。
1. 引用的本质与核心特性
1.1 引用概念
**引用(Reference)**是C++引入的重要特性,是 C++ 中的一种数据类型。
从语法层面讲,引用是变量的别名 。与指针不同,引用在语法层面不开辟新空间,而是与原变量共享内存地址。
引用不会创建新的对象,只是创建另一个访问现有对象的方式,引用类型变量是已有变量的别名。
引用在语法上与指针类似,但其语义和使用方式不同。
我们创建一个变量,其实就是对一块内存空间
取名字
而创建引用类型对象,就是对已有的一块空间取第二个名字。
两个名字代表的是同一块空间。
cpp
int main() {
int a = 10;
int& ra = a; // ra是a的别名
ra = 20; // 修改ra等同于修改a
cout << a; // 输出20
}
![](https://i-blog.csdnimg.cn/direct/8525e63394a24237a0ffd5b53a78ff77.png)
可以看到:
- 对ra进行操作,也就是对a进行操作。
- 变量
ra
和ra
具有相同的地址。
1.2 核心特性
特性 | 说明 | 示例验证 |
---|---|---|
必须初始化 | 定义时必须绑定实体 | int& r; 编译错误 |
不可重绑定 | 绑定后不能指向其他变量 | int b=20; ra=b; 实为赋值 |
类型严格匹配 | 必须与实体类型一致 | double d=1.1; int& rd=d; 错误 |
多级别名支持 | 可对引用再次引用 | int& rra=ra; 合法 |
cpp
int main() {
int a = 666;
int num = 100;
int& b = a;
int& c = b; //可对引用再次引用
int& d = c; //可对引用再次引用
//int& e; //引用必须初始化,该语句编译会报错。
cout << d << endl;
d = num; //引用一旦指定,不可修改 所以这里是 赋值, 是把num的值 100 赋值给 d
cout << &a << endl; //输出的地址相同
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
cout << a << endl; //输出的值相同
cout << b << endl;
cout << c << endl;
cout << d << endl;
}
![](https://i-blog.csdnimg.cn/direct/633a289fb7a448d3b118611954cf971b.png)
- 引用必须初始化
- 引用一旦指定,不可重绑定
- 引用的类型严格匹配
- 一个变量可以有多个引用(多个别名),引用变量也可以有引用(引用的别名)。
- 在语法层面上, 我们认为 引用没有开辟新空间, 只是对同一片内存空间取了多个名字
2. 常引用与权限控制
2.1 权限传递规则
操作 | 合法性 | 说明 |
---|---|---|
变量 → 常引用 | ✔️ | 权限缩小 |
常量 → 非 常引用 | ❌ | 权限放大 |
常引用 → 常引用 | ✔️ | 权限不变 |
注意事项:
- 权限可以平移。
- 权限可以缩小。
- 权限不能放大。
看如下,此处有报错,为什么?
1. 首先声明,每个变量名都有其相应的权限 。
2. 也就是说,每块内存,都有相应的权限 。
3. 引用,就是对一块内存起了别名
int x = 0
, 创建变量xint& y = x
, y是x的引用。此处, y是int型的引用,发生了权限的平移。const int& z = x
, 此处发生了权限的缩小。该内存块在使用名字z
时,权限为const,不能修改。- 名字x和y权限相同,即, 该内存块在使用名字
x
和y
时,可以修改 - 因此
++x
正确,++z
会报错。
2.2 常量引用
cpp
int main() {
const int a = 10;
//int& ra = a; //编译出错,因为 a为常量
const int& ra = a; //正确写法
//int& b = 10; //编译出错,因为 10 为常量, 该语句产生了权限的放大
const int& b = 10; //正确写法
return 0;
}
- 对
const int a = 10;
, 有int& ra = a
, 编译出错,因为 a为常量, 该语句发生了权限的放大
2.3 临时变量保护
1. 样例
声明1:在C/C++中,只要发生类型转换,就会产生临时变量 。
声明2:临时变量具有常性(不能修改)。
类型转换时会产生具有常性的临时变量,看以下例子:
cpp
double d = 12.34;
//int& rd = d; //编译出错,因为 类型不同
const int& rd = d;
// 合法,等价于:
// int temp = d; // d为3.14, 常量
// const int& rd = temp;
以上过程如下:
- 引用时发生类型转换,实质上是对临时变量的引用。
- 临时变量具有常性 。
double d = 12.34
,//int& rd = d; //编译出错
。临时变量具有常性
,实质上可以理解为:int& rd = const temp
发生了权限的放大,因此报错。 - 临时变量具有常性 。
const int& rd = d
, 实质上可以理解为:const int& rd = const d
, 是权限的转移。因此正确。
2. 样例
声明3:函数在进行值返回时,返回的同样是临时变量。该临时变量是原函数的拷贝。
实际上返回的是具有常性的临时变量
。
清楚了这一点后,以下例子的原理同上。
cpp
//例子
int func1() { //返回x的拷贝,会产生临时变量
static int x = 10;
return x;
}
int& func2() { //返回x的别名, 不会产生临时变量
static int x = 10;
return x;
}
int main() {
//int& x = func1(); //权限放大,错误。
int x1 = func1(); // 仅拷贝
const int& y = func1(); //权限平移,可以进行
int& ret2 = func2(); //可以,权限的平移
const int& ret2_ = func2(); //可以,权限的缩小
//总结,func返回的是一个变量的别名,
return 0;
}
3. 测试
cpp
//测试类型转换时会产生临时变量
int main() {
int i = 10;
double j = 10.11;
//过程:double temp = i; double j = temp
//该过程会发生类型提升
//一般是小的往大的进行类型提升,提升的时候不能改变原变量。
//因此只能产生原变量的副本,即临时变量
if (j > i) //此处是 double j 和 double i的比较
cout << "xxxxxxxxxxxxx" << endl;
return 0;
}
运行结果如下:
三、引用使用场景分析
3.1 函数参数传递
输出型参数
cpp
//利用引用,可以避免指针和多级指针
void Swap(int& a, int& b) { //交换值 形参是实参的别名
int temp = a;
a = b;
b = temp;
}
避免多级指针
cpp
void Swap(int*& a, int*& b) { //交换指针 如果不用引用,交换指针变量需要用二级指针
int* temp = a;
a = b;
b = temp;
}
高效传参
cpp
struct BigData { int arr[10000]; };
// 值传递:拷贝4w字节
void ProcessData(BigData data);
// 引用传递:仅传地址(4 或 8字节)
void ProcessDataOpt(const BigData& data);
3.2 做函数返回值
正确使用
cpp
int& GetStatic() {
static int count = 0;
return count; // 静态变量, 生命周期足够
}
危险案例
cpp
int& DangerousRet() {
int local = 10;
return local; // 返回局部变量引用!
}
- 如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回。
- 如果已经还给系统了,则必须使用传值返回。
不能返回局部对象(变量)的引用。
4. 性能对比实验
4.1 参数传递效率
cpp
struct HugeStruct { int data[10000]; };
void ValueFunc(HugeStruct hs) {} // 值传递
void RefFunc(const HugeStruct& hs) {} // 引用传递
// 测试结果(10000次调用):
// 值传递耗时:1587ms
// 引用传递耗时:2ms
4.2 返回效率对比
cpp
HugeStruct g_data;
HugeStruct ReturnByValue() { return g_data; }
HugeStruct& ReturnByRef() { return g_data; }
// 测试结果(100000次调用):
// 值返回耗时:3521ms
// 引用返回耗时:1ms
5. 引用与指针的终极对比
5.1 底层实现
assembly
; 引用实现
mov dword ptr [a], 0Ah
lea eax, [a] ; 取地址
mov dword ptr [ra], eax
; 指针实现
mov dword ptr [a], 0Ah
lea eax, [a]
mov dword ptr [pa], eax
关键区别
- 引用 :在 C++ 中引用通常会被优化为指针 ,底层是通过地址访问,但语法上没有指针的显式解引用和取地址操作。
- 指针 :指针显式地存储内存地址 ,允许进行指针算术操作,指针本身也可以为空(
nullptr
)。
从底层来看,引用和指针的实现非常相似,都是通过存储地址来实现对变量的间接访问。区别在于语法和语义上,引用在 C++ 中看起来更像是变量的别名,而指针则显式地表示地址。
5.2 特性对比表
特性 | 引用 | 指针 |
---|---|---|
初始化要求 |
必须 | 可选 |
空值 |
无NULL引用 | 支持NULL |
重定向 |
不可 | 可以 |
访问方式 |
直接访问 | 需解引用(*或->) |
类型安全 |
更高 | 较低 |
多级间接 |
单级 | 支持多级 |
sizeof |
返回原类型大小 | 返回地址大小(4或8字节) |
6. 高级应用技巧
6.1 链式操作
cpp
struct Matrix {
Matrix& Transpose() { /*...*/ return *this; }
Matrix& Rotate(double angle) { /*...*/ return *this; }
};
Matrix mat;
mat.Transpose().Rotate(45); // 链式调用
7. 总结引用要点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
8. 最佳实践指南
- 优先const引用 :函数参数尽量使用
const T&
形式 - 警惕返回引用:确保返回对象生命周期足够
- 替代输出参数:用引用替代指针作为输出参数
- 类型转换注意:隐式转换产生临时变量需用const引用
- 与智能指针配合 :
std::shared_ptr<T>&
管理资源(后续讲解)
以上就是关于引用的所有内容了,码字整理不易,欢迎各位大佬在评论区交流