C++ const 指针(常量指针与指针常量)

在 C++ 中,我们经常会把 const 关键字与指针结合起来使用,用来限制 “能不能通过指针修改目标的值”,或者限制 “能不能修改指针自己的指向”。

对于一个普通的指针来说,它有两个地方是可以改变的:① 我们可以通过 *p = 20 修改目标变量的值;② 我们可以执行 p = &b 修改指针自己的指向。加上 const 之后,就会把其中某一项或两项锁住,常见形式如下表所示。

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) 就会编译报错。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号