【学习笔记】C++ 常量折叠原理和验证

以下的代码很有意思,在相同时刻,相同的内存地址,数据居然会不一样。

#include <iostream>

int main(void)
{
	const int const_val = 3;

	int *nomal_pot = (int*)&const_val;
	*nomal_pot = 9;

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);

	return 0;
}

运行结果: 

const_val: 0x002DF7B0 -> 3
nomal_pot: 0x002DF7B0 -> 9

造成如此神奇的现象,原因是因为 C++ 常量折叠的特性(C没有),这跟 C++ 编译器相关,在编译阶段,会将代码中所有引用该常量的地方,都会替换为常量的初始值,就像宏定义一样,那么上述代码中的第一句代码:

printf("const_val: 0x%p -> %d\n", &const_val, const_val);

会被编译器修改等同为以下代码(实际以汇编体现): 

printf("const_val: 0x%p -> %d\n", &const_val, 3);

与宏定义不同的是,编译会为常量 const_val 分配内存空间,并且该内存空间处于栈空间,并不是只读的,因此在运行时仍可以通过以下代码修改该内存空间的数据:

int *nomal_pot = (int*)&const_val;
*nomal_pot = 9;

这就出现了同一时间,在相同的 “内存地址” 中输出了不同的值。

以上两点可以通过VS2017查看反汇编的汇编代码即可确认:

#include <iostream>

int main(void)
{
00EA18C0  push        ebp  
00EA18C1  mov         ebp,esp  
00EA18C3  sub         esp,0DCh  
00EA18C9  push        ebx  
00EA18CA  push        esi  
00EA18CB  push        edi  
00EA18CC  lea         edi,[ebp-0DCh]  
00EA18D2  mov         ecx,37h  
00EA18D7  mov         eax,0CCCCCCCCh  
00EA18DC  rep stos    dword ptr es:[edi]  
00EA18DE  mov         eax,dword ptr [__security_cookie (0EAA004h)]  
00EA18E3  xor         eax,ebp  
00EA18E5  mov         dword ptr [ebp-4],eax  
00EA18E8  mov         ecx,offset _2EA97ABB_consoleapplicationtest@cpp (0EAC027h)  
00EA18ED  call        @__CheckForDebuggerJustMyCode@4 (0EA121Ch)  
	const int const_val = 3;
00EA18F2  mov         dword ptr [const_val],3   // 常量赋值为初始值 3 

	int *nomal_pot = (int*)&const_val;
00EA18F9  lea         eax,[const_val]           // 提取常量所在的内存地址
00EA18FC  mov         dword ptr [nomal_pot],eax // 赋给 nomal_pot 指针

	*nomal_pot = 9;
00EA18FF  mov         eax,dword ptr [nomal_pot] // 提取 nomal_pot 指向的内存地址
00EA1902  mov         dword ptr [eax],9  // 往该内存地址写入值 9 (即 const_val 的地址) 

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00EA1908  push        3  // 这里可以看到 const_val 直接被替换为初始值 3,并未从地址取值
00EA190A  lea         eax,[const_val]  
00EA190D  push        eax  
00EA190E  push        offset string "const_val: 0x%p -> %d\n" (0EA7B30h)  
00EA1913  call        _printf (0EA104Bh)  
00EA1918  add         esp,0Ch  

	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
00EA191B  mov         eax,dword ptr [nomal_pot]  // 提取 nomal_pot 指向的内存地址
00EA191E  mov         ecx,dword ptr [eax]  // 从该内存地址提取内容,即 9 而非初始值 3
00EA1920  push        ecx  
00EA1921  mov         edx,dword ptr [nomal_pot]  
00EA1924  push        edx  
00EA1925  push        offset string "nomal_pot: 0x%p -> %d\n" (0EA7C04h)  
00EA192A  call        _printf (0EA104Bh)  
00EA192F  add         esp,0Ch  

	return 0;
00EA1932  xor         eax,eax  
}

那么以下代码,你们觉得 add_val 输出的结果会是什么呢?

#include <iostream>

int main(void)
{
	const int const_val = 3;

	int *nomal_pot = (int*)&const_val;
	*nomal_pot = 9;

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);

	int add_val = const_val + *nomal_pot;
	printf("add_val:%d\n", add_val);

	return 0;
}

按照上述分析,源码中所有对 const_val 的引用都会在编译期间替换为初始值,而 const_val 所在的内存空间是可以在运行时被修改,因此正确的答案应为 3 + 9 = 12

const_val: 0x0036FDC4 -> 3
nomal_pot: 0x0036FDC4 -> 9
add_val:12

对应的汇编代码:

#include <iostream>

int main(void)
{
001218C0  push        ebp  
001218C1  mov         ebp,esp  
001218C3  sub         esp,0E8h  
001218C9  push        ebx  
001218CA  push        esi  
001218CB  push        edi  
001218CC  lea         edi,[ebp-0E8h]  
001218D2  mov         ecx,3Ah  
001218D7  mov         eax,0CCCCCCCCh  
001218DC  rep stos    dword ptr es:[edi]  
001218DE  mov         eax,dword ptr [__security_cookie (012A004h)]  
001218E3  xor         eax,ebp  
001218E5  mov         dword ptr [ebp-4],eax  
001218E8  mov         ecx,offset _2EA97ABB_consoleapplicationtest@cpp (012C027h)  
001218ED  call        @__CheckForDebuggerJustMyCode@4 (012121Ch)  
	const int const_val = 3;
001218F2  mov         dword ptr [const_val],3  

	int *nomal_pot = (int*)&const_val;
001218F9  lea         eax,[const_val]  
001218FC  mov         dword ptr [nomal_pot],eax  
	*nomal_pot = 9;
001218FF  mov         eax,dword ptr [nomal_pot]  
00121902  mov         dword ptr [eax],9  

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00121908  push        3  
0012190A  lea         eax,[const_val]  
0012190D  push        eax  
0012190E  push        offset string "const_val: 0x%p -> %d\n" (0127B30h)  
00121913  call        _printf (012104Bh)  
00121918  add         esp,0Ch  
	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
0012191B  mov         eax,dword ptr [nomal_pot]  
0012191E  mov         ecx,dword ptr [eax]  
00121920  push        ecx  
00121921  mov         edx,dword ptr [nomal_pot]  
00121924  push        edx  
00121925  push        offset string "nomal_pot: 0x%p -> %d\n" (0127C04h)  
0012192A  call        _printf (012104Bh)  
0012192F  add         esp,0Ch  

	int add_val = const_val + *nomal_pot;
00121932  mov         eax,dword ptr [nomal_pot]  // 提取 nomal_pot 所指向的内存地址 
00121935  mov         ecx,dword ptr [eax]  // 从该内存地址中提取数值(即9)
00121937  add         ecx,3  // 将该数值与 3 相加(即 9 + 3)
0012193A  mov         dword ptr [add_val],ecx  // 将结果写入 add_val (即 12) 
	printf("add_val:%d\n", add_val);
0012193D  mov         eax,dword ptr [add_val]  
00121940  push        eax  
00121941  push        offset string "add_val:%d\n" (0127B48h)  
00121946  call        _printf (012104Bh)  
0012194B  add         esp,8  

	return 0;
0012194E  xor         eax,eax  
}

那再略微修改一下,const 增加 volatile 修饰符,输出结果会是一样吗?

#include <iostream>

int main(void)
{
	volatile const int const_val = 3;  // 增加 volatile 修饰符

	int *nomal_pot = (int*)&const_val;
	*nomal_pot = 9;

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);

	int add_val = const_val + *nomal_pot;
	printf("add_val:%d\n", add_val);

	return 0;
}

输出结果: 

const_val: 0x0039F8F8 -> 9
nomal_pot: 0x0039F8F8 -> 9
add_val:18

原因是因为经过 volatile 修饰的变量会明确让编译器编译代码时,每次都要求生成的汇编语句都是从该变量的地址中取出数据,而不是用常量值替代,这个可以通过反汇编证实这一点:

差异的点:


// 原来的 ------------------------------------------------------------------------
	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
00121908  push        3  // 这里可以看到 const_val 直接被替换为初始值 3,并未从地址取值
0012190A  lea         eax,[const_val]  
0012190D  push        eax  
0012190E  push        offset string "const_val: 0x%p -> %d\n" (0127B30h)  
00121913  call        _printf (012104Bh)  
00121918  add         esp,0Ch

......

	int add_val = const_val + *nomal_pot;
00121932  mov         eax,dword ptr [nomal_pot]  // 提取 nomal_pot 所指向的内存地址 
00121935  mov         ecx,dword ptr [eax]  		 // 从该内存地址中提取数值(即9)
00121937  add         ecx,3  					 // 将该数值与 3 相加(即 9 + 3)
0012193A  mov         dword ptr [add_val],ecx  	 // 将结果写入 add_val (即 12)

// 现在的 ------------------------------------------------------------------------
	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
01151908  mov         eax,dword ptr [const_val]  // 注意,这里改从该内存地址取值
0115190B  push        eax  
0115190C  lea         ecx,[const_val]  
0115190F  push        ecx  
01151910  push        offset string "const_val: 0x%p -> %d\n" (01157B30h)  
01151915  call        _printf (0115104Bh)  
0115191A  add         esp,0Ch  

......

	int add_val = const_val + *nomal_pot;
01151934  mov         eax,dword ptr [const_val]  // 提取 const_val 所指向的内存地址 
01151937  mov         ecx,dword ptr [nomal_pot]	 // 提取 nomal_pot 所指向的内存地址  
0115193A  add         eax,dword ptr [ecx]  		 // 注意,将两者数据相加(即 9 + 9)
0115193C  mov         dword ptr [add_val],eax 	 // 将结果写入到 add_val (即 18)

完整汇编:

#include <iostream>

int main(void)
{
011518C0  push        ebp  
011518C1  mov         ebp,esp  
011518C3  sub         esp,0E8h  
011518C9  push        ebx  
011518CA  push        esi  
011518CB  push        edi  
011518CC  lea         edi,[ebp-0E8h]  
011518D2  mov         ecx,3Ah  
011518D7  mov         eax,0CCCCCCCCh  
011518DC  rep stos    dword ptr es:[edi]  
011518DE  mov         eax,dword ptr [__security_cookie (0115A004h)]  
011518E3  xor         eax,ebp  
011518E5  mov         dword ptr [ebp-4],eax  
011518E8  mov         ecx,offset _2EA97ABB_consoleapplicationtest@cpp (0115C027h)  
011518ED  call        @__CheckForDebuggerJustMyCode@4 (0115121Ch)  
	volatile const int const_val = 3;
011518F2  mov         dword ptr [const_val],3  

	int *nomal_pot = (int*)&const_val;
011518F9  lea         eax,[const_val]  
011518FC  mov         dword ptr [nomal_pot],eax  
	*nomal_pot = 9;
011518FF  mov         eax,dword ptr [nomal_pot]  
01151902  mov         dword ptr [eax],9  

	printf("const_val: 0x%p -> %d\n", &const_val, const_val);
01151908  mov         eax,dword ptr [const_val]  
0115190B  push        eax  
0115190C  lea         ecx,[const_val]  
0115190F  push        ecx  
01151910  push        offset string "const_val: 0x%p -> %d\n" (01157B30h)  
01151915  call        _printf (0115104Bh)  
0115191A  add         esp,0Ch  
	printf("nomal_pot: 0x%p -> %d\n", nomal_pot, *nomal_pot);
0115191D  mov         eax,dword ptr [nomal_pot]  
01151920  mov         ecx,dword ptr [eax]  
01151922  push        ecx  
01151923  mov         edx,dword ptr [nomal_pot]  
01151926  push        edx  
01151927  push        offset string "nomal_pot: 0x%p -> %d\n" (01157C04h)  
0115192C  call        _printf (0115104Bh)  
01151931  add         esp,0Ch  

	int add_val = const_val + *nomal_pot;
01151934  mov         eax,dword ptr [const_val]  
01151937  mov         ecx,dword ptr [nomal_pot]  
0115193A  add         eax,dword ptr [ecx]  
0115193C  mov         dword ptr [add_val],eax  
	printf("add_val:%d\n", add_val);
0115193F  mov         eax,dword ptr [add_val]  
01151942  push        eax  
01151943  push        offset string "add_val:%d\n" (01157B48h)  
01151948  call        _printf (0115104Bh)  
0115194D  add         esp,8  

	return 0;
01151950  xor         eax,eax  
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值