某年面试候选人时,突然想到一个问题。C/C++
中的常量是否真的不能修改?因为C/C++中常量只是给编译器看的,用于编译器的代码检查,所以理论上是可以绕过其约束进行修改的。于是有以下的实验。
0x01 实验
如下代码,在Linux下使用gcc 11.2使用默认参数g++ const.cpp
进行编译并运行。大部分人可能跟我一开始理解一样,两行的输出应该是一样的。
#include <iostream>
using namespace std;
int main()
{
const int a = 20;
int* b = (int*) &a;
*b = 10;
cout << a << " " << *b << endl;
cout << *&a << " " << *b << endl;
}
结果如下:
20 10
10 10
意外吧!
添加-g
参数看下汇编代码。如下
0x00005555555551e9 <+0>: endbr64
0x00005555555551ed <+4>: push %rbp
0x00005555555551ee <+5>: mov %rsp,%rbp
0x00005555555551f1 <+8>: sub $0x20,%rsp
0x00005555555551f5 <+12>: mov %fs:0x28,%rax
0x00005555555551fe <+21>: mov %rax,-0x8(%rbp)
0x0000555555555202 <+25>: xor %eax,%eax
0x0000555555555204 <+27>: movl $0x14,-0x14(%rbp) # 对a赋值
0x000055555555520b <+34>: lea -0x14(%rbp),%rax
0x000055555555520f <+38>: mov %rax,-0x10(%rbp) # 将a地址赋给b
0x0000555555555213 <+42>: mov -0x10(%rbp),%rax # 取b的地址
0x0000555555555217 <+46>: movl $0xa,(%rax) # 将b指向的内存内容设为10
=> 0x000055555555521d <+52>: mov $0x14,%esi
0x0000555555555222 <+57>: lea 0x2e17(%rip),%rax # 0x555555558040 <_ZSt4cout@GLIBCXX_3.4>
0x0000555555555229 <+64>: mov %rax,%rdi
0x000055555555522c <+67>: call 0x5555555550f0 <_ZNSolsEi@plt>
0x0000555555555231 <+72>: mov %rax,%rdx
...
=> 0x000055555555526b <+130>: mov -0x14(%rbp),%eax
0x000055555555526e <+133>: mov %eax,%esi
0x0000555555555270 <+135>: lea 0x2dc9(%rip),%rax # 0x555555558040 <_ZSt4cout@GLIBCXX_3.4>
0x0000555555555277 <+142>: mov %rax,%rdi
0x000055555555527a <+145>: call 0x5555555550f0 <_ZNSolsEi@plt>
通过汇编也确实看到a指向的地址内容被修改了。第1个胖箭头指向的那行正是关键,cout
的入参为$0x14
,居然是个立即数。第2行cout
对应第2个胖箭头,参数来自a指向的地址。
0x02 解释
一开始的理解没毛病。“只是给编译器看的,用于编译器的代码检查”,这句话不错,但是编译器还做了我们没想到的事情,即编译期优化。编译器看到a为常量20,就会在编译生成汇编代码时,将a出现的地方都替换为立即数0x14
,即16进制20。这也就是标题中所说的常量传播。
Q1 第2行为什么没被优化?
A1 因为编译器没智能到识别出这种优化点,再说了,这种骚操作一般脑子正常的人(😃)是不会做的。
Q2 C语言中的行为是什么样的?
A2 试了下,C语言没有这个现象。两行输出结果是一样的,猜测下原因,const
是C从C++中借鉴的,真的只用于编译器检查,没有具体的优化动作。这就是所说,学只学得表象,未得其精髓。