【学习笔记】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,从而应用能验证用户身 ...
随机推荐
- 用 VS Code 搞 Qt6:让信号和槽自动建立连接
Qt 具备让某个对象的信号与符合要求的槽函数自动建立连接.弄起来也很简单,只要调用这个静态方法即可: QMetaObject::connectSlotsByName(...); connectSlot ...
- SpringBoot&MyBatisPlus
5. SpringBoot 学习目标: 掌握基于SpringBoot框架的程序开发步骤 熟练使用SpringBoot配置信息修改服务器配置 基于SpringBoot完成SSM整合项目开发 5.1 入门 ...
- 【k8s连载系列】k8s介绍
k8s是Kubernetes的缩写,Google 于 2014 年开源了 Kubernetes 项目. 一.k8s的历史演变 k8s的演变过程:首先从传统的服务-->虚拟机部署-->容器部 ...
- 搜索"xxxx"的进程,同时杀进程
一.搜索"xxxx"的进程,同时杀进程,命令如下: ps -ef|grep xxxx|grep -v 'grep'|awk '{print $2}'|xargs kill -9 命 ...
- Linux操作系统,笔录!
1.Linux 1.1.Linux介绍: Linux是一套免费使用和自由传播的类Unix操作系统,是一个基于POSIX和UNIX的多用户.多任务.支持多线程和多CPU的操作系统.它能运行主要的UNIX ...
- 【翻译】Spring Security抛弃了WebSecurityConfigurerAdapter
原文链接:Spring Security without the WebSecurityConfigurerAdapter 作者:ELEFTHERIA STEIN-KOUSATHANA 发表日期:20 ...
- 什么是 X.509 证书以及它是如何工作的?
X.509 证书是基于广泛接受的国际电信联盟 (ITU) X.509 标准的数字证书,该标准定义了公钥基础设施 (PKI) 证书的格式. 它们用于管理互联网通信和计算机网络中的身份和安全. 它们不显眼 ...
- Quartz的使用
Quartz的使用 可以下载该项目进行测试查看:https://gitee.com/zhou-jiahao/quartz_demoq 1 初始Quartz 如果你的定时任务没有分布式需求,但需要对任务 ...
- C++初阶(类的访问权限以及封装+this指针+构造函数+析构函数+拷贝构造函数+参数列表+友元+内部类)
面向过程与面向对象 C语言是面向过程的,关注的是过程(函数),分析出求解问题的步骤,通过函数调用逐步解决问题. C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成. ...
- 同步与异步、阻塞与非阻塞、创建进程的多种方式、进程间数据隔离、进程的join方法、IPC机制等
目录 同步与异步 阻塞与非阻塞 综合使用 创建进程的多种方式 进程间数据隔离 进程的join方法 IPC机制 生产者消费者模型 进程对象的多种方法 守护进程 僵尸进程与孤儿进程 多进程数据错乱问题 同 ...