从一道笔试题一探const的本质

前言

先来看一段程序:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

using namespace std;

int main(void)
{

const int a = 10;
int *p = (int*)(&a);
*p = 20;
cout<<"a = "<<a<<",*p = "<<*p<<endl;
}

问上面程序输出的是__:

  • 编译阶段报错
  • 运行阶段报错
  • a = 10, *p = 10
  • a = 20, *p = 20
  • a = 10, *p = 20
  • a = 20, *p = 10

这是阿里的一道笔试题,看似简单却有很多值得深究的地方。写个程序一跑,跟开始想象的不是一回事,
在折腾了一段时间之后,终于对const有了进一步的认识。

写篇东西记录一下,也算是对自己如此不扎实的知识水平的警醒。

正文

什么是const

const是C/C++中的一个类型修饰符,用来表明该变量的值不能被更新。
但是,C/C++中是如何保证这一点,而且到底是多大限度上的“常类型”呢。

剖析上面的程序

先用Visual Studio跑一下这个程序:

1
a= 10,*p= 20

为什么?

在程序里面,先声明了一个常变量a,值为10,指针p指向变量a,然后把指针p指向的内容更新为20,最后输出a*p

更有意思的在于,如果你用VS单步调试,用监视窗口看a的值,会发现a被更新成了20.

要知道为什么这段程序的输出是这样,只能从汇编指令中一探究竟了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
	const int a = 10;
00E614AE mov dword ptr [a],0Ah
int *p = (int*)(&a);
00E614B5 lea eax,[a]
00E614B8 mov dword ptr [p],eax
*p = 20;
00E614BB mov eax,dword ptr [p]
00E614BE mov dword ptr [eax],14h
cout<<"a= "<<a<<",*p= "<<*p<<endl;
00E614C4 mov esi,esp
00E614C6 mov eax,dword ptr [__imp_std::endl (0E6A314h)]
00E614CB push eax
00E614CC mov edi,esp
00E614CE mov ecx,dword ptr [p]
00E614D1 mov edx,dword ptr [ecx]
00E614D3 push edx
00E614D4 push offset string ",*p= " (0E67834h)
00E614D9 mov ebx,esp
00E614DB push 0Ah
00E614DD push offset string "a= " (0E67830h)
00E614E2 mov eax,dword ptr [__imp_std::cout (0E6A318h)]
00E614E7 push eax
...

前三句语句的汇编都比较容易理解,主要看输出语句。大体来说就是从右往左依次压栈,最后调用输出函数。
第12行表示把回车符压栈,第16句把*p的值压栈,第17句把字符串",*p"压栈,然后第19句就是把a的值压栈。

1
19 00E614DB  push        0Ah

在这句话中,压栈的是一个常数0Ah,即十进制的10。这就解释了为什么最后的输出结果是a= 10,*p= 20

所以对于上面的程序,在编译过程中,会把所有的const变量名替换成变量值。a的值事实上是被改变了,所以*p的值是20,在调试时监视窗口中a的值也是20,但是由于在编译过程中就已经进行了替换,所以输出里面,a还是10。

看一个例子:

1
2
int b = 1;
int c = a + b;

编译结果:

1
2
3
4
5
6
  int b = 1;
013114C4 mov dword ptr [b],1
int c = a + b;
013114CB mov eax,dword ptr [b]
013114CE add eax,0Ah
013114D1 mov dword ptr [c],eax

可以看到编译之后把程序中出现的常变量都替换成了该变量的值。

如果在程序中加上这么一句,结果会是怎么样呢:

1
int c = a + 1;

结果是:

1
2
	int c = a + 1;
012914C4 mov dword ptr [c],0Bh

可见在编译过程中,如果是常变量跟立即数的运算,就会先计算出运算结果,再把包含常变量的整个运算式子替换成运算结果。

const - C vs. C++

文首给出的程序是C++代码,那么C对const的处理是否一样呢?

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(void)
{

const int a = 10;
int *p = (int*)(&a);
*p = 20;
printf("a= %d,*p= %d\n", a, *p);
return 0;
}

把上面的代码保存成.c文件,用Visual Studio编译运行,结果是:

1
a= 20,*p= 20

三观尽毁。。

看一下编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	const int a = 10;
00B4139E mov dword ptr [a],0Ah
int *p = (int*)(&a);
00B413A5 lea eax,[a]
00B413A8 mov dword ptr [p],eax
*p = 20;
00B413AB mov eax,dword ptr [p]
00B413AE mov dword ptr [eax],14h
printf("a= %d,*p= %d\n", a, *p);

00B413B4 mov esi,esp
00B413B6 mov eax,dword ptr [p]
00B413B9 mov ecx,dword ptr [eax]
00B413BB push ecx
00B413BC mov edx,dword ptr [a]
00B413BF push edx
00B413C0 push offset string "a= %d,*p= %d\n" (0B4573Ch)
00B413C5 call dword ptr [__imp__printf (0B482B0h)]
00B413CB add esp,0Ch
00B413CE cmp esi,esp
00B413D0 call @ILT+295(__RTC_CheckEsp) (0B4112Ch)

看第14、15行,这里并没有把a替换成10,而是直接去取a的值,所以才会有这样的输出结果。

把上面代码保存成.cpp文件,用Visual Studio编译运行,结果是:

1
a= 10,*p= 20

看一下编译结果,大家来找茬。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	const int a = 10;
00FF139E mov dword ptr [a],0Ah
int *p = (int*)(&a);
00FF13A5 lea eax,[a]
00FF13A8 mov dword ptr [p],eax
*p = 20;
00FF13AB mov eax,dword ptr [p]
00FF13AE mov dword ptr [eax],14h
printf("a= %d,*p= %d\n", a, *p);

00FF13B4 mov esi,esp
00FF13B6 mov eax,dword ptr [p]
00FF13B9 mov ecx,dword ptr [eax]
00FF13BB push ecx
00FF13BC push 0Ah
00FF13BE push offset string "a= %d,*p= %d\n" (0FF573Ch)
00FF13C3 call dword ptr [__imp__printf (0FF82B0h)]
00FF13C9 add esp,0Ch
00FF13CC cmp esi,esp
00FF13CE call @ILT+295(__RTC_CheckEsp) (0FF112Ch)

可以看到,唯一的区别在于第14行,这里的处理方式是上一节说的一样,在编译时把a替换成了它的值。

不了解Visual Studio的编译器跟gcc和g++的关系,用gcc和g++试了一下,发现gcc得到的结果是a=20,g++得到的结果是a=10

小结

经过一番折腾,发现一个简单的const,并非看起来这么简单。简单小结一下,可能描述不尽严谨:

  • C中,编译过程不把const修饰的变量替换
  • C++中,编译时把const修饰的变量替换成变量值,如果在语句中出现const变量和立即数的运算,
    则先计算出运算结果,然后把运算语句替换成运算结果

写在文末

看了一下,这篇东西第一次动笔是一周之前,现在总算是把坑给填完了。勤于思考和总结,我坚信这肯定是有好处的,要坚持下去。

在本科期间,我曾经把const这个词打印在一张A4纸上,贴在书桌上。这举动源自大一学生节的一个DV节目,《on the way》,剧中男主浑浑噩噩四年过去,然后找工作四处碰壁。一次面试中,考官问const是什么意思,他支支吾吾半天后说,可能是静态变量吧。他在综体前面给妈妈打电话时强忍泪水,关电话之后放声痛哭。这场景让我很震撼。所以我打印了这么一张纸,希望能不断告诉自己不要荒废了自己的时间,真正学到东西。

本科毕业之后,搬宿舍时,这张纸被撕掉了,没有带到新宿舍。我觉得放在心中比放在哪都更重要。这篇文章,也算是提醒一下自己吧~

Fight and move forward!