在 C++ 中,我们经常会把 const 关键字与指针结合起来使用,用来限制 “能不能通过指针修改目标的值”,或者限制 “能不能修改指针自己的指向”。
对于一个普通的指针来说,它有两个地方是可以改变的:① 我们可以通过 *p = 20 修改目标变量的值;② 我们可以执行 p = &b 修改指针自己的指向。加上 const 之后,就会把其中某一项或两项锁住,常见形式如下表所示。
| 指针写法 | 常用名称 | 修改目标值(*p) | 修改指针指向(p) |
|---|---|---|---|
| int* p | 普通指针 | 可以 | 可以 |
| const int* p | 常量指针 | 不可以 | 可以 |
| int* const p | 指针常量 | 可以 | 不可以 |
| const int* const p | 指向常量的指针常量 | 不可以 | 不可以 |
区分这几种写法其实不用死记硬背,我们只需要以星号 * 为分界线,记住一句口诀:“左定值,右定向”。
具体来说,const 在 * 左边时修饰的是 *p,表示目标变量的值不能通过指针修改。而 const 在 * 右边紧贴变量名 p 时修饰的是 p 自身,表示指针的指向不能修改。
提示 不同中文教材或资料对 “常量指针” 和 “指针常量” 的中文叫法有时会反过来。小伙伴们在看代码或做题时不用去纠结中文译名,直接看 const 在星号 * 的左边还是右边(“左定值,右定向”)才是最准确的判断方法。
常量指针(const int*)
当 const 放在星号 * 的左边时,它锁住的是 *p 所代表的目标值。因为通过这个指针看过去,目标是一个不能修改的量,所以我们通常把它叫做 “常量指针”(也叫 “指向常量的指针”)。
语法:
const 类型名* 指针变量名 = &变量名;说明:
定义了 const int* p = &a; 之后,我们可以通过 *p 读取变量的值,但不能写 *p = 30 去修改它。由于指针变量 p 自身没有被锁住,我们依然可以执行 p = &b 让它改为指向其他变量,也可以先将它初始化为 nullptr。
在使用常量指针时,我们还要留意以下 2 个细节:
- 原变量本身不一定变成了常量:
const int* p = &a只是限制了 “不能通过指针p去修改a”。如果原变量a本身是普通变量,我们依然可以直接通过a = 30修改它的值。 - 指向 const 常量必须使用常量指针:如果目标本身是一个常量(如
const int MAX_SCORE = 100),普通指针int*无法指向它,我们必须使用const int*才行。因为如果把只读常量的地址交给可读可写的普通指针,就会留下修改常量的漏洞,编译器会直接报错拦截。
示例 1:使用常量指针
#include <iostream>
int main() {
int a = 10;
int b = 20;
const int MAX_SCORE = 100;
const int* p = &a;
std::cout << "*p = " << *p << '\n';
p = &b; // 更换指向到变量 b
std::cout << "*p = " << *p << '\n';
b = 30; // 直接修改普通变量 b
std::cout << "*p = " << *p << '\n';
p = &MAX_SCORE; // 指向 const 常量
std::cout << "*p = " << *p << '\n';
return 0;
}运行结果如下。
*p = 10
*p = 20
*p = 30
*p = 100分析:
const int* p = &a; 定义了一个指向 a 的常量指针,第 1 次输出 *p 得到 10。接着执行 p = &b 让指针改为指向变量 b,第 2 次输出得到 20。
随后直接执行 b = 30 修改普通原变量是合法的,再次通过 *p 读取到的就是最新的 30。最后执行 p = &MAX_SCORE 指向 const 常量,同样可以正常读取。
反过来,如果我们尝试通过常量指针修改目标值,或者使用普通指针去指向 const 常量,都会直接导致编译报错。下面这两种写法就是错误的。
// 错误方式 1
*p = 30;
// 错误方式 2
int* q = &MAX_SCORE;这里顺便提一点:只要 const 位于星号 * 的左边,写成 int const* p = &a 与 const int* p = &a 是完全等价的。平时写代码时,大家统一写成 const int* p 即可。
指针常量(int* const)
当 const 放在星号 * 的右边(紧挨着指针名)时,它锁住的就是指针变量自己。因为指针本身成了一个不能更换指向的常量,所以我们通常把它叫做 “指针常量”。
语法:
类型名* const 指针变量名 = &变量名;说明:
因为 int* const p 一旦定义就不能再修改指向,所以我们在定义时必须同时完成初始化。后续使用时,只要目标变量本身不是常量,我们就可以通过 *p = 95 修改目标变量的值,但不能再写 p = &bonus 更换指向。
示例 2:使用指针常量
#include <iostream>
int main() {
int score = 80;
int* const p = &score;
*p = 95; // 修改目标变量的值
std::cout << "score = " << score << '\n';
std::cout << "*p = " << *p << '\n';
return 0;
}运行结果如下。
score = 95
*p = 95分析:
在这个例子中,int* const p = &score; 让指针 p 固定指向变量 score。由于星号 * 左边的 int 没有加 const,目标值并没有被锁住,我们可以对比一下正确写法与错误写法。
int score = 80;
int bonus = 100;
int* const p = &score;
// 正确方式
*p = 95;
// 错误方式
p = &bonus;看到这里你会发现,指针常量 “定义时必须初始化、不能更换指向、但能修改目标值” 的特点,和前面学过的引用非常相似。实际上,编译器在底层通常就是借助指针常量来实现引用的。
指向常量的指针常量(const int* const)
如果我们把上面两种限制叠加在一起,在星号 * 的左边和右边各放一个 const,就得到了 “指向常量的指针常量”。
语法:
const 类型名* const 指针变量名 = &变量名;说明:
这种写法把两头都锁住了:我们既不能通过 *p 修改目标变量的值,也不能修改指针 p 自己的指向,因此在定义时同样必须直接完成初始化。
示例 3:指向常量的指针常量
#include <iostream>
int main() {
int a = 10;
const int MAX_SCORE = 100;
const int* const p1 = &a;
const int* const p2 = &MAX_SCORE;
a = 20; // 原变量 a 本身仍可直接修改
std::cout << "*p1 = " << *p1 << '\n';
std::cout << "*p2 = " << *p2 << '\n';
return 0;
}运行结果如下。
*p1 = 20
*p2 = 100分析:
在这个例子中,p1 和 p2 同时锁住了目标值与指针指向,既能绑定普通变量 a,也能绑定常量 MAX_SCORE。
需要注意的是,p1 锁住的只是 “通过指针 p1 修改值” 的通道。由于原变量 a 本身是普通变量,直接执行 a = 20 依然是合法的,因此 *p1 输出了最新的 20。但只要通过 p1 操作,无论是写 *p1 = 30 改值,还是写 p1 = &MAX_SCORE 换指向,都会直接引发编译报错。
const 指针的实际应用
在上面介绍的 3 种形式中,实际开发用得最多的是第 1 种常量指针 const int*。
在前面的 “指针与数组” 这一节中,我们学过把数组传给函数时,数组名会自动退化为指针。如果一个函数只是为了遍历读取数组元素(比如打印数组或求和),不需要修改数组里的数据,就可以把形参写成 const int* arr,防止在函数内部误改原数组。
示例 4:使用 const 指针保护数组参数
#include <iostream>
// 使用 const int* 限制只读
void printArray(const int* arr, int size) {
for (int i = 0; i < size; i++) {
std::cout << arr[i] << ' ';
}
std::cout << '\n';
}
int main() {
int scores[4] = {85, 90, 92, 98};
const int LEVELS[3] = {1, 2, 3};
printArray(scores, 4); // 传入普通数组
printArray(LEVELS, 3); // 传入 const 数组
return 0;
}运行结果如下。
85 90 92 98
1 2 3分析:
printArray() 将形参 arr 声明为常量指针 const int* arr。对于数组参数来说,下面 2 种形参写法也是完全等价的。
// 方式 1(推荐)
void printArray(const int* arr, int size);
// 方式 2
void printArray(const int arr[], int size);加上 const 之后,小伙伴们在函数内部就只能读取 arr[i] 的值。如果不小心写了 arr[0] = 60,编译阶段就会直接报错提醒。同时,const int* 形参既能接收普通数组 scores,也能安全接收只读数组 LEVELS。如果把形参写成不带 const 的 int* arr,调用 printArray(LEVELS, 3) 就会编译报错。
