第8章 引用与const限定
第8章 引用与const限定
- 引用的声明、初始化和基本用法
- 引用与指针的核心区别与选用原则
- const关键字修饰变量、指针和引用的不同效果
指针给了你直接操作内存地址的能力,但随之而来的是空指针、野指针和复杂的语法。有没有一种方式,既能像指针一样高效地“指向”某个对象,又更安全、更直观?这就是引用(Reference)的用武之地。而const,则是你代码中的“安全锁”,它能明确告诉编译器和你自己:哪些数据是只读的,不能被修改。
8.1 引用:变量的“别名”
生活类比:你的本名是“张三”,在家里小名叫“三儿”。无论别人叫“张三”还是“三儿”,指的都是你这个人。技术定义:引用是为一个已存在的变量起的另一个名字,它和原变量共享同一块内存地址,对引用的操作直接影响原变量。
8.1.1 引用的声明与使用
声明引用时,使用&符号,并且必须在声明时进行初始化,绑定到一个已存在的对象。一旦绑定,终身不变。
int value = 10;
int& ref = value; // ref是value的引用,即别名
ref = 20; // 通过引用修改值
cout << value; // 输出 20,原变量也被修改
cout << &value << " " << &ref; // 输出相同的地址
关键特性
- 必须初始化:引用在诞生时就必须指明它是谁的别名,不能先声明后赋值。
- 绑定不可变:一个引用一旦绑定到某个变量,就不能再绑定到其他变量。
- 无空引用:不存在“空引用”的概念,这比指针安全。
- 无引用的引用:C++不支持引用的引用(但可以通过类型别名实现类似效果)。
8.1.2 引用 vs 指针:一张表看清本质
理解了引用的基础,我们立刻把它和上一章学的指针放在一起对比。这是C++面试的经典问题,也是实际开发中选择的依据。
| 特性 | 引用 (Reference) | 指针 (Pointer) |
|---|---|---|
| 初始化要求 | 必须在声明时初始化 | 可以声明时不初始化(危险!) |
| 能否为空 | 不能为空,总代表某个对象 | 可以为nullptr或NULL |
| 能否重新绑定 | 不能,一旦绑定终身不变 | 可以,指向不同地址 |
| 访问语法 | 像普通变量一样使用(ref = 5) | 需要通过解引用(*ptr = 5) |
| 内存占用 | 通常不占用额外存储(由编译器实现) | 占用独立内存存储地址(通常4或8字节) |
| 多级间接 | 不支持 | 支持多级指针(int**) |
| 主要用途 | 函数参数传递、返回值,创建别名 | 动态内存管理、数组遍历、复杂数据结构 |
我在项目中踩过这个坑:早期为了“灵活”,大量使用指针作为函数参数,结果团队里新手经常忘记检查空指针,导致程序崩溃。后来我们制定规范:如果函数参数目的只是读取或修改传入的对象,且不允许为空,优先使用引用。这从语法层面就杜绝了空值问题。
常见误区
- 误区一:引用是“安全版的指针”。不完全对。引用有更严格的语义(必须初始化、不可重绑定),这带来了安全性,但也失去了指针的灵活性(如链表遍历)。
- 误区二:引用不占内存。这是实现细节。在语法层面,你不需要关心它是否占内存;在底层,编译器可能会用指针实现引用,但这不影响你的代码逻辑。
- 误区三:可以用引用实现链表。不行!因为引用不能重绑定,而链表节点的
next需要指向不同的节点。
8.2 const限定符:定义“只读”规则
const关键字用于定义一个值不可被修改的对象。它就像给你的变量贴上一个“只读”标签,编译器会帮你检查任何试图修改它的操作。
const int MAX_SIZE = 1024; // 定义一个常量
// MAX_SIZE = 2048; // 错误!不能修改const变量
const double PI = 3.1415926;
int radius = 5;
double area = PI * radius * radius; // 正确,只是读取
实际开发中,我推荐用const替换C语言风格的#define宏定义常量。因为const变量有明确的类型,会参与类型检查,并且会进入符号表,调试时可以看到它的值。而宏只是简单的文本替换。
编者提示:关于#define宏的详细弊端以及const的优势,我们会在第23章《命名空间与作用域》中深入讨论,包括宏可能引发的命名污染和调试困难问题。
8.2.1 const与指针的组合:四种情况
当const遇到指针,位置不同,含义天差地别。这是C++语法的一个难点,但只要你记住“const修饰它左边的东西,如果左边没东西,就修饰右边的东西”这个口诀,就能轻松破解。
我们用具体代码来验证:
int a = 10, b = 20;
// 情况1:指向常量的指针(指针可以变,指向的值不能变)
const int* p1 = &a;
// *p1 = 30; // 错误!不能通过p1修改a的值
p1 = &b; // 正确,可以让p1指向b
// 情况2:常量指针(指针不能变,指向的值可以变)
int* const p2 = &a;
*p2 = 30; // 正确,a现在等于30
// p2 = &b; // 错误!p2不能再指向别的地址
// 情况3:指向常量的常量指针(都不能变)
const int* const p3 = &a;
// *p3 = 40; // 错误!
// p3 = &b; // 错误!
记忆技巧
从右向左读声明:int* const p读作“p是一个常量,这个常量是指针,指向int”。const int* p读作“p是一个指针,指向int常量”。
8.3 引用与const的强强联合
引用本身不能重绑定,但如果加上const,我们主要约束的是不能通过这个引用来修改目标对象。这产生了两种主要用途:常量引用和函数参数传递。
常量引用
- 绑定到常量或非常量
- 承诺“只读不写”
- 可延长临时对象生命周期
函数参数传递
- 避免大对象拷贝开销
- 防止函数内部意外修改
- 可接受常量和非常量实参
8.3.1 常量引用
声明为const T&的引用,不能用来修改它绑定的对象。但它可以绑定到常量、非常量,甚至临时对象(右值)。
int x = 100;
const int& cref1 = x; // 正确,绑定到非常量
// cref1 = 200; // 错误!不能通过常量引用修改
const int y = 200;
const int& cref2 = y; // 正确,绑定到常量
// 绑定到临时对象(右值)
const int& cref3 = 300; // 正确!字面量300是临时对象
// int& bad_ref = 300; // 错误!非常量引用不能绑定右值
这里有个隐藏的坑:常量引用绑定临时对象时,会延长该临时对象的生命周期,使其生命周期与引用相同。这是C++语言的特殊规则,非常有用。
8.3.2 函数参数中的常量引用
这是const引用最实用、最高频的场景。当函数只需要读取参数值,而不需要修改它时,使用常量引用作为参数。
// 不好的设计:传值,大对象会有拷贝开销
void printStudentBad(Student stu) {
cout << stu.name << ", " << stu.age;
}
// 好的设计:传常量引用,零拷贝,且安全
void printStudentGood(const Student& stu) {
// stu.age = 25; // 错误!编译器阻止你意外修改
cout << stu.name << ", " << stu.age;
}
// 调用
Student s {"Alice", 20};
printStudentGood(s); // 传递对象本身,没有拷贝
printStudentGood(Student{"Bob", 22}); // 甚至可以传递临时对象!
我在项目中强制要求:所有自定义类型(结构体、类)作为函数参数时,如果不需修改,必须使用const T&。对于内置类型(int, double等),由于拷贝开销极小,传值或传引用均可,但为了代码一致性,也推荐使用const T&。
8.4 函数返回引用
函数可以返回引用,但必须确保返回的引用绑定的是一个在函数返回后依然有效的对象。最常见的是返回参数引用、类成员引用,或者全局/静态变量引用。
// 正确:返回参数引用
int& getLarger(int& a, int& b) {
return (a > b) ? a : b;
}
// 使用
int x = 5, y = 10;
getLarger(x, y) = 100; // 现在y等于100
cout << y; // 输出100
// 错误:返回局部变量的引用(悬空引用)
int& badFunction() {
int local = 42;
return local; // 警告!local在函数结束时销毁
} // 返回的引用指向已销毁的内存,行为未定义
如果你看到编译器警告“returning reference to local variable”,100%是犯了上面的错误。实际开发中,返回引用常用于实现链式调用,比如cout << a << b;中的<<运算符就返回流对象的引用。
动手试一试
1. 编写一个函数swap,使用引用参数交换两个整数的值。比较一下,如果用指针实现,代码有什么区别?
2. 定义一个表示二维点的结构体Point,包含x和y坐标。编写一个函数printPoint,使用常量引用接收Point参数并打印。
3. 尝试写出以下声明的含义,并验证你的理解:
int a = 1, b = 2;
const int* ptr1 = &a;
int* const ptr2 = &a;
const int* const ptr3 = &a;
分别尝试修改*ptr1、ptr1、*ptr2、ptr2,观察编译器报错信息。
检验你的理解
- 为什么引用必须在声明时初始化,而指针不需要?
- 下面代码错在哪里?
int& ref; ref = someVariable; - 给定
int data = 10;,const int& r = data;之后,能否通过data = 20;来修改值?为什么? - 函数参数使用常量引用(
const T&)相比传值(T)的主要优势是什么? - 判断:
int* const p表示p指向的整数是常量,p本身可以指向其他地址。
本章小结
- 引用是变量的别名:必须初始化、不可重绑定、无空引用,使用起来比指针更安全直观。
- const定义只读变量:保护数据不被意外修改,应用常量引用可以避免函数参数拷贝并保证安全。
- const与指针组合有四种情况:牢记“const修饰左边”的口诀来区分指向常量的指针、常量指针等。
- 函数参数传递优先使用常量引用:对于非内置类型,这能避免拷贝开销;对于所有类型,它能明确表达“只读”意图。
- 返回引用需谨慎:确保返回的引用在函数调用后依然有效,绝不能返回局部变量的引用。
微信扫码关注
抖音扫码关注