天天看点

const与static原理

作者:evilknight摘自邪恶八进制

编译环境: WinXP sp2 + VC6.0 SP 6

        对于许多C/C++初学者,往往知道static变量只是被初始化一次,对于const变量,只知道他的值是不能被修改的,但是对于其实现却不知所有然。这里我以VC6.0 SP6为平台,揭开其编译器实现原理。

下面看一段程序: 引用:

#include <iostream.h>

void fun(int i)

{

    static int n = i ;

    int *p = &n ;

    cout << n << endl ;

    ++n ;

    //

    // 等下我们要在这写代码,让static int n

    // 每次进这个函数都初始化一次

    //

}

int main(void)

{

    for (int i(10); i > 0; --i)

    {

        fun(i) ;

    }

    return 0;

}程序的输出结果是: 引用:

10

11

12

13

14

15

16

17

18

19

下面我们调试一下,看下编译器如何实现:

我们在fun函数的第一行设一个断点。static int n = i ;所在行,按F5。

按Alt+6打开Memory。按F10单步执行,当p有值的时候,我们将他的值拖到Memory窗口,这时就会转到n所在的内存地址,可是这时static已经初始化了,我们不知道编译器对他做了什么操作了。这时我们重新开始调试,一般n的内存地址不会变的,还是在那里。

我这里以我这边的地址为例: 引用:

0042E058 00 00 00 00 ....

0042E05C 00 00 00 00 .... // 中间这个为n的内存地址

0042E060 00 00 00 00 ....我们按F10单步执行一下一条语句(static int n = i ;) 引用:

0042E058 01 00 00 00 ....

0042E05C 0A 00 00 00 ....// n

0042E060 00 00 00 00 ....执行完这条语句之后,除了n有了初值,上面有内存空间也有了变化。

我们接着按F5直接执行到那个断点处,再单步执行一下,发现这次只是n的值有变化,所以我们猜测上面的那个位可能是static的标志位,如果是0的话,说明没有初始化,如果是1的话,说明已经初始化了,下次再进来的时候就不用初始化了,为了验证我们的猜测,我们现在在函数里面加几句语言,修改那个值。 引用:

void fun(int i)

{

    static int n = i ;

    int *p = &n ;

    cout << n << endl ;

    ++n ;

    //

    // 等下我们要在这写代码,让static int n

    // 每次进这个函数都初始化一次

    --p ;

    *p = 0 ;   //这两句的意思是把指针指向static变量的标志位,并把标志位的值改为0,表示静态变量未初始化

    //

}

写完上面二句,我们执行一下,是不是发现执行结果已经和上面的不同了,每次进函数都会对static int n进行赋初值操作。

下面我们再来看2个static类型的情况,在上面的代码中,我们再加一个 static变量; 引用:

void fun(int i)

{

    static int n1 = i ;

    static int n2 = i ;

    int *p = &n1 ;

    cout << n1 << endl ;

    ++n1 ;

    //

    // 等下我们要在这写代码,让static int n

    // 每次进这个函数都初始化一次

    --p ;

    *p = 0 ;

    //

}还是继续调试。

二个static变量初始化之前内存里面的值 引用:

0042E050 00 00 00 00 ....

0042E054 00 00 00 00 ....

0042E058 00 00 00 00 ....

0042E05C 00 00 00 00 .... // n1

0042E060 00 00 00 00 .... // n2

0042E064 00 00 00 00 ....当执行完static int n1 = i ;语句之后,内存的值变成这样了 引用:

0042E058 01 00 00 00 ....

0042E05C 0A 00 00 00 ....

0042E060 00 00 00 00 ....接着我们再单步执行

内存的值变成这样。 引用:

0042E058 03 00 00 00 ....

0042E05C 0A 00 00 00 ....

0042E060 0A 00 00 00 ....这样就很明显了,编译器分别用一位来表示一个static变量是否已经始化。

上面是对于用变量对 static进行初始化,对于用常量初始化的情况是怎么样的呢?

我们将上面的代码改成: 引用:

#include <iostream.h>

void fun(int i)

{

    static int n1 = 0x12345678 ;

    int *p = &n1 ;

    cout << *p << endl ;

}

int main(void)

{

    for (int i(10); i > 0; --i)

    {

        fun(i) ;

    }

    return 0;

}当指针取到值之后,我们结束调试。我这里的地址值是0x0042ad64。

好了,我们结束调试,用winhex打开生成的可执行文件,按Alt+g跳到n的地址,这里要减去0x400000,也就是2ad64。是不是看到我们的初值了。

因为intel使用的是小端法,所以我们看到的值是反过来的。

下面我们再来探索一下const的原理;

下面看一个程序段 引用:

#include <iostream.h>

int main(void)

{

    const int n = 1 ;

    int *p = (int *)&n ;

    *p = 0 ;

    cout << n << endl ;

    cout << *p << endl ;

    return 0;

}

我们执行一下,结果是不是和我们所期望的不同呢,我们在第一行下断点,一条一条的执行。

确认每一步操作是否正确。

当执行到*p = 0的时候我们发现n内存所在的值已经变成0了,但是为什么执行结果令我们大失所望呢?

我们按Alt +8打开汇编窗口。 引用:

7:        cout << n << endl ;

0041161E   push        offset @ILT+40(endl) (0040102d)

00411623   push        1

00411625   mov         ecx,offset cout (0042e070)    //此处0042e070直接替换了n

0041162A   call        ostream::operator<< (004012a0)

0041162F   mov         ecx,eax

00411631   call        @ILT+30(ostream::operator<<) (00401023)

8:        cout << *p << endl ;

00411636   push        offset @ILT+40(endl) (0040102d)

0041163B   mov         edx,dword ptr [ebp-8]

0041163E   mov         eax,dword ptr [edx]

00411640   push        eax

00411641   mov         ecx,offset cout (0042e070)

00411646   call        ostream::operator<< (004012a0)

0041164B   mov         ecx,eax

0041164D   call        @ILT+30(ostream::operator<<) (00401023)原来编译器将我们的const变量直接用常量给替换掉了!

可能有人会想,那这样为什么还要给const变量分配空间呢,这个留给大家思考吧,或者给你们设计编译器的话,你们也会这样实现的!

(我的看法是:  如果当其他的指针变量指向它时,可以使用这个变量空间, 就相当于是两个"不同的"变量 )

End

第一篇原文链接,感谢原作者

常量有没有存储空间,或者只是编译时的符号而已?

不一定。

在C中,define常量是预处理阶段的工作,其不占据内存。但是const常量总是占据内存

在C++中,const常量是否占据存储空间取决于是否有引用该常量地址的代码。C++对于const默认为内部链接,是放置在符号表中的,因此const常量定义通常都放在头文件中,即使分配内存也不会出现链接错误。

若不引用常量对应的地址,则不会为其分配空间。

(c++ : 对于基本数据类型的常量,编译器会把它放到符号表中而不分配存储空间,

而ADT/UDT的const对象则需要分配存储空间(大对象)。还有一些情况下也需要分配存储空间,

例如强制声明为extern的符号常量或取符号常量的地址等操作)

Const是用来替换define的,因此其必须能够放在头文件中,在C++中const变量是默认为内部链接的,即在其他文件中看不到本文件定义的const变量,因此链接不会出现问题。Const变量在定义的时候必须初始化,除非显式的指定其为extern的。通常C++中会尽量避免为const变量分配内存storage的,而是在编译阶段将其保存在符号表symbol table中。当用extern修饰const变量或引用其地址时,将强制为其分配内存,因为extern表示采用外部链接,因此其必须有某个地址保存其值。

#include <iostream.h>

const int i=100;   //无法找到i的符号,因为没有为其分配存储空间。

const int j=i+100;   //强迫编译器为常量分配存储空间

long address=(long)&j;

char buf[j+10];

int main(int argc, char* argv[])

{

 const char c=cin.get();

 const char d=3;  // 局部变量栈区

 char test1[d+10];

 //char test2[c+10];  error const char c必须到运行时刻动态获取其初值。char test2[c+10]编译无法通过,因为无法确定c就无法确定数组长度。

 const char c2=c-'a'+'A';

 cout<<c<<" "<<c2<<endl;

 return 0;

}

第二篇原文链接,感谢原作者

定义const 只读变量,具有不可变性。

例如:

const intMax=100;

intArray[Max];

这里请在Visual C++6.0 里分别创建.c 文件和.cpp 文件测试一下。你会发现在.c 文件中,

编译器会提示出错,而在.cpp 文件中则顺利运行。为什么呢?我们知道定义一个数组必须指

定其元素的个数。这也从侧面证实在C 语言中,const 修饰的Max 仍然是变量,只不过是只

读属性罢了;而在C++里,扩展了const 的含义,这里就不讨论了。

编译器通常不为普通const 只读变量分配存储空间,而是将它们保存在符号表中,这使

得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

例如:

#define M 3 //宏常量

const int N=5; //此时并未将N 放入内存中

......

int i=N; //此时为N 分配内存,以后不再分配!

int I=M; //预编译期间进行宏替换,分配内存

int j=N; //没有内存分配

int J=M; //再进行宏替换,又一次分配内存!

const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define

一样给出的是立即数,所以,const 定义的只读变量在程序运行过程中只有一份拷贝(因为

它是全局的只读变量,存放在静态区),而#define 定义的宏常量在内存中有若干个拷贝。

#define 宏是在预编译阶段进行替换,而const 修饰的只读变量是在编译的时候确定其值。

第三段原文链接,感谢原作者