const“常量”的值不const?

· · 科技·工程

当你初学 C++ 时,老师告诉你可以使用 const 来定义一个常量。你尝试修改一个常量,得到了一个编译错误。于是你记下了这个知识点,而常量有什么用、编译器怎么保证常量不变,你不知道、也不关心。
后来你突发奇想,想试试“常量”真的不可修改吗,写出了这样的代码:

const int m = 111;
printf("%d\n", m);
int *p = (int *)&m;
*p = 222;
printf("%d\n", m);

编译通过了!但是为什么输出了两行 111
查看生成的汇编代码可以发现:聪明的编译器知道 m 是一个常量,所以它把所有用到 m 的地方替换成了 111,就像这样:

printf("%d\n", 111);
printf("%d\n", 111);

怎么办呢?聪明的你使用 volatile 修饰符来告诉编译器不要优化对 m 的读取:

const volatile int m = 111;

果然,得到了一行 111 和一行 222。不过,通过 volatile 的方法有些不太纯粹,有没有更纯粹的方法?
你新建了一个文件,把它编译成一个动态链接库:

extern "C" const int value = 111;
extern "C" const int* get_point() {
    return &value;
}

好了,现在的目标是通过 get_point() 返回的指针修改 value,你故技重施:

const int *p = get_point();
printf("%d\n", *p);
int *p2 = (int *)p;
*p2 = 456;
printf("%d\n", *p);

段错误!事情变得有趣起来了。
聪明的你发现 value 被放在了动态库的 .rodata 节,这个节在加载时会放进一个只读的内存页中,当程序尝试写入时便引发了段错误。
\mathtt{0x0bad1406}指令引用的\mathtt{0x0bad1406}内存,该内存不能为written.
怎么办呢?聪明的你知道 Linux 有一个系统调用 mprotect ,它可以更改内存页的权限,只需要给 value 所在的内存页添加写权限就可以了:

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
extern "C" const int* get_point();
void unlock_page_write(const char *maps) {
    FILE *stream = fmemopen((void *)maps, strlen(maps), "r");
    char *line = nullptr;
    size_t len = 0;
    while (getline(&line, &len, stream) != -1) {
        unsigned long long begin_addr = 0;
        unsigned long long end_addr = 0;
        sscanf(line, "%llx-%llx", &begin_addr, &end_addr);
        mprotect((void *)begin_addr, end_addr - begin_addr, PROT_READ | PROT_WRITE | PROT_EXEC);
    }
}
int main() {
    int fd = open("/proc/self/maps", O_RDONLY);
    char buffer[8193];
    ssize_t readed = read(fd, buffer, sizeof(buffer) - 1);
    buffer[readed] = '\0';
    unlock_page_write(buffer);
    const int *p = get_point();
    printf("%d\n", *p);
    int *p2 = (int *)p;
    *p2 = 222;
    printf("%d\n", *p);
}

果然,得到了一行 111 和一行 222
不错,我们通过奇淫技巧绕过了 const 的保护……吗? 我们查阅C++标准就会发现:

Any attempt to modify a const object during its lifetime results in undefined behavior. (9.2.9.2)
修改const对象的任何尝试是未定义行为。 (翻译仅供参考)

也就是说,你之前做的一切和 (i++) + (++i)一样,没有任何保证。它不像 1+2+3,标准保证你得到的就是从左加到右得到的值。编译器不会在运行时保证你不修改常量所在的内存,它只会在编译时告诉你不能修改,至于运行时你你不能改成功,鬼才知道呢!

那么,const 到底有什么用?
也许对于 OIer 只要 AC 就行的哲学来说,const 确实没用。但是在真实项目开发中,你可能需要为一个一年前的代码添加功能,而一个 const 就会在编译时保证你不会修改不该修改的值,而不至于运行出现 bug 时 debug 半天。