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

#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++ 常量折叠原理和验证的更多相关文章

  1. Java IO学习笔记:概念与原理

    Java IO学习笔记:概念与原理   一.概念   Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...

  2. golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息

    golang学习笔记10 beego api 用jwt验证auth2 token 获取解码信息 Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放 ...

  3. tensorflow学习笔记——模型持久化的原理,将CKPT转为pb文件,使用pb模型预测

    由题目就可以看出,本节内容分为三部分,第一部分就是如何将训练好的模型持久化,并学习模型持久化的原理,第二部分就是如何将CKPT转化为pb文件,第三部分就是如何使用pb模型进行预测. 一,模型持久化 为 ...

  4. [Firefly引擎][学习笔记一][已完结]带用户验证的聊天室

    原地址:http://bbs.9miao.com/thread-44571-1-1.html 前言:早在群里看到大鸡蛋分享他们团队的Firefly引擎,但一直没有时间去仔细看看,恰好最近需要开发一个棋 ...

  5. 强化学习-学习笔记7 | Sarsa算法原理与推导

    Sarsa算法 是 TD算法的一种,之前没有严谨推导过 TD 算法,这一篇就来从数学的角度推导一下 Sarsa 算法.注意,这部分属于 TD算法的延申. 7. Sarsa算法 7.1 推导 TD ta ...

  6. 机器学习实战(Machine Learning in Action)学习笔记————10.奇异值分解(SVD)原理、基于协同过滤的推荐引擎、数据降维

    关键字:SVD.奇异值分解.降维.基于协同过滤的推荐引擎作者:米仓山下时间:2018-11-3机器学习实战(Machine Learning in Action,@author: Peter Harr ...

  7. Android(java)学习笔记95:Android原理揭秘系列之View、ViewGroup

    作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的容器, ...

  8. java学习笔记 (2) —— Struts2类型转换、数据验证重要知识点

    1.*Action.conversion-properties 如(point=com.test.Converter.PointListConverter) 具体操作类的配置文件 2.*Action. ...

  9. Android(java)学习笔记34:Android原理揭秘系列之View、ViewGroup

    1. 作过Android 应用开发的朋友都知道,Android的UI界面都是由View和ViewGroup及其派生类组合而成的.其中,View是所有UI组件的基类,而ViewGroup是容纳这些组件的 ...

  10. Shiro 学习笔记(二)——shiro身份验证

    身份验证: 在应用中证明他就是他本人.一般上用身份证.用户/密码 来证明. 在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身 ...

随机推荐

  1. 将java装进u盘指南

    将java装入u盘指南 idea 将下载好的idea的文件夹移动到u盘中.在idea的bin目录里找到idea.properties文件,在最后添加以下两行 idea.config.path=U:/I ...

  2. prefetch和preload

    前面的话 基于VUE的前端小站改造成SSR服务器端渲染后,HTML文档会自动使用preload和prefetch来预加载所需资源,本文将详细介绍preload和prefetch的使用 资源优先级 在介 ...

  3. 线上Electron应用具备哪些特征?

    新用户购买<Electron + Vue 3 桌面应用开发>,加小册专属微信群,参与群抽奖,送<深入浅出Electron>.<Electron实战>作者签名版. 1 ...

  4. 关于Redis的,你了解多少?来看看我的笔记

    Redis 概述 Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据 ...

  5. Java8新特性—四大内置函数式接口

    Java8新特性--四大内置函数式接口 预备知识 背景 Lambda 的设计者们为了让现有的功能与 Lambda 表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念. 什么是函数式接口? 函数 ...

  6. 【lvgl】01-lvgl移植之在linux上跑

    目录 前言 linux安装SDL2 官方推荐 移植lvgl v8.0 目录框架 拉取lvgl 添加lv_conf.h和lv_drv_conf.h配置文件 lv_conf.h lv_drv_conf.h ...

  7. php变量规范命名用了记得消除,保证唯一性

    PHP中的命名规则 类的命名  在为类(class )命名前首先要知道它是什么.如果通过类名的提供的线索,还是想不起这个类是什么的话,那么就说明设计存在问题. 超过三个词组成的混合名是容易造成系统各个 ...

  8. 【OpenStack云平台】SecureCRT 连接 CentOS虚拟机

    1.安装SecureCRT SecureCRT是一款支持SSH等协议的终端仿真软件,可以在windows下登录Linux服务器,这样大大方便了开发工作.安装SecureCRT可以通过网上的各种教程安装 ...

  9. 一次MTU问题导致的RDS访问故障

    导语 VPN是一种通过公网连接两个或多个私网站点的专用网络,使得这些站点仿佛是通过专线连接在一起.IPSec是一套协议框架,用于保证数据传输的私密性,完整性,真实性.但是VPN网络经常会带来一些连通性 ...

  10. 【大数据面试】Flink 04:状态编程与容错机制、Table API、SQL、Flink CEP

    六.状态编程与容错机制 1.状态介绍 (1)分类 流式计算分为无状态和有状态 无状态流针对每个独立事件输出结果,有状态流需要维护一个状态,并基于多个事件输出结果(当前事件+当前状态值) (2)有状态计 ...