C++的常量折叠(一)


前言


前几天女票问了我一个阿里的面试题,是有关C++语言的const常量的,其实她一提出来我就知道考察的点了:肯定是const常量的内存不是分配在read-only的存储区的,const常量的内存分配区是很普通的栈或者全局区域。也就是说const常量只是编译器在编译的时候做检查,根本不存在什么read-only的区域。

所以说C++的const常量和常量字符串是不同的,常量字符串是存储在read-only的区域的,他们的具体的存储区域是不同的。

 

就好像杨立翔老师在上课举得那个例子(讲的东西忘得差不多了,但是例子都记得,什么撕户口本,月饼模子之类的):

场景:老式的电影放映机,门口的检票员,最后排那个座位上的放映师傅,好多的观众席。

你的电影票上有你的名字,检票员的手里有个名字和座位号的对应表,检票员不允许观众乱坐位置。

所以游戏就这样开始了,电影院的每一个观众席都是const常量类型的,检票员就是编译器,在观众进门的时候,每个人都必须坐自己票上的位子,也就是说观众必须拿着自己的票进门。你拿着别人的票就不让你进去,所以就不能更换座位。

类比到C++语言上,场景应该是这样的:对于哪些const常量,在编译的时候就已经决定了,他们在内存的存储位置,你如果直接改这个常量的值是不合法的。就好像你不能拿着一张别人的电影票进电影院,因为检票员手里有一张座位号到姓名的映射表,他知道哪个座位该坐谁,并且不让观众随意调换(这个例子尼玛真的好形象啊,杨老师说的没有这么好,我后期根据自己的理解稍微添油加醋)。其实检票员手里的那张映射表就是编译器的符号表,是不是真的很形象,关于这个符号表后面说。

那么新的问题来了,观众进到电影院以后,我跟旁边的哥们说一声,我给你100块,你给我换一下位置吧,如果他同意了,我们就能换。检票员管不着,进都进来了,还能怎样。所以两个人就调换了位子。

C++的const常量一样是这个样子,编译器在编译的时候要查看它手里的符号表,如果你拿的是一个const常量,并且你要修改这个常量的值,他就拒绝。但是这种检查值会发生在编译器,如果我能绕过编译器,在运行的时候修改这个const的值,将会是很easy的一件事,就像前面说的,我给那哥们100块就搞定了。所以说const常量的存储空间同其他的变量的存储空间没有任何的区别,它的这种常量不允许就该的检查只发生在编译器的编译阶段。

但是常量字符串的存储位置就不同了,它的存储位置是read-only的区域,就像电影院最后的那个放映师的位置,那里是一个特殊的位置,是真的不允许随便的调换,你给他1000也不给你换,因为师傅要在那个位置放映,不然没法看电影。当然了,实现这种read-only的存储区域也很简单,把那个内存的页(page)的属性标记为只读的就好了。

对了,当时我还想起了这个:

const int *p1; /* p1所指向的int变量值不可改变,为常量,但可以改变p1指针的值 */
int * const p2; /* p2指针为常量,即p2的值不可改变,但可以改变p2指向对象的值 */
const int * const p3; /* p3指针是常量,同时p3所指向int对象的值也是常量 */


啰啰嗦嗦说这么长一个例子,其实很简单的道理,就是为了好玩,好记。先宏观的说一下const常量的实现机制,下面说一些具体的实现。


其实女票提出的问题不算难,是C++语言的一个知识点,总而言之就是一个常量折叠。

先说一个错误的理解(为什么要说一个错误的理解,因为它有助于正确的理解,哈哈):可折叠的常量就像宏一样,在预编译阶段对const常量的引用一律被替换为常量所对应的值。就和普通的宏替换没有什么区别,并且编译器不会为该常量分配存储空间。

看清楚了,上面说的是一个错误的理解,常量折叠确实会像宏替换一样把对常量的引用替换为常量对应的值,但是该常量是分配空间的,并且靠编译器来检查常量属性。

#define PI 3.14
int main()
{
    const int r = 10;
    
    int p = PI;//这里在预编译阶段产生宏替换,PI直接替换为3.14,其实就是int p = 3.14
    int len = 2 * r;//这里会发生常量折叠,也就是说常量r的引用会替换成它对应的值,相当于int len = 2 * 10;
    return 0;
}

如上述代码中所述,常量折叠表面上的效果和宏替换是一样的,只是,“效果上是一样的”,而两者真正的区别在于,宏是字符常量,在预编译阶段的宏替换完成后,该宏名字会消失,所有对宏如PI的引用已经全部被替换为它所对应的值,编译器当然没有必要再维护这个符号。而常量折叠发生的情况是,对常量的引用全部替换为该常量如r的值,但是,常量名r并不会消失,编译器会把他放入到符号表中,同时,会为该变量分配空间,栈空间或者全局空间。既然放到了符号表中,就意味着可以找到这个变量的地址(埋一个伏笔先)。

符号表不是一张表,是一系列表的统称,这里的const常量,会把这个常量的名字、类型、内存地址、都放到常量表中。符号表还有一个变量表,这个表放变量的名字、类型、内存地址,但是没有放变量的值。

为了更能体现出常量折叠,看下面的对比实验:

int main()
{int i0 = 11;const int i = 0;         //定义常量iint *j = (int *) &i;   //看到这里能对i进行取值,判断i必然后自己的内存空间*j = 1;                  //对j指向的内存进行修改printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //观看实验效果const int ck = 9;     //这个对照实验是为了观察,对常量ck的引用时,会产生的效果int ik = ck;int i1 = 5;           //这个对照实验是为了区别,对常量和变量的引用有什么区别int i2 = i1;return 0;
}
下面看一下不同编译器的输出结果

vc6.0:

image

vs2010:

image

g++的输出结果:

image

注意:对于Linux的GUN中的gcc是用来编译.c文件的C语言编译器,g++是用来编译.cpp的C++语言编译器。

我们这里讲的是C++的商量折叠,所以源文件要是.cpp的才可以。(C语言的const常量最后再说)


上面的程序的运行结果至少说明两点:

(1)i和j地址相同,指向同一块空间,i虽然是可折叠常量,但是,i确实有自己的空间

(2)i和j指向同一块内存,但是*j = 1对内存进行修改后,按道理来说,*j==1,i也应该等于1,而实验结果确实i实实在在的等于0。
这是为什么呢,就是本文所说的内容,i是可折叠常量,在编译阶段对i的引用已经别替换为i的值了,同时不同于宏替换的是,这个i还被存到了常量表中。
也就是说:

printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j);

中的i在预处理阶段已经被替换,其实已经被改为:

printf("0x%p\n0x%p\n%d\n%d\n",&i,j,0,*j);

同时在常量表中也有i这个变量,不然的话对i取地址是不合法的,这是和宏替换的不同点,宏替换是不会把宏名称放到常量表中的,预编译完就用不到了。

为了更加直观,下面直接上这个程序的反编译的汇编语言:

(1)方法:打个断点调试->窗口反汇编(还有好多功能,比如内存多线程等)

imageimage

(2)反汇编代码:

 1 --- d:\cv_projects\commontest\commontest\commontest.cpp ------------------------
 2 #include <stdio.h>
 3 int main()
 4 {
 5 01311380  push        ebp  
 6 01311381  mov         ebp,esp  
 7 01311383  sub         esp,114h  
 8 01311389  push        ebx  
 9 0131138A  push        esi  
10 0131138B  push        edi  
11 0131138C  lea         edi,[ebp-114h]  
12 01311392  mov         ecx,45h  
13 01311397  mov         eax,0CCCCCCCCh  
14 0131139C  rep stos    dword ptr es:[edi]  
15     int i0 = 11;
16 0131139E  mov         dword ptr [i0],0Bh  
17 
18     const int i = 0;         //定义常量i
19 013113A5  mov         dword ptr [i],0    //编译器确实为常量i分配了栈空间,并赋值为0  
20     int *j = (int *) &i;   //看到这里能对i进行取值,判断i必然后自己的内存空间
21 013113AC  lea         eax,[i]  
22 013113AF  mov         dword ptr [j],eax  
23     *j = 1;                  //对j指向的内存进行修改
24 013113B2  mov         eax,dword ptr [j]  
25 013113B5  mov         dword ptr [eax],1  
26     printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //观看实验效果
27 013113BB  mov         esi,esp  
28 013113BD  mov         eax,dword ptr [j]  
29 013113C0  mov         ecx,dword ptr [eax]  
30 013113C2  push        ecx  
31 013113C3  push        0  
32 013113C5  mov         edx,dword ptr [j]  
33 013113C8  push        edx  
34 013113C9  lea         eax,[i]  
35 013113CC  push        eax  
36 013113CD  push        offset string "0x%p\n0x%p\n%d\n%d\n" (131573Ch)  
37 013113D2  call        dword ptr [__imp__printf (13182B0h)]  
38 013113D8  add         esp,14h  
39 013113DB  cmp         esi,esp  
40 013113DD  call        @ILT+295(__RTC_CheckEsp) (131112Ch)  
41 
42     const int ck = 9;     //这个对照实验是为了观察,对常量ck的引用时,会产生的效果
43 013113E2  mov         dword ptr [ck],9  //为常量分配栈空间,这是符号表中已经有了这个变量
44     int ik = ck;
45 013113E9  mov         dword ptr [ik],9  //看到否,对常量ck的引用,会直接替换为常量的值9,这种替换很类似于宏替换,再看下面的实验
46 
47     int i1 = 5;           //这个对照实验是为了区别,对常量和变量的引用有什么区别
48 013113F0  mov         dword ptr [i1],5  
49     int i2 = i1;    //这里引用变量i1,对i2进行赋值,然后看到否,对常量i1引用没有替换成i1的值,而是去栈中先取出i1的值,到edx寄存器中,然后再把值mov到i2所在的内存中
50 013113F7  mov         eax,dword ptr [i1]  
51 013113FA  mov         dword ptr [i2],eax  
52 
53     return 0;
54 013113FD  xor         eax,eax  
55 }
View Code

通过上述实验的分析可以容易看出,对可折叠的常量的引用会被替换为该常量的值,而对变量的引用就需要访问变量的内存。

总结:常量折叠说的是,在编译阶段,对该变量进行值替换,同时,该常量拥有自己的内存空间,并非像宏定义一样不分配空间,需澄清这点


前面说了个不许用gcc编译.c的C语言的程序,不然就没有了常量折叠的问题,先看一下执行的结果:

image

最后说一下gcc编译的C语言的const常量,这里并没有做常量折叠的这种优化,类似于const常量前面加上volatile这个关键字。具体是怎么回事?下一篇博客再说。

转载于:https://www.cnblogs.com/stemon/p/4406824.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/400573.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【机器视觉学习笔记】二值图像和灰度图像的膨胀、腐蚀、开运算、闭运算算法(C++)

目录二值图像原理腐蚀结构的原点设置在结构内部情况举例结构的原点设置在结构的外部情况举例膨胀结构的原点设置在结构的内部情况举例结构的原点设置在结构的外部情况举例开闭运算完整源码效果原图腐蚀运算膨胀运算开运算闭运算灰度图像原理腐蚀膨胀开闭运算完整源码效果原图腐…

【机器视觉学习笔记】二值图像连通区域提取算法(C++)

目录原理二值图像连通区域&#xff08;Connected Component&#xff09;连通区域分析&#xff08;Connected Component Analysis,Connected Component Labeling&#xff09;算法&#xff1a;Two-Pass&#xff08;两遍扫描法&#xff09;思路&#xff1a;Two-Pass算法的简单步骤…

【机器视觉学习笔记】Harris 角点检测算法(C++)

目录原理算法步骤优缺点源码效果原图输出平台&#xff1a;Windows 10 20H2 Visual Studio 2015 OpenCV 4.5.3 本文摘自2、Harris角点检测算法 —— 诺亚方舟369 原理 Harris算子是对Moravec算子的改进&#xff0c;包括&#xff1a; &#xff08;1&#xff09;Harris算子用…

剖析ASSERT函数

2019独角兽企业重金招聘Python工程师标准>>> &#xfeff;&#xfeff; 搜集与总结了assert函数的用法&#xff0c;记于此&#xff0c;以备后续与查找使用&#xff1a; ****身在Windows的世界&#xff0c;但还是深爱着我的它---Linux****I love Linux forever***** …

MSP430G2553 移植 Contiki RTOS 实时操作系统

目录新建工程移植过程关于时间多任务示例main.c内存使用情况效果平台&#xff1a;Code Composer Studio 10.4.0 MSP-EXP430G2 LaunchPad 试验板 MSP430G2553 LaunchPad™ Development Kit (MSP‑EXP430G2ET) contiki下载&#xff1a;contiki-os Github 本文参考自二、Contiki…

Raphael JS 矢量客户端开源框架

之前有基于svg进行矢量绘制&#xff0c;当时是自己写的一些js类库&#xff0c;最近项目中需要&#xff0c;对Raphael做了一些了解&#xff0c;总体不错。 Raphael可以对浏览器的兼容做的不错&#xff0c;基于IE的会自动使用vml进行绘制&#xff0c;其他浏览器基于svg进行绘制&a…

mac os x10.8下如何使用git与github

2019独角兽企业重金招聘Python工程师标准>>> 1、准备工作&#xff1a; 下载安装git客户端 http://code.google.com/p/git-osx-installer/downloads/list?can3(安装了git客户端&#xff0c;命令行中才有git命令) 注册github账号 https://github.com/ -->Pricing …

使用 VS 附加到进程 调试发布的网站

适用场景&#xff1a;调试已发布的网站。 1、把项目 bin 目录下的 pdb 文件复制到服务器上相应网站的 bin 目录下。 2、把VS的调试工具中的 x64 文件夹复制到服务器任意位置。 3、服务器中打开调试工具&#xff0c;并配置端口&#xff08;注意端口是否可用&#xff09;。 4、在…

Github git 命令下载加速

目录手动输入法批处理文件系统&#xff1a;Windows 10 20H2 手动输入法 在要下载到的目录按住shift右键 选择在此处打开Powershell窗口 如要下载的仓库地址&#xff1a; 通常的git命令下载&#xff1a; git clone https://github.com/apache/incubator-nuttx.gitCNPMJS.ORG…

MySql 自动更新时间为当前时间

字段类型 : timestamp默认值 : CURRENT_TIMESTAMP如果是创建时间&#xff0c;则下面的“根据当前时间戳更新”不勾选。转载于:https://blog.51cto.com/9625815/1630767

ZKP Mathematical Building Blocks (2)

MIT IAP 2023 Modern Zero Knowledge Cryptography课程笔记 Lecture 3: Mathematical Building Blocks (Yufei Zhao) Fiat Shamir heuristic Turn an interactive proof to a non-interactive proofP can simulate V whenever V picks a random valueP can simulate V’s ran…

【AVR ASF4库函数学习笔记】一、使用Microchip Studio图形化配置工程

目录新建工程配置点灯效果平台&#xff1a;Microchip Studio (原Atmel Studio 7) 欣世纪 DMAVR-L Atmega 128A - AU 新建工程 选择所用的芯片后点击CREATE NEW PROJECT创建工程 配置 设置CPU 根据所用晶振设置频率 点灯示例&#xff1a; D7由PE7控制&#xff0c;低电平点…

Jenkins部署Web项目到远程tomcat

原网址&#xff1a;http://blog.sina.com.cn/s/blog_b5fe6b270102v7xi.html 之前讲到的是如何构建一个项目&#xff0c;并且将代码进行编译、打包&#xff0c;那么打包完成最后的结果就需要发布到应用服务器&#xff0c;将项目部署成功。在之前的项目中我们采用的shell脚本来部…

奔跑的小车……

转载于:https://www.cnblogs.com/hebaichuanyeah/archive/2013/06/07/3124244.html

【机器视觉学习笔记】OpenCV C++的安装、配置及多版本共存 (VS2015)

目录下载&#xff08;可选 —— 二选一&#xff09;添加环境变量新建项目和配置配置包含目录配置库目录添加库&#xff08;可选 —— 二选一&#xff09;添加dll文件的路径测试平台&#xff1a;Windows 10 20H2 Visual Studio 2015 OpenCV 2.4.13.6 本文参考自最新opencv-c安…

Java环境搭建若干问题

2019独角兽企业重金招聘Python工程师标准>>> 0.总体说明 本次搭建环境&#xff0c;为了偷懒&#xff0c;使用的是&#xff0c;阿里云镜像。 自带了Nginx、Tomcat、JDK等。 比较坑爹的是&#xff0c;虽然镜像带了很多安装好的软件&#xff0c;但是也有各种问题&#…

JAVASCRIPT 等比例缩放图片 限定最大宽度和最大高度

在Web上显示图片&#xff0c;通常都会有图片显示比例问题&#xff0c;如果不给<img />限制width和height&#xff0c;那么如果图片大了就会将整个页面挤乱&#xff0c;图片小了又会使图片失真。 1、预先定义好图片显示的标准宽度和高度。 2、如果图片的大小超过了…

【机器视觉学习笔记】python安装OpenCV并设置自动补全及代码提示

目录安装测试设置自动补全及代码提示平台&#xff1a;Windows 10 20H2 Python 3.8.12 (default, Oct 12 2021, 03:01:40) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32 OpenCV 4.5.4.58 安装 本节内容引自使用pip安装Opencv —— xwc2 在要安装的环境内使用如下…

Android背景色渐变效果(shape,gradient) (转)

Android设置背景色可以通过在res/drawable里定义一个xml,如下&#xff1a; [代码]xml代码&#xff1a; <?xml version"1.0" encoding"utf-8"?> <shape xmlns:android"http://schemas.android.com/apk/res/android"> <gradie…

C语言字符串数字提取函数,支持负数、浮点数、科学记数法

目录效果普通数字小数科学记数法源码平台&#xff1a; STC89C52 Keil uVision V5.29.0.0 PK51 Prof.Developers Kit Version:9.60.0.0 效果 这里以51单片机为例&#xff0c;对串口接收的字符串中的第三个数进行解码&#xff1a; 普通数字 小数 科学记数法 源码 /* * Str2N…