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

#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. Element Ui 安装以及配置

    npm 安装 推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用. npm i element-ui -S 引入 Element 你可以引入整个 Element,或是根据需要 ...

  2. Python基础之模块:3、os模块 sys模块 json模块 json实战

    目录 一.os模块 1.创建目录 2.删除目录 3.查看指定路径下目录内容 4.删除/重命名文件 5.获取/切换当前目录 6.动态获取项目根路径 7.拼接/切割路径 8.判断文件.目录是否存在 9.判 ...

  3. Perl引用

    引用就是C语言中的指针,perl引用是一个标量类型可以指向变量.数组.哈希表(也叫关联数组)甚至子程序,可以应用在程序的任何地方. 在变量前面加一个\就得到了这个变量的一个引用 #!usr/bin/p ...

  4. css属性样式整合

    font-size 字体大小 常用色值:#000 黑色:#fff 白色:#ccc.#333.#2f2f2f.#666.#ddd 灰色: rgb表示法:color:rgb(255,255,255); r ...

  5. 写一个linux平台的桌面宠物

    效果图 前言 我一直在用python 写一下有趣的东西,让编程不那么无聊,之前一直有写一个桌面宠物的想法,无奈这些都是依赖资源文件,没有图片资源没办法写里面的逻辑,直到我看见了 shimiji手机桌面 ...

  6. 谁说.NET没有GC调优?只改一行代码就让程序不再占用内存

    经常看到有群友调侃"为什么搞Java的总在学习JVM调优?那是因为Java烂!我们.NET就不需要搞这些!"真的是这样吗?今天我就用一个案例来分析一下. 昨天,一位学生问了我一个问 ...

  7. 打印菱形-java

    public class WeekendDemo01 { /** 打印菱形 * * * *** * ***** * *** * * */ public static void main(String[ ...

  8. 【zookeeper】Zookeeper相关概念、重难点(myid)、语法、使用、工具

    1234567890 1请按照我是想额度插入fvtgb6yhn7ujm8ik,9ol.

  9. Java开发如何通过IoT边缘ModuleSDK进行协议转换?

    摘要:使用ModuleSDK开发插件应用,接入其他协议设备(如HTTP请求数据),将其他协议的数据转化为MQTT协议JSON数据上报到IoTDA. 本文分享自华为云社区<[华为云IoTEdge开 ...

  10. Python面试常见算法题集锦(递归部分)

    0x1 前言 开始学习python基础的时候,有以下几种算法是面试中常见的,也是前期学习python的时候可以连带学习了解的,不卡门槛哟 0x2 实现算法的方式很多种,而算法的实现也是分程序语言的,此 ...