C++ const_cast

在 C++ 中,一旦给指针或引用加上了 const 限定,我们就不能再通过它去修改所指向的数据了。但如果底层变量本身其实是可以修改的,或者我们需要把只读指针传给形参漏写 const 的旧函数,此时应该怎么办呢?

针对这种需求,C++ 提供了专门的 const_cast 转换运算符。它是 4 种专用类型转换运算符中,唯一能够去掉指针或引用上 const 只读限定的工具。

在动手写代码之前,小伙伴们可以先记住一条基本原则:指针或引用上的 const 就像是贴在盒子外面的 “只读标签”。const_cast 只能撕掉这张标签,并不会改变盒子里装的原变量本身是不是常量,如下图所示。

用只读标签理解 const_cast 的安全修改条件

去掉指针与引用的 const 限定

当一个原本可修改的普通变量被 const 指针或常量引用指向时,我们可以使用 const_cast 去掉这层只读限制,恢复对原变量的修改权限。

语法:

const_cast<目标类型>(表达式)

说明:

其中,尖括号 <> 里的目标类型必须是指针类型(如 int*、char*)或引用类型(如 int&),圆括号 () 里的表达式则是对应的同类型 const 指针或 const 引用。

需要注意的是,const_cast 只能用来调整 const 限定符,既不能改变基础数据类型,也不能直接转换非指针、非引用的普通变量。

示例 1:去掉指针上的 const 限定

#include <iostream>

int main() {
    int score = 90;
    const int* readPtr = &score;

    // 去掉指针上的 const 限定
    int* writePtr = const_cast<int*>(readPtr);
    *writePtr = 95;

    std::cout << score << '\n';
    return 0;
}

运行结果如下。

95

分析:

在这个例子中,指针 readPtr 的类型是 const int*,直接写 *readPtr = 95; 会编译报错。但我们可别忘了,score 从一开始定义的就是普通的 int 变量,它本身并不是常量。

因此,当我们使用 const_cast<int*>(readPtr) 去掉指针上的只读限制后,得到的 writePtr 指向的依然是原本就可修改的 score,通过 *writePtr = 95; 修改它的值是完全合法且安全的。

除了指针之外,const_cast 同样可以用于引用。下面我们再来看一个例子。

示例 2:去掉引用上的 const 限定

#include <iostream>

int main() {
    int count = 10;
    const int& readRef = count;

    // 去掉引用上的 const 限定
    int& writeRef = const_cast<int&>(readRef);
    writeRef = 20;

    std::cout << count << '\n';
    return 0;
}

运行结果如下。

20

分析:

const_cast<int&>(readRef) 表示去掉只读引用 readRef 上的 const 限制,将其转换为普通引用 writeRef,随后通过 writeRef = 20; 就能正常修改原变量 count 的值。

在编写代码时,初学者容易犯的语法错误是拿 const_cast 直接转换普通变量,或者同时改变指针指向的数据类型。下面这两种写法在编译阶段都会直接报错。

const int a = 10;
const int* p = &a;

int b = const_cast<int>(a);             // 报错:非指针或引用
double* dp = const_cast<double*>(p);    // 报错:改变了基础类型

const_cast 的实际应用场景

看到上面的例子,为什么我们不直接修改原变量 score 和 count,还要绕一圈使用 const_cast 呢?其实前面的例子只是为了方便大家看清语法,在实际开发中,我们很少会在同一个函数里先加上 const 再手动去掉。

const_cast 最常见的用途是对接形参漏写 const 的旧函数接口。有些早期编写的函数内部只读取字符串、并不会修改数据,但定义形参时写成了 char* 而忘了加 const。当我们手里只有 const char* 指针时,就可以借助 const_cast 临时去掉 const 再传进去。

示例 3:对接未加 const 的旧函数

#include <iostream>

// 形参漏写 const 的只读旧函数
void printMessage(char* text) {
    std::cout << "消息内容:" << text << '\n';
}

int main() {
    char buf[] = "欢迎来到绿叶网!";
    const char* msg = buf;

    // 去掉 const 限定后传参
    printMessage(const_cast<char*>(msg));
    return 0;
}

运行结果如下。

消息内容:欢迎来到绿叶网!

分析:

在这个例子中,指针 msg 的类型是 const char*,而旧函数 printMessage() 要求的参数类型是 char*。如果我们直接写 printMessage(msg),编译器会因为不能自动丢弃 const 限定而报错。

这里数据原本存放在可读可写的字符数组 buf 中,并且 printMessage() 内部只是打印文字、不会改动字符内容,因此我们通过 const_cast<char*>(msg) 转换后再传参是完全安全的。

注意 对接未加 const 的旧函数前,我们必须先确认该函数内部确实只读取、不修改数据。尤其当指针直接指向 "hello" 这类存放在只读内存区的字符串字面量时,一旦函数内部尝试修改字符,程序就会直接崩溃。

const_cast 的安全边界

在使用 const_cast 时有一条重要的安全红线:千万不要通过 const_cast 去修改一个原本就定义为 const 的常量。

下面我们将 “原变量不是常量” 和 “原变量本身是常量” 这两种情况放在一起对比:

// 正确方式
int score = 90;
const int* p1 = &score;
int* w1 = const_cast<int*>(p1);
*w1 = 95;

// 错误方式
const int limit = 100;
const int* p2 = &limit;
int* w2 = const_cast<int*>(p2);
*w2 = 120;

在上面的错误方式中,虽然代码在语法上能通过编译,但 limit 从定义开始就是 const 常量。const_cast 只撕掉了指针 p2 上的只读标签,并没有把内存里的常量 limit 变成普通变量。

强行通过指针或引用去修改真正的常量属于未定义行为,运行结果不再可控。比如编译器常在编译阶段把读取 limit 的地方直接替换成 100,就算执行了 *w2 = 120;,再去输出 limit 看到的可能还是 100。如果该常量存放在只读内存区,强行写入还会导致程序直接崩溃。

对于这一点,小伙伴们只需要记住这么一句话就可以了:使用 const_cast 转换后能不能修改数据,只看最源头的变量在定义时有没有加 const。源头没加 const 就能改,源头加了 const 就不能改。

常见问题

1. 把普通指针(int*)转换为只读指针(const int*),需要使用 const_cast 吗?

不需要。虽然 const_cast 在语法上也支持给指针加上 const,比如写成 const_cast<const int*>(p),但在实际开发中完全没必要这样做。

把可读可写的 int* 变成只读的 const int* 属于收紧权限的安全操作,C++ 支持自动隐式转换。我们平时直接写 const int* readPtr = &score; 就可以了,既不需要使用 const_cast,也不需要使用 static_cast。

上一篇:

下一篇:

给站长反馈

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

邮箱:lvyenet@vip.qq.com

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