现在的位置: 首页 > 综合 > 正文

深入理解const变量

2014年09月05日 ⁄ 综合 ⁄ 共 6451字 ⁄ 字号 评论关闭

    有这样一段毫无意义的C++代码:

#include <stdio.h>

#define AA 0x44

extern const int g_const_a = 0x5555; 
int g_b;
int g_c = 0x55aa;

int function(void)
{
    return 0;
}

int main(void)
{
   int l_a = 0x11 + g_const_a; //考察全局const
   int l_b = 0x22 + AA; //考察宏

   int *l_pA = (int *) &g_const_a;
   *l_pA = 0x3333;
   int l_c = g_const_a;

   printf("g_const_a = 0x%x, *l_pA = 0x%x, l_c = 0x%x\n", 
           g_const_a, *l_pA, l_c);

    return 0;
}

    我们首先看打印结果:

g_const_a = 0x5555, *l_pA = 0x3333, l_c = 0x5555

    我们通过g_const_a的地址l_pA来改变了g_const_a的值,为啥打印结果却没有改变呢?我们来看一下它的反汇编代码:

(图1)

(图2)

    对反汇编代码栈帧结构不清楚的同学请查考本博客转载的文章:http://blog.csdn.net/xumin330774233/article/details/18188069

    我们从第148行看起,movl   $0x5566,0x1c(%esp) 。栈申请了0x20个字节,为什么是0x20个字节?
我只知道肯定是函数形参、局部变量、该函数的被调函数的入参等影响。具体值怎么得知,只能去研究一下编译原理的具体细节了。
这里将
0x11+0x5555放到当前栈指针+0x1c处,可以看到编译时直接进行了常量折叠替换。我看有些博客上说const变量的折叠替换是发生在预编译阶段,我认为是不对的。应该是发生在编译阶段(准确地讲是在g++
-S mytest.i -o mytest.s这个阶段,查看一下mytest.s汇编,发现此时已经发生了折叠替换,而对于非const的全局变量,譬如g_c=0x55aa则没有折叠替换)。
预编译阶段我们看到进行了宏的折叠替换,但对const变量根本没有进行折叠替换,const变量的折叠替换是发生在g++
-S mytest.i -o mytest.s这个阶段
。如下图:

 

(图3)

 

(图4)

     作为题外话,既然宏已经在预编译时就已经被折叠替换,那我们在gdb运行程序时如何查看宏呢? 很简单,先加上编译选项-g3 -gdwarf-2,然后gdb 文件后,注意要首先list一下文件,就可以查看宏了。

 

(图5)

    回到我们讨论的地方,我们可以看到反汇编代码的第150行,宏已经折叠替换(其实是发生在预编译阶段),第152行将g_const_a的地址放到栈变量l_pA155行将新值0x3333放到了指针变量l_pA所指的内存中,我们此时用gdb跟踪代码可以看到g_const_a的内容已经变成新值0x3333了。如下图:

 

(图6)

    那为什么我们打印g_const_a的时候却又变成了原来的0x5555

    我们继续来看反汇编代码的156行,发现虽然g_const_a已经变化,但赋值给变量l_c时,依然是用原来的值赋值。我们可以判定这是编译器从中作梗了,在运行阶段根本就没有从内存中取内容,前面已经谈过,其实在编译阶段就已经给折叠替换了。我们继续来看反汇编代码的158165行,它的目的是把printf4个函数参数从右到左入栈(为什么不是从左到右,当然可以的,不过C语言一般默认的调用惯例cdecl,当然还有其他惯例,我这里摘录《程序员的自我修养》的一个表):

(图7)

     第163行是g_const_a变量入栈,运行阶段根本就没有从内存地址中取值,同样的道理,也是在编译阶段进行了折叠替换。另外第165是传的printf中字符串(字符串放在只读区域哦)的地址,这个我们可以验证一下:

 

(图8)

     我们可能还有疑问,既然g_const_a加了extern作为外部链接,那别的文件有一个函数引用这个变量的时候,那么是取这个变量的常量值,还是这个变量所在内存中的值呢?我们做一下实验,修改原.cpp,加上test()语句。在别的文件中的test函数中打印这个g_const_a值。我们发现这次是取内存中的值了。其实我们反汇编链接后生成的目标文件,就会发现g_const_a在编译阶段就已经分配了内存,当然这个内存是虚拟内存。

 

  (图9)


    我的结论是:放到函数外部的全局变量const int g_const_a =0x5555 和 extern int g_const_a = 0x5555的区别在于前者是内连接,后者是外连接,另外编译器也发挥了自己的作用,当把g_const_a作为右值时,在本模块中是直接用其常量值赋值,没有从它所在的数据section的内存空间里取值。当在外部模块引用这个extern g_const_a变量时,是从它的数据section的内存空间里取值而已。
他们的相同点是,就是在编译时(不是运行时)都为他们分配了虚拟内存。有些同学可能想穷根究底,你说是在编译阶段,有什么根据,而且广义的编译还有: 预编译阶段、编译阶段、汇编阶段、链接阶段,那到底是哪一个阶段? 我的结论是,虚拟内存的分配发生在编译阶段,即在g++ -S mytest.i > mytest.s 这个阶段。至于依据,我们可以比较const int g_const_a =0x5555 和 extern int g_const_a = 0x5555在汇编语言的区别:

                     加上extern的全局变量(图10)

                   没加extern的全局变量(图11)

    图10 显示_g_const_a声明为globl(表示可以被其他模块访问), 长度为long(4个字节),内容为21845(即0x5555),放在.rdata只读数据段。图11显示__ZL9g_const_a,其实就是没有加extern的g_const_a的名字修饰,占.rdata段4个字节,内容为21845(即0x5555)。很显然也分配了空间。

    如果我们在const后加上volatile,编译器就不能对const变量做优化。此时其他cpp文件对g_const_a的所有引用都是从内存取的。

     如果g_const_a放在函数内部作为局部变量呢? 它有局部变量的性质,在栈中有一个存储空间。当把g_const_a作为右值时,编译器会做一个优化,不是从这个栈的存储空间去取这个变量值,而是直接取它的常量值去赋值而已。

     一些同学可能关心变量和函数放在什么.o文件或可执行文件的那个section,我们可以通过nm -o .o或可执行文件去查看。当然也可通过objdump -t 去查看。nm里的TtDdBb你肯定很想知道是什么,那就man nm吧。不过我要说明的是一般大写表示表全局变量,小写表示静态变量。另外对于cc++有一点要注意的是,对于gcc的非可执行文件的.o文件,全局未初始化变量没有放在.bss段,只是一个未定义的“COMMON符号”,当然最后连接后生成的可执行文件还是放在.bss段了。而对于g++,没有这个区别,全局未初始化变量始终放在.bss段了。附一段nm的帮助吧:

   GNU nm lists the symbols from object files objfile....  If no object files are listed as arguments, nm assumes the file a.out.

       For each symbol, nm shows:

        ·  The symbol value, in the radix selected by options (see below), or hexadecimal by default.

 

       ·  The symbol type.  At least the following types are used; others are, as well, depending on the object file format.  If lowercase, the symbol is usually local; if

           uppercase, the symbol is global (external).  There are however a few lowercase symbols that are shown for special global symbols ("u", "v" and "w").

 

           "A" The symbol's value is absolute, and will not be changed by further linking.

 

           "B"

           "b" The symbol is in the uninitialized data section (known as BSS).

 

           "C" The symbol is common.  Common symbols are uninitialized data.  When linking, multiple common symbols may appear with the same name.  If the symbol is defined

               anywhere, the common symbols are treated as undefined references.

 

           "D"

           "d" The symbol is in the initialized data section.

 

           "G"

           "g" The symbol is in an initialized data section for small objects.  Some object file formats permit more efficient access to small data objects, such as a

               global int variable as opposed to a large global array.

 

           "i" For PE format files this indicates that the symbol is in a section specific to the implementation of DLLs.  For ELF format files this indicates that the

               symbol is an indirect function.  This is a GNU extension to the standard set of ELF symbol types.  It indicates a symbol which if referenced by a relocation

               does not evaluate to its address, but instead must be invoked at runtime.  The runtime execution will then return the value to be used in the relocation.

 

           "N" The symbol is a debugging symbol.

 

           "p" The symbols is in a stack unwind section.

 

           "R"

           "r" The symbol is in a read only data section.

 

           "S"

           "s" The symbol is in an uninitialized data section for small objects.

 

           "T"

           "t" The symbol is in the text (code) section.

 

           "U" The symbol is undefined.

 

           "u" The symbol is a unique global symbol.  This is a GNU extension to the standard set of ELF symbol bindings.  For such a symbol the dynamic linker will make

               sure that in the entire process there is just one symbol with this name and type in use.

 

           "V"

           "v" The symbol is a weak object.  When a weak defined symbol is linked with a normal defined symbol, the normal defined symbol is used with no error.  When a

               weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes zero with no error.  On some systems, uppercase indicates

               that a default value has been specified.

 

           "W"

           "w" The symbol is a weak symbol that has not been specifically tagged as a weak object symbol.  When a weak defined symbol is linked with a normal defined

               symbol, the normal defined symbol is used with no error.  When a weak undefined symbol is linked and the symbol is not defined, the value of the symbol is

               determined in a system-specific manner without error.  On some systems, uppercase indicates that a default value has been specified.

 

【上篇】
【下篇】

抱歉!评论已关闭.