一、什么是引用?
引用是c++中的一种语法特性,可以理解为变量的别名。引用本身不占用独立的内存空间,它和被引用的变量共享同一块内存。
引用的基本用法
int main() {
int a = 10;
int& ra = a; // ra是a的别名
a += 10;
cout << ra << endl; // 输出20
ra += 10;
cout << a << endl; // 输出30
return 0;
}关键点:对引用的操作就是对原变量的操作,两者完全等价。
二、引用的三大特点
1. 定义引用时必须初始化
int& ra; // ❌ 错误:引用必须初始化 int& ra = a; // ✅ 正确
原因:引用是变量的别名,别名必须依附于某个已存在的变量,不能独立存在。
2. 不存在空引用
int& ra = null; // ❌ 错误:不能给引用赋空值 int* p = null; // ✅ 指针可以为空
原因:引用必须绑定到一个有效的变量,不能指向空地址。指针是独立的变量,可以存储空地址,但引用不是变量,它只是别名。
3. 引用一旦绑定,不能改变指向
int a = 10, b = 20; int& ra = a; ra = b; // 这不是让ra指向b,而是把b的值赋给a(即a = 20)
原因:引用在初始化时绑定了目标变量,之后所有对引用的操作都直接作用于该变量,无法重新绑定到其他变量。
对比指针:
int a = 10, b = 20; int* p = &a; // 指针可以改变指向 p = &b; // 现在p指向b *p = 100; // 修改b的值为100
三、引用作为函数参数
引用的本质:引用是变量的别名,不是副本。对引用的操作就是对原变量的操作。
基于这个特性,引用最常用的场景是作为函数参数,主要解决两个问题:
1. 在函数内部修改外部变量
c语言中,函数参数默认是传值:形参是实参的副本,函数内部修改形参,外面的实参不受影响。
// c语言:传值,交换失败
void swap(int a, int b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 10, y = 20;
swap(x, y);
// x和y还是10和20,没变
}想要修改实参,必须传指针:
// c语言:传指针,交换成功
void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}c++的引用可以做到同样的事,语法更简洁:
// c++:传引用,交换成功
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 10, y = 20;
swap(x, y); // 调用时和传值一样,但实参被改了
cout << x << " " << y << endl; // 输出20 10
}优点:调用时不用取地址,函数内部不用解引用,代码更清晰。
2. 避免拷贝,提高效率
当参数是大型对象(如结构体、类)时,传值会拷贝整个对象,开销很大。传引用只传递地址(4/8字节),效率更高。
// 传值:拷贝整个student对象(几十字节)
void printstudent(student s) { ... }
// 传引用:只传递地址(4/8字节),不拷贝
void printstudent(const student& s) { ... }注意:对于 int、char 这类小型数据,传值反而更快,因为直接复制到寄存器,不需要间接访问。所以小型数据用传值,大型对象用const引用。
3. 三种参数传递方式对比
| 方式 | 本质 | 能否修改实参 | 效率(小型数据) | 效率(大型对象) |
|---|---|---|---|---|
| 传值 | 副本 | ❌ 不能 | 高 | 低 |
| 传指针 | 地址 | ✅ 能 | 中 | 高 |
| 传引用 | 别名 | ✅ 能 | 中 | 高 |
四、普通引用 vs const引用
1. 普通引用(可读可写)
int a = 10; int& ra = a; // 普通引用 ra = 100; // ✅ 可以修改 cout << ra; // ✅ 可以读取
2. const引用(只读)
int a = 10; const int& cra = a; // 常引用,只读 cout << cra; // ✅ 可以读取 cra = 100; // ❌ 错误:不能通过常引用修改
3. 权限规则:看原变量的权限
引用的权限不能超过原变量。
- 原变量是普通变量(可读可写)→ 引用可以是普通引用,也可以是常引用
- 原变量是常量(只读)→ 引用只能是常引用
int a = 10; // 普通变量:可读可写 const int b = 20; // 常量:只读 int& ra = a; // ✅ 普通引用引用普通变量 const int& cra = a; // ✅ 常引用引用普通变量 int& rb = b; // ❌ 错误:普通引用不能引用常量(权限超了) const int& crb = b; // ✅ 常引用引用常量
一句话:变量是什么权限,引用就不能超过这个权限。
五、const引用的万能特性
const引用可以引用任何东西:普通变量、常变量、字面常量,甚至临时对象。
1. 引用普通变量
int a = 10; const int& cra = a; // ✅
2. 引用常变量
const int b = 20; const int& crb = b; // ✅
3. 引用字面常量
const int& crx = 30; // ✅ 可以!
背后原理:
当 const 引用绑定到字面常量时,编译器会生成一个临时变量来保存这个常量,然后让引用指向这个临时变量。
// 编译器实际做的事: int tmp = 30; const int& crx = tmp;
注意:引用本身不占内存,但被引用的对象(临时变量)需要占内存。这个临时变量由编译器自动创建和管理,它的生命周期和引用一致。
4. 引用表达式结果
int a = 10, b = 20; const int& cr = a + b; // ✅ 表达式结果是临时变量,const引用可以绑定
六、引用的常见误区
误区1:引用可以重新绑定
int a = 10, b = 20; int& ra = a; // ra是a的别名 ra = b; // ❌ 不是让ra指向b,而是把b的值赋给a(a变成20)
误区2:引用占用空间
引用本身不占用独立内存,它只是原变量的别名。sizeof 引用的结果是原变量的大小。
int a = 10; int& ra = a; cout << sizeof(ra); // 输出4(int的大小),不是指针的大小
原因:sizeof 作用于引用时,返回的是被引用对象的大小。
误区3:引用可以指向引用
int a = 10; int& ra = a; int&& rra = ra; // ❌ 错误:不存在"引用的引用"
原因:引用是别名,不是对象。不能给别名再起别名吗?语法上不允许。
七、不能返回局部变量的引用或地址
先看一个错误示例
student& func() {
student sa{ "s001", "alice", "f", 20 };
return sa; // 返回局部变量的引用
}
int main() {
student& p = func();
printf("%s", p.s_name); // 可能崩溃,可能乱码,可能碰巧正确
}为什么错了?
困惑1:返回地址本身有什么错?
返回地址本身没错。return &sa 返回的是一个数字(内存地址),这个操作是合法的。错的是函数结束后你还去用这个地址访问内存。
困惑2:为什么函数结束后就不能用了?
函数调用时,系统会在栈上给局部变量分配空间。函数结束后,这块空间被系统回收,标记为“可重用”。但回收不是清零,这块内存还在,只是不再属于你了。
下次调用其他函数时,系统可能会把这块空间分配给别的变量,里面的内容随时可能被覆盖。所以你去访问它,结果是不可预测的。
困惑3:返回值和返回引用有什么区别?
// 返回引用:返回地址,危险
student& func1() {
student sa{ ... };
return sa; // 返回地址,sa销毁后地址失效 ❌
}
// 返回值:返回副本,安全
student func2() {
student sa{ ... };
return sa; // 返回副本,sa销毁不影响副本 ✅
}区别:
- 返回引用:把原件的地址告诉别人,原件没了,地址就成了空地址
- 返回值:复印一份交给别人,原件没了,复印件还在
困惑4:返回局部变量的地址,和返回局部变量本身,有什么区别?
看返回类型:
- 返回类型是
student&或student*:返回的是地址(危险) - 返回类型是
student:返回的是副本(安全)
student& func1() { return sa; } // 返回类型是引用 → 地址
student* func2() { return &sa; } // 返回类型是指针 → 地址
student func3() { return sa; } // 返回类型是对象 → 副本类比理解
你在酒店开房:
sa就是你住的房间- 返回引用或地址:你把房间号告诉别人
- 退房(函数结束):房间被回收,不再属于你
- 别人拿着房间号回去找:房间可能住了别人,可能在打扫,可能已经被改成仓库
- 去用这个房间号 → 结果不可预测
返回值:你退房时把房间里的东西复印一份带走,原来的房间怎么变都跟你无关。
正确的做法
1. 返回值(拷贝)
student func() {
student sa{ ... };
return sa; // 返回副本
}2. 返回静态局部变量
student& func() {
static student sa{ ... }; // 静态变量,生命周期贯穿整个程序
return sa;
}student& func() {
static student sa{ ... }; // 静态变量,生命周期贯穿整个程序
return sa;
}3. 在堆上分配
student* func() {
student* p = new student{ ... }; // 堆上分配,手动管理
return p;
}一句话总结
返回局部变量的地址本身没错,错的是函数结束后你还去用这个地址。因为这块内存已经被系统回收,不再属于你了。
八、能不能定义引用数组?
结论:不能定义“元素为引用的数组”,但可以定义“数组的引用”。
1. 不能定义元素为引用的数组
int& rbr[5]; // ❌ 错误:不能定义引用数组
原因:数组是一段连续的内存空间,每个元素都需要占用内存。引用本身不占内存,它只是别名,无法作为数组元素存在。
2. 可以定义数组的引用
int ar[5] = {1,2,3,4,5};
int(&rar)[5] = ar; // ✅ 正确:rar是数组ar的引用含义:rar 是整个数组 ar 的别名,不是元素为引用的数组。
3. 对比
| 写法 | 含义 | 是否正确 |
|---|---|---|
int& rbr[5] | 元素为引用的数组 | ❌ 错误 |
int(&rar)[5] | 数组的引用 | ✅ 正确 |
记忆技巧:括号位置决定一切。& 靠近数组名,表示引用整个数组;& 靠近类型,表示元素是引用。
九、数组名退化问题
1. 数组名退化的规则
数组名在三种情况下不会退化,保持为整个数组:
sizeof(ar)&ar(取数组地址)- 引用数组
int(&rar)[n] = ar
其他情况数组名退化为数组首元素的地址。
int ar[5] = {1,2,3,4,5};
cout << sizeof(ar); // 20(5*4,未退化)
int(*p)[5] = &ar; // 取数组地址,未退化
int(&rar)[5] = ar; // 引用数组,未退化
int* q = ar; // 退化:ar变成int*2. 数组名作为函数参数:退化问题
c++中,数组名作为参数传递给函数时,会退化为指针,丢失数组大小信息。
void funa(int br[5]) {
cout << sizeof(br); // 输出8(指针大小),不是20
// 函数内部不知道数组有多大,只能通过额外参数传入长度
}如果希望函数内部能知道数组的大小,可以用数组的引用作为参数:
void funb(int (&rar)[5]) {
cout << sizeof(rar); // 输出20,保留了数组大小
// 函数内部可以直接用sizeof获取数组长度
}关键点:用数组的引用作为参数,可以保留数组的大小信息,避免退化。
十、引用与指针的区别
1. 语法规则上的区别
| 对比项 | 引用 | 指针 |
|---|---|---|
| 本质 | 变量的别名 | 存储变量的地址 |
| 内存分配 | 不分配独立内存 | 分配4/8字节内存 |
| 使用方式 | 直接使用 | 需要解引用(*) |
| 能否改变指向 | 不能 | 能 |
| 必须初始化 | 是 | 否 |
| 是否可为空 | 不能 | 可以(null/nullptr) |
| sizeof结果 | 原变量的大小 | 指针本身的大小(4/8) |
| 多级 | 无(无引用的引用) | 有(二级、n级指针) |
| ++操作 | 改变原变量的值 | 改变指向的地址 |
// 演示++操作的区别
int ar[5] = {10,20,30,40,50};
int& ra = ar[0];
int* p = &ar[0];
++ra; // ar[0]变成11
++p; // p指向ar[1]2. 汇编层面的本质
从汇编角度看,指针和引用没有区别,都是地址。
int a = 10; int& ra = a; int* p = &a; // 底层实现:ra和p都是存储a的地址
引用在底层也是用指针实现的,只是编译器做了语法糖,让使用更方便、更安全。
3. 一句话总结
| 维度 | 引用 | 指针 |
|---|---|---|
| 语法层面 | 别名,更安全 | 地址,更灵活 |
| 汇编层面 | 都是地址 | 都是地址 |
使用建议:能用引用就用引用,需要改变指向或可为空时再用指针。
总结
| 知识点 | 核心要点 |
|---|---|
| 引用本质 | 变量的别名,不占独立内存 |
| 三大特点 | 必须初始化、不能为空、不能改指向 |
| const引用 | 只读,可引用普通变量/常变量/字面常量 |
| 权限规则 | 引用权限不能超过原变量 |
| const引用万能 | 可绑定字面常量和临时对象,编译器自动生成临时变量 |
| 引用作为参数 | 可修改外部变量、避免大对象拷贝 |
| 返回局部变量 | 不能返回局部变量的引用或地址,函数结束后内存被回收 |
| 数组的引用 | 可定义数组引用 int(&rar)[5],不能定义引用数组 int& rbr[5] |
| 数组名退化 | sizeof/&/数组引用 三种情况不退化,函数参数会退化 |
| 引用 vs 指针 | 引用更安全(不能为空、不能改指向),指针更灵活 |
到此这篇关于c++引用全解的文章就介绍到这了,更多相关c++引用内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论