const与static原理

来源:互联网 发布:南昌大学网络教育平台 编辑:程序博客网 时间:2024/05/16 00:59

作者: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 修饰的只读变量是在编译的时候确定其值。

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



0 0
原创粉丝点击