【学习笔记】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
}
【学习笔记】C++ 常量折叠原理和验证的更多相关文章
- Java IO学习笔记:概念与原理
Java IO学习笔记:概念与原理 一.概念 Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...
- golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息
golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放 ...
- tensorflow学习笔记——模型持久化的原理,将CKPT转为pb文件,使用pb模型预测
由题目就可以看出,本节内容分为三部分,第一部分就是如何将训练好的模型持久化,并学习模型持久化的原理,第二部分就是如何将CKPT转化为pb文件,第三部分就是如何使用pb模型进行预测. 一,模型持久化 为 ...
- [Firefly引擎][学习笔记一][已完结]带用户验证的聊天室
原地址:http://bbs.9miao.com/thread-44571-1-1.html 前言:早在群里看到大鸡蛋分享他们团队的Firefly引擎,但一直没有时间去仔细看看,恰好最近需要开发一个棋 ...
- 强化学习-学习笔记7 | Sarsa算法原理与推导
Sarsa算法 是 TD算法的一种,之前没有严谨推导过 TD 算法,这一篇就来从数学的角度推导一下 Sarsa 算法.注意,这部分属于 TD算法的延申. 7. Sarsa算法 7.1 推导 TD ta ...
- 机器学习实战(Machine Learning in Action)学习笔记————10.奇异值分解(SVD)原理、基于协同过滤的推荐引擎、数据降维
关键字:SVD.奇异值分解.降维.基于协同过滤的推荐引擎作者:米仓山下时间:2018-11-3机器学习实战(Machine Learning in Action,@author: Peter Harr ...
- Android(java)学习笔记95:Android原理揭秘系列之View、ViewGroup
作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的容器, ...
- java学习笔记 (2) —— Struts2类型转换、数据验证重要知识点
1.*Action.conversion-properties 如(point=com.test.Converter.PointListConverter) 具体操作类的配置文件 2.*Action. ...
- Android(java)学习笔记34:Android原理揭秘系列之View、ViewGroup
1. 作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的 ...
- Shiro 学习笔记(二)——shiro身份验证
身份验证: 在应用中证明他就是他本人.一般上用身份证.用户/密码 来证明. 在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身 ...
随机推荐
- centos7离线安装PHP7
环境 centos7.9 PHP7.4.30 准备工作 在编译PHP时会提示一些包版本不够或者缺少某些包,一般选择yum来安装缺少的包,但因为是离线安装,所以可以手动配置本地yum源.先看一下系统版本 ...
- jmeter执行报错:java.lang.UnsupportedClassVersionError解决办法
做个记录. 问题记录: jmeter版本:5.4.1 本地Java版本:1.8.0_151 执行jmeter,报错: 2022-10-14 12:06:27,372 ERROR o.a.j.JMete ...
- 2022-11-04 Acwing每日一题
本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...
- Go语言核心36讲13
我们已经讨论过了通道的基本操作以及背后的规则.今天,我再来讲讲通道的高级玩法. 首先来说说单向通道.我们在说"通道"的时候指的都是双向通道,即:既可以发也可以收的通道. 所谓单向通 ...
- lvm+xfs的扩缩容
ext4文件系统可以经行扩缩容操作,但xfs的文件系统只能扩容,无法缩容 所以如果需要进行xfs的缩容,可以先使用xfsdump备份文件系统,然后对逻辑卷(/分区)进行缩容操作(此时原xfs文件系统会 ...
- ROS应用层通信协议解析
参考:http://wiki.ros.org/ROS/Master_API http://wiki.ros.org/ROS/Connection Header 说明 ROS本质上就是一个松耦合的通信框 ...
- C++ 动态规划:一维动态规划,背包问题,区间动态规划
C++ 动态规划 动态规划的定义 动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程.动态规划是一种在数学.管理科学.计算机科学.经济学和生物信息学 ...
- 1. PyQt5开发环境的搭建
专栏地址 ʅ(‾◡◝)ʃ 因为我个人使用的是 Linux 还有之前用过Windows 没用过 Mac 所以这里我简单结束 Linux 和 Windows 开发环境的搭建 Windows 开发PyQt5 ...
- eclipse 无法将节点解析到句柄
将 干掉即可
- 2022年鲜为人知的CSS 特性了解起来~
前言 随着CSS的不断发展,一些很酷且有用的属性要么完全被忽视,要么由于某种原因不像其他常见属性那样被开发者熟练应用.这篇文章我们将一起学习那些CSS中陌生但非常有用的CSS属性,这些属性你可能听说过 ...