前言

前几天女票问了我一个阿里的面试题,是有关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 = ; const int i = ; //定义常量i
int *j = (int *) &i; //看到这里能对i进行取值,判断i必然后自己的内存空间
*j = ; //对j指向的内存进行修改
printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //观看实验效果 const int ck = ; //这个对照实验是为了观察,对常量ck的引用时,会产生的效果
int ik = ck; int i1 = ; //这个对照实验是为了区别,对常量和变量的引用有什么区别
int i2 = i1; return ;
}
下面看一下不同编译器的输出结果

vc6.0:

vs2010:

g++的输出结果:

注意:对于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)方法:打个断点调试->窗口反汇编(还有好多功能,比如内存多线程等)

(2)反汇编代码:

 --- d:\cv_projects\commontest\commontest\commontest.cpp ------------------------
#include <stdio.h>
int main()
{
push ebp
mov ebp,esp
sub esp,114h
push ebx
0131138A push esi
0131138B push edi
0131138C lea edi,[ebp-114h]
mov ecx,45h
mov eax,0CCCCCCCCh
0131139C rep stos dword ptr es:[edi]
int i0 = ;
0131139E mov dword ptr [i0],0Bh const int i = ; //定义常量i
013113A5 mov dword ptr [i], //编译器确实为常量i分配了栈空间,并赋值为0
int *j = (int *) &i; //看到这里能对i进行取值,判断i必然后自己的内存空间
013113AC lea eax,[i]
013113AF mov dword ptr [j],eax
*j = ; //对j指向的内存进行修改
013113B2 mov eax,dword ptr [j]
013113B5 mov dword ptr [eax],
printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //观看实验效果
013113BB mov esi,esp
013113BD mov eax,dword ptr [j]
013113C0 mov ecx,dword ptr [eax]
013113C2 push ecx
013113C3 push
013113C5 mov edx,dword ptr [j]
013113C8 push edx
013113C9 lea eax,[i]
013113CC push eax
013113CD push offset string "0x%p\n0x%p\n%d\n%d\n" (131573Ch)
013113D2 call dword ptr [__imp__printf (13182B0h)]
013113D8 add esp,14h
013113DB cmp esi,esp
013113DD call @ILT+(__RTC_CheckEsp) (131112Ch) const int ck = ; //这个对照实验是为了观察,对常量ck的引用时,会产生的效果
013113E2 mov dword ptr [ck], //为常量分配栈空间,这是符号表中已经有了这个变量
int ik = ck;
013113E9 mov dword ptr [ik], //看到否,对常量ck的引用,会直接替换为常量的值9,这种替换很类似于宏替换,再看下面的实验 int i1 = ; //这个对照实验是为了区别,对常量和变量的引用有什么区别
013113F0 mov dword ptr [i1],
int i2 = i1; //这里引用变量i1,对i2进行赋值,然后看到否,对常量i1引用没有替换成i1的值,而是去栈中先取出i1的值,到edx寄存器中,然后再把值mov到i2所在的内存中
013113F7 mov eax,dword ptr [i1]
013113FA mov dword ptr [i2],eax return ;
013113FD xor eax,eax
}

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

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


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

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

C++的常量折叠(一)的更多相关文章

  1. C++中的常量折叠

    先看例子: #include <iostream> using namespace std; int main() { ; int * p = (int *)(&a); *p = ...

  2. 常量折叠 const folding

    http://bbs.byr.cn/#!article/CPP/86336?p=1 下列代码给出输出结果: #include"stdafx.h" #include <iost ...

  3. const常量折叠

    首先来看一个例子: int main(int argc, char* argv[]) { ; int *j = (int *) &i; *j=; cout<<&i<& ...

  4. C++的常量折叠(二)

    前面的C++的常量折叠(一)的最后留下了一个问题,那就是在声明i的时候,加上修饰符volatile关键字,发现结果输出的就不一样了,下面来说一下volatile这个关键字. C/C++中的volati ...

  5. java之常量折叠

    为什么会写着篇博客,因为昨天看了关于final关键字的解析.但是有个问题始终没有得到解决,于是请教了我qq上之前添加的知乎大神.他给我回复的第一条消息:常量折叠.身为渣渣猿的我立马查询了这个概念.这是 ...

  6. C++高级进阶 第四季:const具体解释(二) 常量折叠

    一.文章来由 const具体解释之二 二.const 取代 #define const最初动机就是取代 #define. const 优于 #define: (1) #define没有类型检查,con ...

  7. const常量,常量折叠,字面常量

    const int a=10: 涉及到一个叫常量折叠的概念(认为我这说得太简单或者不好理解的可以google一下它获取更多信息), 即编译器虽然会给a分配空间(如果取a的地址进行操作的时候,会强迫编译 ...

  8. Python优化机制:常量折叠

    英文:https://arpitbhayani.me/blogs/constant-folding-python 作者:arprit 译者:豌豆花下猫("Python猫"公众号作者 ...

  9. C++的常量折叠(三)

    背景知识 在开始之前先说一下符号表,这个编译器中的东西.下面看一下百度百科中的描述: 符号表是一种用于语言翻译器中的数据结构.在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如 ...

随机推荐

  1. GDKOI2015 Day2

    P1 题目描述: 给出一个二分图,选择互不相交的边,使得边覆盖的点权和最大. solution: 简单DP,用树状数组维护最大值. 时间复杂度:$O(n \log n) $ P2 题目描述: 给出N个 ...

  2. 清风注解-Swift程序设计语言

    前言 Apple 发布了全新的 Swift 程序设计语言,用来开发 iOS 和 OS X 平台的应用程序.其目的不言而喻:就是为了给老迈的 Objective-C 一个合适接班人!因此,不难预见,未来 ...

  3. linux中读写锁的rwlock介绍-nk_ysg-ChinaUnix博客

    linux中读写锁的rwlock介绍-nk_ysg-ChinaUnix博客 linux中读写锁的rwlock介绍 2013-02-26 13:59:35 分类: C/C++   http://yaro ...

  4. UVA507-- Jill Rides Again

    题目链接:http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...

  5. 前端开发工具—fiddle

  6. 利用sass构建组件化的ui库

    创建公用的Sass项目模板 在做项目时,不管什么项目,他们之间总是有一些可以共用的部分.比如说重置样式.公用样式.模块组件.UI库等.那么在Sass项目中也是如此.为了避免在每个项目中做一些相同的事情 ...

  7. spring mvc 返回json数据的四种方式

    一.返回ModelAndView,其中包含map集 /* * 返回ModelAndView类型的结果 * 检查用户名的合法性,如果用户已经存在,返回false,否则返回true(返回json数据,格式 ...

  8. hadoop 配置文件注意问题

    一定要配置成hostname形式: 如伪分布:配成localhost:9000 完全分布:配成big1:9000

  9. C++类的常成员函数

    让一个成员函数带上常量性是什么意思呢?通常的答案是,一个常成员函数不会更改其class对象.这是一种平凡的表述,而编译器实现的手法也相当平凡. 任何非静态成员函数其实都被编译器隐式插入了一个指针类型的 ...

  10. Log4net 自定义字段到数据库(二)

    这种方法比第一种方法麻烦些 Log4Net.config <?xml version="1.0" encoding="utf-8" ?> <c ...