C/C++内存分布与变量初始化顺序


关于入栈、出栈,栈顶栈底之类的分析见

函数调用的压栈出栈过程分析

下面继续分析C/C++的内存分布。



虽然0x10比一个变量需要的4个地址大了一些,但是0x10应该是规定的最小单位了。假如你要用的空间刚好是它的整数倍,其实是不浪费一分钱栈空间的,下边做一个数组,证明栈空间大小刚好是所有非静态变量占用空间的大小。

这里可以先无视hello的声明,它不在栈空间,后面会分析。

   x6       #include<stdio.h>                                       x
   x7       int main(){                                             x
   x9               static int hello;                               x 
B+>x11              int array[100] = {7,4,1};                       x
   x16              return 0;                                       x
   x17      } 

下边是汇编代码:比普通变量情况下复杂些,多用了一些寄存器,多了一些push、pop操作,还有rep指令。

   x0x8048394 <main>                push   %ebp                     x
   x0x8048395 <main+1>              mov    %esp,%ebp                x
   x0x8048397 <main+3>              push   %edi                     x
   x0x8048398 <main+4>              push   %ebx                     x
   x0x8048399 <main+5>              sub    $0x190,%esp              x
B+>x0x804839f <main+11>             lea    -0x198(%ebp),%ebx        x
   x0x80483a5 <main+17>             mov    $0x0,%eax                x
   x0x80483aa <main+22>             mov    $0x64,%edx               x100个变量(应该还有个标志类型大小是4的东西吧)
   x0x80483af <main+27>             mov    %ebx,%edi                x
   x0x80483b1 <main+29>             mov    %edx,%ecx                x
   x0x80483b3 <main+31>             rep stos %eax,%es:(%edi)        x
   x0x80483b5 <main+33>             movl   $0x7,-0x198(%ebp)        x初始化了三个数。
   x0x80483bf <main+43>             movl   $0x4,-0x194(%ebp)        x
   x0x80483c9 <main+53>             movl   $0x1,-0x190(%ebp)        x
   x0x80483d3 <main+63>             mov    $0x0,%eax                x
   x0x80483d8 <main+68>             add    $0x190,%esp 
   x0x80483de <main+74>     pop    %ebx                             x
   x0x80483df <main+75>     pop    %edi                             x
   x0x80483e0 <main+76>     pop    %ebp                             x
   x0x80483e1 <main+77>     ret                                     x
   x

下边是栈空间大小。两者相减,0x190,等于400bytes,刚好是100个int。

(gdb) print $esp
$1 = (void *) 0xbffff480
(gdb) print $ebp
$2 = (void *) 0xbffff618

我们都知道,”加了static就延长了变量的生命周期到程序结束“,或者还知道”加了static的变量地址不太一样“!

确实,地址变了,静态变量不在栈里。

int main(){
        static int hello;
        int top;
        return 0;
}
地址,top在栈中,而hello不在

(gdb) print &top
$3 = (int *) 0xbffff614
(gdb) print &hello
$4 = (int *) 0x8049614
栈大小是0x10
(gdb) print $esp
$3 = (void *) 0xbffff608
(gdb) print $ebp
$4 = (void *) 0xbffff618
去掉int型的声明,只留一个静态变量的声明,会发现一个有意思的现象
也就是说,连语句都被”优化“走了,不在main()函数里边执行了,不在main()函数里执行意味着什么?生命周期不光被向后延长到程序结束,也被向前延长了,它会在main之外,更早的地方被初始化。
那么如果我在静态变量声明语句之前,main()函数之内执行,究竟允不允许呢?可能语法上会做屏蔽,但是按运行顺序应该是可以的。
这句话换算成代码的话,是这样的:
#include<stdio.h>
int main(){
        hello = 4;
        static int hello;
        return 0;
}
连初学者都认为这样不可能,但是,如果我换一种写法呢?
#include<stdio.h>
static int hello;
int main(){
        hello = 4;
        return 0;
}
这样大家就都认可了吧!
但是我觉得这两个本质上是等价的, 只不过编译器在码农层面把这个操作给屏蔽了,可能太奇葩不便于理解。个人觉得是这样的!不用去试了,确实编译不过!!!!
换成可编译的版本运行一次:
   x0x8048394 <main>                push   %ebp                     x
   x0x8048395 <main+1>              mov    %esp,%ebp                x
B+>x0x8048397 <main+3>              movl   $0x4,0x8049614           x
   x0x80483a1 <main+13>             mov    $0x0,%eax                x
   x0x80483a6 <main+18>             pop    %ebp                     x
   x0x80483a7 <main+19>             ret                             x

main内声明肯定是看不到的,直接用了一下而已,如果没有那个赋值语句,main内部什么命理你个都看不到,直接就结束了。


我又想到另一个法子,因为编译后我可以看地址, 有时候(可能吧)不做”变量列表“(也就是所有变量类型和数量的综合)级的改动,其实地址都是差不多的,所以可以尝试先编译运行,看到地址后,再强行提取变量,C语言的强大!(C语言不是这样玩的好么-_-)
这是我的设想:
#include<stdio.h>
int main(){
        int *ptr = 0x8049614;
        printf("%d\n",*ptr);
        static int hello = 100;
        return 0;
}
print *ptr = 0(期望100)
没成功,但是还有希望,因为这次编译地址变了:


再来:
#include<stdio.h>
//static int hello = 100;
int main(){

        int *ptr = 0x8049578;
        printf("%d\n",*ptr);
        static int hello = 100;

        return 0;
}
不过讨厌的是又变了。。。。。。。这个(全局变量区?怎么叫来着?)地址分配很动态,不如栈的地址那么稳固。

一定要抓住它,这下试试664。

终于被我抓住了。

静态变量hello的声明和定义都在13行,但我在12行就提前并打印了出来。
来来来,再来个普通运行。



然并卵,现实中哪有人运行完了再去看结果,硬把地址写进去的,我也不知道这么玩能应用在哪。

这个例子只为证明:
1.静态变量的存储区域不在栈,并且也不像栈的地址规则那么稳定,但是还是能在一定程度上稳定住的,所以能抓住。
2.遇到那个“static”,这句声明加定义就和放在main外边没区别,这种代码的执行等于在程序正式执行之前就做完了,算初始化吧。
3.所以,无论静态变量声明在哪,它的生命周期都贯穿程序始终。
4.明明很早就声明定义了,之所以你不能直接这样提取静态变量的值,应该只是编译器设的规则,来避免一些不必要的人为错误吧。
5. static叫静态,静态静在哪?动态又动在哪?动态当然是动在你运行它才有结果,不运行就没有。栈虽然也会提前(运行函数体前)分配够空间,但是最起码变量的定义赋值是不会有的,而static,是一早就定义赋值完了,所以叫静态。(所以有充分理由怀疑编译器就是根据变量类型和数量,假设叫”变量列表“来分配栈空间的,谁叫我学渣没怎么学编译原理呢,只能先猜测一下)
6.不要模仿



扩展训练:
多加两层函数呢?


请容许我复习一下C++:
C++类静态成员是否会有所不同?是很早就初始化了,还是从某一个对象运行该函数开始?假如说也是带定义的形式,比如,static int i = 1;而不是static int i;
首先,第一种形式,ISO C++又不允许了(错误:ISO C++ 不允许在类内初始化非常量静态成员)。
其次,又不允许使用A::i这种形式访问A的静态成员变量i。


“奇怪”的发现:

当用一个类连续声明多个类对象的时候,后边的类对象被某种优化机制放在不一样的地方了(堆?全局静态区?)

具体流程如下:
第一次编译:
 F f,f2;//对象在栈
第二次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//只有f和f2在栈
//注:此时esp和ebp没变,栈没有扩容,是出于某种“懒惰”,还是“不屑”?
第三次编译:
F f,f2,f3,f4;
F f5,f6,f7,f8,f9;//对象都在栈
第四次编译:
F f,f2,f3,f4,f5,f6,f7,f8,f9;//所有对象都在栈。


首先,排除人为错误,虽然分配堆的时候F *pF = F();//print  &pF指针地址在栈,print pF指针指向的对象在堆。但是此处并不是指针,是实体。。。


编译器做了某种优化,这个优化的区分绝对不是代码使用不使用,f2就没使用,为什么和f一样放到栈了。但是


我的总结就是,这个对象变量上次有地址,这次就还有,因为之前只有
F f,f2;//导致了f2在栈,其他的f3到f8都是后来追加了, 编译器因为习惯,把f2保留在了栈,其他的扔一边去了。这次也是,因为拆分成两句进行声明时f4,f5们也有了栈地址,所以再合并回去的时候, 编译器出于习惯,继续把他们放在栈中。
所以说,编译器是带记忆(缓存)的 。

不过话说回来,一次声明太多对象变量,他们为什么被扔到其他位置了?这是“把未声明的变量放在静态变量区”了?
而如果把E e,e2;//突然变成
E e,e2,e3,e33,e333,e3333,e33333,e333333,e3333333,e33333333;//再去编译
结果所有对象也在栈中,随机性过强,所以不再纠结这种编译器的个例特性。浪费时间。

=======================================================================================================================
20160324:补充一个关于函数体与代码块的
关于入栈、出栈与栈帧,此文和前边的一篇关于函数调用的博文已经写得很清楚(也可能不清楚,两篇有重合,归类乱了点~~~~)
总之,关于函数的局部变量,因为是在存在当前栈帧的,所以外部变量在此时是完全屏蔽的,局部变量也活不到外部。
但是代码块是个特例(只是从表象知识点来说):
代码块可以声明“局部”变量,而对这个“局部”变量的修改不会影响到上一层的“此”变量,说起来乱,上代码:
#include<stdio.h>
int main()
{
        int i = 0;
        {
                int j = 1;
                i = 1;
                printf("i:%d\n",i);
                int i = 2;
                printf("inside of block:i:%d\n",i);
                printf("j:%d\n",j);
        }
        printf("outside of block:i:%d\n",i);

}

i的打印结果是1,2,1.
这里i是可以“重复声明”的,并且声明的局部的i的修改不会影响外部的。有些人根据表面概念,可能对此有些迷惑。这看似也是局部,代码块的局部和函数体的局部有什么区别呢?
首先确定的是,如果不重复声明,在代码块内的修改,对外部是全部生效的,这点通过for循环之类的代码块也会有所体会。
那么如果重复声明,区别在哪呢?其实这个“重复声明”,不过是新声明的一个变量罢了,你把它假设为k,这样对k的任何修改,当然都不会影响i。

其他的机制啦,协调工作啦,都是编译器自己映射好了,不用管了。
不过这样做也有缺点,都要占用栈空间嘛,又不是你真的去短暂覆盖原变量过后再去恢复,没有个内存用来记忆怎么可能覆盖了再恢复?
这样的好处可能也就是你常用
for(int i = 0;condition;modify i);
这种东西的话,避免互相干扰吧。
所以,如果有需求,局部干脆还是换个名字(k)吧,本质一样免得降低可读性。


======================================================================================================================
关于main函数之前的那些汇编,我还不太懂,有空再摸索,想知道静态变量的初始化,各种类,还有虚函数表的初始化过程。






猜你喜欢

转载自blog.csdn.net/huqinweI987/article/details/50816254