再探C++中的常量

什么是常量?相信绝大多数写过程序的人见到这两个字的第一反应就是——不能更改的量。没问题,书本上这样告诉我们,编译器也这样告诉我们,好像常量就真的是那么简单,纯粹,让人心安理得的去使用它,不需要任何顾虑。但事实并没有想象中的那么单纯。

1.常量并不一定是常量

int main()
{
    const int x=3;
}

这是一个“假常量”,当我们在下边敲出 x=4; 这样的代码时,编译器根本不会让你通过,至于为什么说它是假的,因为编译器可以被轻而易举的绕过去。

首先我们来看看表面现象,对p的一顿操作让p成功的指向了x,我们成功的绕过了编译器,获得了指向常量x的非常量指针,并且没有动用const_cast,当我们窃喜可以修改这个“常量”x的值的时候,运行结果给我们当头一棒,x的值并没有改。难道是编译器明察秋毫发现我们的小伎俩了?不,编译器真的那么聪明是不会把结果显示出来的,甚至都不会让你通过编译。那为什么x的值没有改变呢?让我们来看看汇编代码。我先吧整体的代码贴出来再节选部分进行分析。

	const int x=3;
00176E2E  mov         dword ptr [x],3  
	int y=4;
00176E35  mov         dword ptr [y],4  
	int*p=&y;
00176E3C  lea         eax,[y]  
00176E3F  mov         dword ptr [p],eax  
	p=p-(&y-&x);
00176E42  lea         eax,[y]  
00176E45  lea         ecx,[x]  
00176E48  sub         eax,ecx  
00176E4A  sar         eax,2  
00176E4D  shl         eax,2  
00176E50  mov         edx,dword ptr [p]  
00176E53  sub         edx,eax  
00176E55  mov         dword ptr [p],edx  
	*p=5;
00176E58  mov         eax,dword ptr [p]  
00176E5B  mov         dword ptr [eax],5  

	cout<<x<<endl;
00176E61  mov         esi,esp  
00176E63  mov         eax,dword ptr ds:[001802F0h]  
00176E68  push        eax  
00176E69  mov         edi,esp  
00176E6B  push        3  
00176E6D  mov         ecx,dword ptr ds:[1802ECh]  
00176E73  call        dword ptr ds:[1802F4h]  
00176E79  cmp         edi,esp  
00176E7B  call        __RTC_CheckEsp (01712D0h)  
00176E80  mov         ecx,eax  
00176E82  call        dword ptr ds:[1802F8h]  
00176E88  cmp         esi,esp  
00176E8A  call        __RTC_CheckEsp (01712D0h)  
	cout<<y<<endl;
00176E8F  mov         esi,esp  
00176E91  mov         eax,dword ptr ds:[001802F0h]  
00176E96  push        eax  
00176E97  mov         edi,esp  
00176E99  mov         ecx,dword ptr [y]  
00176E9C  push        ecx  
00176E9D  mov         ecx,dword ptr ds:[1802ECh]  
00176EA3  call        dword ptr ds:[1802F4h]  
00176EA9  cmp         edi,esp  
00176EAB  call        __RTC_CheckEsp (01712D0h)  
00176EB0  mov         ecx,eax  
00176EB2  call        dword ptr ds:[1802F8h]  
00176EB8  cmp         esi,esp  
00176EBA  call        __RTC_CheckEsp (01712D0h)  
	cout<<&x<<endl;
00176EBF  mov         esi,esp  
00176EC1  mov         eax,dword ptr ds:[001802F0h]  
00176EC6  push        eax  
00176EC7  mov         edi,esp  
00176EC9  lea         ecx,[x]  
00176ECC  push        ecx  
00176ECD  mov         ecx,dword ptr ds:[1802ECh]  
00176ED3  call        dword ptr ds:[1802FCh]  
00176ED9  cmp         edi,esp  
00176EDB  call        __RTC_CheckEsp (01712D0h)  
00176EE0  mov         ecx,eax  
00176EE2  call        dword ptr ds:[1802F8h]  
00176EE8  cmp         esi,esp  
00176EEA  call        __RTC_CheckEsp (01712D0h)  
	cout<<p<<endl;
00176EEF  mov         esi,esp  
	cout<<p<<endl;
00176EF1  mov         eax,dword ptr ds:[001802F0h]  
00176EF6  push        eax  
00176EF7  mov         edi,esp  
00176EF9  mov         ecx,dword ptr [p]  
00176EFC  push        ecx  
00176EFD  mov         ecx,dword ptr ds:[1802ECh]  
00176F03  call        dword ptr ds:[1802FCh]  
00176F09  cmp         edi,esp  
00176F0B  call        __RTC_CheckEsp (01712D0h)  
00176F10  mov         ecx,eax  
00176F12  call        dword ptr ds:[1802F8h]  
00176F18  cmp         esi,esp  
00176F1A  call        __RTC_CheckEsp (01712D0h)

不要被这一大堆乱七八糟的字符吓到,其实大多数跟我们的主题没有关系,这里全贴出来是为了让读者相信的确是所有的代码都在这了,并没有漏掉什么关键的语句,先来看下面这两行。

    const int x=3;
00176E2E  mov         dword ptr [x],3  
    int y=4;
00176E35  mov         dword ptr [y],4  

所以说为什么把这个x叫做假常量,因为有没有这个const并不影响翻译出来的汇编代码,也就是说除了我们和编译器,谁也不知道有这个const的存在,我们之所以不能无视它,是因为“严厉”的编译器决定了我们的代码能不能变成可执行的程序。这里有一点非常重要,x是在栈上有空间的,和y一样,都是栈上的变量。一定记住这句话,这很重要。在下文中,形如x这种常量我称作“栈上常量”。

    *p=5;
00176E58  mov         eax,dword ptr [p]  
00176E5B  mov         dword ptr [eax],5  

我跳过了前两行语句,这里只需要知道p现在确实指向x的地址就够了,下边的输出结果也印证了这一点。可见,我们确实改变了x的值。那么为什么打印出来的x还是3呢?

所有的原因都在这里,当我们输出y的时候,编译器忠实的把y的值放到了寄存器里,把寄存器里的值当成参数传给了函数,但是对于x,编译器根本没有从x的内存里取x的值,它直接就push了一个3给函数,这就是为什么打印出的是3而不是5。那么这个3是从哪里来的呢?3是一个常量,它存在于进程地址空间的常量区,当我们敲出const int x=3; 的时候,我们不但把x所属的空间赋值为3,我们还在常量区创建了一个数字3的常量。

那么为什么我们使用x的时候编译器却回过头来操作这个3呢?这个现象叫做“常量折叠”,是一种编译器对于栈上常量的优化,当使用栈上常量的时候会直接从常量区取对应的值出来,这样少了一个从变量中取值到寄存器的过程,可以和对y的操作对比来看。

如果你还是不理解常量折叠,可以试试看一下下边的代码

换成字符串可能会比整数要方便理解一些,不过本质上来看,作为常量的它们都是内存地址空间常量段中的一块内存罢了。

想要禁用常量折叠也很简单,使用volitile让x在每次使用的时候都从内存中取值就ok了

这里出现了一个很大的问题,对x取地址居然输出的是1,我测试了一下只要变量前边加了volatile,对它取地址就是1,汇编代码我暂时也看不懂发生了什么,这个坑以后再填。

2.常量真的就是常量

const int x=3;
int main ()
{

}

把定义的位置放到全局空间里,一切都不一样了,这下它真的是常量了。

const int y=4;
int main()  
{  
	const int x=3;
        int*p=const_cast<int*>(&y);
	int*q=const_cast<int*>(&x);
}

    const int x=3;
003D4BFE  mov         dword ptr [x],3  
    int*p=const_cast<int*>(&y);
003D4C05  mov         dword ptr [p],3DCBB0h  
    int*q=const_cast<int*>(&x);
003D4C0C  lea         eax,[x]  
003D4C0F  mov         dword ptr [q],eax  

对y取地址获得的是一个实打实的常量区地址,我们可以用 *q=12345; 改变x的值,但是就别想 *p=56789了,编译器虽然发现不了,但是操作系统是不会允许你这样做的,运行的时候程序会报错,因为你企图修改进程虚拟地址空间中的只读段。

总结

在全局空间用const声明常量的时候,这个常量处于地址空间的常量段,是一个真正的常量

在栈内空间用const声明常量的时候,这个常量本质上和非常量没有区别,它处于堆栈中,存的值和常量相同,因为在赋值的时候执行了拷贝,但是在声明的同时,进程虚拟地址空间的常量段也会同时多出一个对应的常量,当我们使用这个栈上常量的时候,编译器会自动帮我们把它替换成常量段的常量。

猜你喜欢

转载自blog.csdn.net/qq_33113661/article/details/89193350
今日推荐