文档章节

C\C++编译器关于变量的内存分配顺序总结

NineRec
 NineRec
发布于 2014/08/26 08:33
字数 2309
阅读 18
收藏 1

 关于《纠结的N皇后问题》中数组越界导致的sum出现非正常的变化这个问题,继而引发出关于内存到底是如何被分配的疑问,今天早上便着手进行探索,当然其中借鉴了广大网友的总结,其中包括birdzb,NEO等牛人关于这方面的讨论。特别是看到一些讨论,感触颇深啊http://www.programfan.com/club/showtxt.asp?id=191048。怎么说呢,我还是觉得研究一下是有必要的,但要注意适可而止。

       知识储备:1. 内存的分区:代码区,数据区,堆,栈。 四个区域司职不同,相互配合。

                         2. 变量的分类以及初始化情况:局部变量,全局变量,静态的,非静态的。C++里面又包括成员变量。

一. 局部变量

编译器按照内存地址递减的方式来给变量分配内存

局部变量很多书籍中也叫自动变量,它声明在函数块内,作用范围也在函数块内。 不能被同一源文件的其他函数使用,也不能被其他文件中的函数使用。局部变量存储在栈中。无论局部变量显示初始化,或者未初始化,都只有当定义它们的程序块被调用时(即执行时),才分配空间,声明或定义时并不分配。局部变量不是可执行模块的一部分!!除非显示地对局部变量进行初始化,否则,它们的初始值是不确定的。为了验证“编译器按照内存地址递减的方式来给变量分配内存”,我们进行如下实验:

测试一:

[cpp]  view plain copy
  1. <span style="font-size:16px;">#include "iostream"  
  2. using namespace std;  
  3.   
  4. void main() {  
  5.     int i,j;  
  6.     cout<<&i<<' '<<&j<<endl;  
  7. }</span>  

测试结果是:

&i = 0012FF44, &j = 0012FF40

可见,虽然i在j之前被定义,但在编译器给变量分配内存时采用了内存地址递减的方式,所以j在内存中的位置比i超前了4个字节(因为是整型)。

测试二:

[cpp]  view plain copy
  1. #include "iostream"  
  2. using namespace std;  
  3.   
  4. void main() {  
  5.     int j,i;  
  6.     cout<<&i<<' '<<&j<<endl;  
  7. }  

测试结果是:

&i = 0012FF40, &j =  0012FF44

根据上面说明的采用内存递减的方式进行空间分配的话,首先分配的 j ,然后分配i,测试一首先分配i,然后分配j。

由此可见:在局部变量分配空间的顺序跟变量的声明顺序直接相关,同时按照内存由高到低的顺序进行空间分配。

下面看一段代码可以加深对数组越界出现昨天那个问题的理解。

[cpp]  view plain copy
  1. int i,a[10];  
  2.  for(i=1;i<=10;i++)  
  3.   a[i]=0;  

在for语句的比较部分本来是i<10;却写成了i<=10;因此实际上并不存在的a[10]被设置为0,也就是内存在数组a之后的一个字(word)的内存被设置为0。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。此时本来循环计数器i的值为10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就陷入死循环。

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. void main()  
  3. {  
  4.  int i,a[4];  
  5.  cout<<&i<<endl<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<' '<<&a[3]<<' '<<&a[4]<<endl;  
  6. }  

这段代码的执行结果:

0x0012FF44
0x0012FF34 0x0012FF38 0x0012FF3C 0x0012FF40 0x0012FF44
由此可以发现&i = &a[4],因为先声明的是i,然后才是a[4],故i处于高位,a处于低位,但是越界后a[4]来到高位,覆盖i

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. void main()  
  3. {  
  4.  int a[4],i;  
  5.  cout<<&i<<endl<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<' '<<&a[3]<<' '<<&a[4]<<endl;  
  6. }  


这段代码的执行结果:

0x0012FF34
0x0012FF38 0x0012FF3C 0x0012FF40 0x0012FF44 0x0012FF48

由于首先声明的是数组a[4],于是在高位上优先分配a,低位上分配i,因此可以发现这是并没有出现&i = &a[4]的情况。此时要是执行的话程序就不是假死了,直接是内存出错。

二. 全局变量

全局变量没有声明在任何一个函数内,作用范围在程序运行始终存在。能被同一源文件的任何函数使用,也能被其他文件中的函数使用,但要使用extern关键字。全局变量存储在数据段中。全局变量显示初始化时,或者未初始化时,在程序映像中有不同的分区:已初始化的全局变量是可执行模块的一部分。未初始化的全局变量则不是可执行模块的一部分,只有当定义它们的程序被调用时(即执行时),才分配空间,声明或定义时并不分配。未初始化的全局变量在运行时被初始化为0。

全局变量的空间一般分配在数据区,因此并不像局部变量那样在函数被调用的时候才按照声明顺序由递减的方式分配空间,全局变量在编译阶段就会把空间分配完毕,这其中的机制根据编译器的优化以及操作系统的原理都有很大的不同。

猜测一: 全局变量的显示初始为0与默认初始化为0效果一致,并不会导致内存分配地址出现不同,这时候遵循的规则是按章全局变量命名的字母顺序分配空间。

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int a;  
  3. int c;  
  4. int b;  
  5. void main() {  
  6.     cout<<&a<<' '<<&b<<' '<<&c<<endl;  
  7. }  

0x0042E058 0x0042E05C 0x0042E060

[cpp]  view plain copy
  1. #include <iostream.h>  
  2.   
  3. int c=0;  
  4. int a;  
  5. int b;  
  6. void main() {  
  7.     cout<<&a<<' '<<&b<<' '<<&c<<endl;  
  8. }  


0x0042E058 0x0042E05C 0x0042E060

由上述代码可以推测当所有的变量都默认初始化,或者显示初始化为0时,空间分配是按照声明变量的字母顺序按照空间递增顺序进行分配的。

添加数组测试用例:

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int a[2];  
  3. int b;  
  4. void main() {  
  5.     cout<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<endl<<&b<<endl;  
  6. }  


0x0042E058 0x0042E05C 0x0042E060
0x0042E060

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int b;  
  3. int a[2];  
  4. void main() {  
  5.     cout<<&a[0]<<' '<<&a[1]<<' '<<&a[2]<<endl<<&b<<endl;  
  6. }  


0x0042E058 0x0042E05C 0x0042E060
0x0042E060
可见两者的测试结果相同,说明的验证是正确的,作为默认初始化或者显示初始化为0的全局变量,其内存的分配与声明顺序无关,与变量命名的字母顺序有关。进一步测试:

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int c[2];  
  3. int b;  
  4.   
  5. void main() {  
  6.     cout<<&c[0]<<' '<<&c[1]<<' '<<&c[2]<<endl<<&b<<endl;  
  7. }  


0x0042E05C 0x0042E060 0x0042E064
0x0042E058

[cpp]  view plain copy
  1. #include <iostream.h>  
  2.   
  3. int b;  
  4. int c[2];  
  5. void main() {  
  6.     cout<<&c[0]<<' '<<&c[1]<<' '<<&c[2]<<endl<<&b<<endl;  
  7. }  


0x0042E05C 0x0042E060 0x0042E064
0x0042E058
数组也满足这样的猜测,其内存的分配按照变量名的字母顺序进行递增分配。

但是这时候要是有个有个变量初始化为0,结果就不按字母顺序进行递增分配了,而是按照声明顺序进行。

$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

上述猜测只对全局变量的命名为单字母成立,且字母的排序按照ASCII码中字母的顺序进行比较。如果全局变量的命名是多字母,或者数组中数值是宏并不是数字(若MAX = 4,那么a[MAX]被分配的地址与a[4]有可能不同),甚至因为代码的优化也会影响到地址的分配。这时候的规律是很难找的,而且也没有意义,最重要的是在自己写程序的时候尽量不要出现程序越界之后溢出覆盖的情形,那样是在太危险了。

不得不说别人说的很有道理,猜测一直接 陷入混乱,可能编译器的规则实在太多了,不能深究,掌握一些基本的就可以了。

$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

猜测二:全局变量初始化为非零的初始值时,内存分配按照声明顺序进行的

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int b=1;  
  3. int c=2;  
  4. int a=1;  
  5.   
  6. void main() {  
  7.     cout<<&a<<' '<<&b<<' '<<&c<<endl;  
  8. }  


0x0042D708 0x0042D700 0x0042D704

[cpp]  view plain copy
  1. #include <iostream.h>  
  2. int a=1;  
  3. int b=1;  
  4. int c=2;  
  5. void main() {  
  6.     cout<<&a<<' '<<&b<<' '<<&c<<endl;  
  7. }  


0x0042D700 0x0042D704 0x0042D708
由上述两个测试可以推测,当所有的全局变量初始化为非零值时,空间分配按照声明顺序按空间递增进行分配。 

总结

       事实上,全局变量不管有没有被初始化,其实都是被存放在DATA这个域中的,但是唯一不同的是这个DATA数据域有的时候又被划分成几个小的区域(说有的时候是因为并不是所有的系统都一定会这样做),分成initialized和un-initialized,因此,我们讨论的全局变量默认初始化或者初始化为零时,数据存储在un-initilized区域中,被初始化为非零时,数据存储在initialized区域中

本文转载自:http://blog.csdn.net/liuhuiyi/article/details/7526889

NineRec
粉丝 12
博文 75
码字总数 27999
作品 0
海淀
程序员
私信 提问
C语言编程新手基础入门学习—了解声明与定义

C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到...

小辰带你看世界
2018/03/19
0
0
10个Objective-C基础面试题,iOS面试必备

苹果的iOS系统越来越火了,苹果这个金矿平台也吸引了大量的iOS开发者参与其中,这也促使越来越多的公司向iOS应用开发方向靠拢,因此市场上 对iOS开发的人才需求自然也非常巨大。如果你准备去...

ruby_chen
2013/07/15
56.3K
18
【转】浅谈Java与C/C++的性能对比

大多数程序员都认为C/C++会比Java语言快,甚至于觉得从Java语言诞生以来,“执行速度缓慢”的帽子就应当被扣在头顶,这种观点的出现是由于Java刚出现的时候JIT编译技术还不成熟,主要靠解释器...

mj4738
2011/11/29
2.2K
13
函数的调用规则(__cdecl,__stdcall,__fastcall,__pascal)

关于函数的调用规则(调用约定),大多数时候是不需要了解的,但是如果需要跨语言的编程,比如VC写的dll要delphi调用,则需要了解。 microsoft的vc默认的是cdecl方式,而windows API则是std...

AlphaJay
2010/08/20
1K
1
Java 走向C++

本文不涉及一些微妙蛋疼的语法比较, 关注的是宏观方面, 当然后期逐步更新如果, 如果觉得必要, 可能会加上. 议题之一: 初始化的比较 1. CPP基类的任何类构造函数会默认调用父类的不带参数的构...

晨曦之光
2012/03/09
61
0

没有更多内容

加载失败,请刷新页面

加载更多

PostgreSQL 11.3 locking

rudi
今天
5
0
Mybatis Plus sql注入器

一、继承AbstractMethod /** * @author beth * @data 2019-10-23 20:39 */public class DeleteAllMethod extends AbstractMethod { @Override public MappedStatement injectMap......

一个yuanbeth
今天
11
1
一次写shell脚本的经历记录——特殊字符惹的祸

本文首发于微信公众号“我的小碗汤”,扫码文末二维码即可关注,欢迎一起交流! redis在容器化的过程中,涉及到纵向扩pod实例cpu、内存以及redis实例的maxmemory值,statefulset管理的pod需要...

码农实战
今天
4
0
为什么阿里巴巴Java开发手册中不建议在循环体中使用+进行字符串拼接?

之前在阅读《阿里巴巴Java开发手册》时,发现有一条是关于循环体中字符串拼接的建议,具体内容如下: 那么我们首先来用例子来看看在循环体中用 + 或者用 StringBuilder 进行字符串拼接的效率...

武培轩
今天
9
0
队列-链式(c/c++实现)

队列是在线性表功能稍作修改形成的,在生活中排队是不能插队的吧,先排队先得到对待,慢来得排在最后面,这样来就形成了”先进先出“的队列。作用就是通过伟大的程序员来实现算法解决现实生活...

白客C
今天
81
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部