静态存储区、堆、栈的内存空间分配——合理设立对照组与发散思维

很适合新手(我也是新手),涉及知识点有sizeof、strlen、堆空间、栈空间、编译原理(都很肤浅)。

 

最初不打算写这个的,只为学习一个小知识点,但就是因为思维太发散偷笑,就把整个主题都改了——谁说只能以专业知识为主题的?

至于为什么弄这么个题目,完全有感而发,个中细节,且听我细细道来~~

 

因为很久不用,记忆模糊,被sizeof和strlen搞得稍微糊涂。

现在对比一下两者区别:

 

性质:前者是操作符,后者是库

 

功能:前者可对type操作,后者只能对以'\0'结尾的string(包括数组char[])操作。

 

之前造成我误解的就在这,sizeof一个字符串或者整形数组,是要乘以type大小的,而strlen是单纯的字符串长度,而恰巧,sizeof(char)又是1,二者就更容易混淆了。

 

下面设立一个对照组做个小测试,对比一下两者(我的环境是xp+VMWARE+UBUNTU+GCC):

 

source code:

        char str1[] = "hhhhhh";
        char str2[10] = "asd";
        char str3[10] = "asdasdasda";

        printf("the length of str1 is %d\n",strlen(str1));
        printf("the sizeof   str1 is %d\n",sizeof(str1));
        printf("the length of str2 is %d\n",strlen(str2));
        printf("the sizeof   str2 is %d\n",sizeof(str2));
        printf("the length of str3 is %d\n",strlen(str3));
        printf("the sizeof   str3 is %d\n",sizeof(str3));


//字符串结束符为'\0',下面输入的下标为结束符应在的位置:
        printf("the end of str1 is %d\n",str1[6]);
        printf("the end of str2 is %d\n",str2[3]);
        printf("the end of str3 is %d\n",str3[10]);

//列出三个数组所占用地址空间范围。
        printf("the address of str1[0] is %p\n",&str1[0]);
        printf("the address of str1[6] is %p\n",&str1[6]);
        printf("the address of str3[0] is %p\n",&str3[0]);
        printf("the address of str3[9] is %p\n",&str3[9]);
        printf("the address of str2[0] is %p\n",&str2[0]);
        printf("the address of str2[9] is %p\n",&str2[9]);


 

 

 

Resault Print:

the length of str1 is 6
the sizeof   str1 is 7
//没有把申请的空间占满的前提下:sizeof作为空间大小,是计算了那个结束符的。
the length of str2 is 3
the sizeof   str2 is 10
//str2直接定义了大小10:sizeof完整的显示了空间大小,而strlen只给出了字符串的长度3
the length of str3 is 16
the sizeof   str3 is 10
//看到str3的长度已经明显不对了,按要求是10,此处达到了16

the end of str1 is 0
the end of str2 is 0
the end of str3 is 104
//因为涉嫌出界,所以这个str3[10]不是结束符0而是(int)104,用%c控制输出是h,这个h是巧合么?其实根本就是str1中的内容~!
//比较简单的方法,如果把str1中的内容改了(例:"dddddd"),发现相应str3[10]也变了
//注意两点:首先,str3[10]是str1中的h;其次,str1的长度是6,str3的(strlen认为的)“长度”是16,正好是10+6,
//加上sizeof(str3)为10这个事实,这验证了strlen是只认'\0'结束符的。

//至于原因
//因为这些是栈空间的分配了吧,连续分配的,并且向低地址扩展(其实至少在本例,向低地址扩展的说法是错的,
//这种错误是基于“windows下栈空间是向低地址空间扩展的”产生的,后边会解释,这只是思考的一个过程,必然有不完善),
//所以str3出界了(相对高地址)以后是str1,至于为什么str2跳了过去。

the address of str1[0] is 0xbf919755
the address of str1[6] is 0xbf91975b
the address of str2[0] is 0xbf919741
the address of str2[9] is 0xbf91974a

//这个规律很容易理解,在数组内,因为原则上,下标不过就是数组起始地址+“i”,所以自然是递增的。
//但是数组之间(也就是各变量之间)的地址是递减的。(同上,这些想法在本例都错了,后边会有验证)
//总结起来就是一增一减

the address of str3[0] is 0xbf91974b
the address of str3[9] is 0xbf919754

 

这结果里,其实是有bug的:

 

回过头去,把str2改长,不管怎么改,发现了一个非常奇怪的规律:str2地址最低,str3其次,但是str3的尾部总能“接壤”str1的头部。

其实不难想到,本例有个特别之处就是:str2和str3是声明时直接定义了固定长度,str1是用一个字符串来定义的

 

想起来了一个概念,其实就是栈空间和堆空间的区别?(其实不是堆,后边还会解释,这里应该只是编译器优化了一下顺序)

 

回头去在str2之前定义另一个占用堆空间的字符串,最好在后边也试一个,按编译器的一些特性——比如栈空间在编译时先分配(这也应了栈空间地址比堆空间地址低了,因为编译时先分配的嘛)——在str2与str3的前边和后边定义变量应该是一样的效果,重新修改部分代码,声明定义改成:

        char str1[] = "dddddd";
        char str4[] = "hello";
        char str2[10] = "asdasdads";
        char str3[10] = "asdasdasda";
        char str5[] = "world";

地址输出语句:

        printf("the address of str1[0] is %p\n",&str1[0]);
        printf("the address of str1[6] is %p\n",&str1[6]);
        printf("the address of str4[0] is %p\n",&str4[0]);
        printf("the address of str4[5] is %p\n",&str4[5]);
        printf("the address of str2[0] is %p\n",&str2[0]);
        printf("the address of str2[9] is %p\n",&str2[9]);
        printf("the address of str3[0] is %p\n",&str3[0]);
        printf("the address of str3[9] is %p\n",&str3[9]);
        printf("the address of str5[0] is %p\n",&str5[0]);
        printf("the address of str5[5] is %p\n",&str5[5]);
//长度为5的字符串之所以下标也到5,是考虑了结束符占用的一个地址。 

打印结果为:

 

the address of str1[0] is 0xbfc4e1e9
the address of str1[6] is 0xbfc4e1ef

the address of str4[0] is 0xbfc4e1f0
the address of str4[5] is 0xbfc4e1f5

//中间的str2和str3占用栈空间,其余堆空间(堆空间的判断和定义暂时持保留意见)
the address of str2[0] is 0xbfc4e1d5
the address of str2[9] is 0xbfc4e1de
the address of str3[0] is 0xbfc4e1df
the address of str3[9] is 0xbfc4e1e8

the address of str5[0] is 0xbfc4e1f6
the address of str5[5] is 0xbfc4e1fb

那么,堆和栈区分开了(关于本例是不是用到了堆空间,还是全是栈?还是持保留意见,详见后边分析),那么在栈内,数组和普通变量是否也要分开呢?答案也是肯定的

验证如下:

各变量和数组变量总的定义如下:

        int i = 10;
        long  int l = 10;
        long long int ll = 100;
        double d = 2.2;
        float f = 1.1;
        char c = 'c';
        short s = 2;
        int before = 4;
        char str1[] = "dddddd";
        char str4[] = "hello";
        int middle = 5;
        char str2[10] = "asdasdads";
        char str3[10] = "asdasdasda";
        char str5[] = "world";
        double end = 2.2;

 

(int)before、(int)middle和(double)end穿插数组前中后,打印输出的代码省略:

结果:数组的地址全部大于普通变量,验证!

(数组地址的打印结果也省略,因为重点是有新发现)

 

the address of i is 0xbfc5e90c
the address of l is 0xbfc5e910
the address of ll is 0xbfc5e8f0
the address of d is 0xbfc5e8f8
the address of f is 0xbfc5e914
the address of c is 0xbfc5e924
the address of s is 0xbfc5e922
the address of before is 0xbfc5e918
the address of middle is 0xbfc5e91c
the address of end is 0xbfc5e900

 

通过观察可以发现两个小规律,就是:

1.各变量也是分类“组团”(比如两个double型的地址连着——尾数8f8和900)往栈里插的,这很合乎逻辑,因为好管理嘛,编译器肯定有一定规则。

2.哪种类型的变量地址更低,不是定义顺序说了算的。和组团插入一个思路——编译器按固定规则去找(相信编译原理都有相关解释,不过我现在不了解)

 

到此,问题似乎基本解决?

 

不过,关于堆和栈的区分,可能还是不准确~!如果简单的把“动态”分配大小的

char str1[] = "hhhhhh";

当做占用堆空间,而把“固定长度”固定大小的

char str2[10] = "asdasdads";

当做占用栈空间,那么至少知道malloc是堆空间的:

char *str = malloc(sizeof(char)*10);

 

malloc分配的空间地址也应该和所谓的占用堆空间的str1、str4、str5地址很近才对,可是试过了才知道,

<span style="color:#000000">the address of str[0] is 0x8220008

the address of str[10] is 0x8220012
the address of str1[0] is 0xbf8071b9
the address of str1[5] is 0xbf8071be</span>

 

这个malloc分配的空间,不仅不和他们沾边,和整个程序中所有变量的地址都相去甚远。

再联想各种单变量被编译器各种组合各种排序,推断各字符数组(之前认为的堆空间和栈空间两种数组),不过就是对定义过长度和没定义(也许算隐含定义)过长度的数组的一种整合排序,总体来讲他们都在栈中,都是自动分配,这样想比较靠谱。之前的误区是字符串常量的问题,不过并不是C++,不是直接用指针指向字符串常量,都是定义的char数组,所以char数组之间互相是没有差别的,其实地址也挨得很近,只不过被编译器优化排序了。不同类型也是同理,都是被编译器优化了一下排列顺序。

字符串常量自身应该在常量区,不同于字符数组。

 

 

事实上,linux下的栈空间增长顺序还是没能准确说出来,也许之前以为的正序,反过来看就是倒序了?谁知道到底是数组优先还是普通变量优先。不过至少,在同类型中,还是按定义顺序而增长地址的。

没有指导理论,光凭规律总结,那么假设堆、栈空间就是这样分开的,根据空间的“抱团”分区原则,结合的这么紧密,那么除了malloc分配的空间,几乎可以肯定其他都在栈空间了~~~

 

 

最后~忘了静态存储区(static storage area)了,区分于堆区和栈区,这又是一个单独的区域,保存自动全局变量和static变量(static也包括全局和局部)。静态区的主要特征是生命周期长,可以超越局部函数体对栈变量的限制,而局部栈变量生命周期是很短的。

一小例:

 

#include<stdio.h>
#define _PRINT_H_
int global = 100;

main(){
        int stack = 10;
//自由设置下面几个堆区的大小,查看分配规律
        char *heap1 = (char*)malloc(10*sizeof(char));
        char *heap2 = (char*)malloc(1*sizeof(char));
        char *heap3 = (char*)malloc(1000*sizeof(char));
        char *heap4 = (char*)malloc(10*sizeof(char));

        int i = 0;
        {
                static  int localStatic = 6;
                int localStack =8;
#ifdef _PRINT_H_
        printf("the address of localStack  is %p\n",&localStack);
        printf("the address of localStatic  is %p\n",&localStatic);
#endif
        }
#ifdef _PRINT_H_
        printf("the address of global is %p\n",&global);
        printf("the address of stack is %p\n",&stack);
        printf("the address of heap1 isn't %p\n",&heap1);
//将由malloc分配的(由heap1指向的)空间命名为heap area1
        printf("the address of heap area1 isn't %p\n",&heap1);
        printf("the address of heap area1 is %p\n",heap1);
        printf("the address of heap area2 is %p\n",heap2);
        printf("the address of heap area3 is %p\n",heap3);
        printf("the address of heap area4 is %p\n",heap4);
#endif
}

打印结果:
the address of localStack  is 0xbf841a54
the address of localStatic  is 0x804a01c
the address of global is 0x804a018
the address of stack is 0xbf841a4c
the address of heap1 isn't 0xbf841a50
//申请堆再小,也要隔开0x10,也许堆中划分块最小就是16byte。
the address of heap area1 is 0x8e30008
the address of heap area2 is 0x8e30018
the address of heap area3 is 0x8e30028
the address of heap area4 is 0x8e30418


可以看到,其实分了三个区域,global和localstatic是一起的,静态存储区(static storage area);

 

localstack和各种其他自定义变量(省略了,随便几个int i)是栈区(stack area);

heap1和heap2等指向的malloc分配区域是堆区(heap area),有些乱,但是通过地址可以看出是属于一个范围内。。注意heap1和heap2本身是存在栈区的,他们是指针。

ps:根据malloc的分配方式的特性(先申请,按大小找空闲空间,一般是找链表,最先找到满足需求的空间、或者找到满足要求的最小空间分配,从上例可以看到最小分配的堆空间可能是16byte),他们有可能是不连续的,但是至少有很大的可能,地址比较近的(相比栈空间地址,就近太多了),如果分配的空间本身又不大,看起来就更连续了。

另外,根据局部变量覆盖全局变量的原则,即使把变量“stack”和“localstack”起同样的名字,他们也是两个地址,可试~

===================================================================================================================================

CONCLUSION:虽然“结论”不停的被推翻,并且最后的也不一定权威,但是至少一些发现的很多规律还是没错的,至少在本环境内。

串了这么多知识点,结果还是不够用,还有一大堆知识点要学习。所以说,比较合理的对照组设计能带出很多问题,也是我之前修改u-boot时发现的,有时候,犯一个错误,能帮你发现并避免另一个错误!编程时遇到的巧合太多了,不设立合理的对照组根本无法发现,不深挖细节根本无法理解。另外,虽然出现的意外能够应付,但也算是小吃一堑,这次对照组设立其实是简化了,在开始就能根据所要测试的N个条件设立2的N次方个实例的话会省很多功夫,很多东西也更为直观。

 

除了对照组的设立,能从单纯对比strlen与sizeof的单位,发展到研究strlen对结束符的判定,再到观察对比数组地址,到最后的堆栈空间,还有windows与linux栈指针的增长方向,堆栈等存储区域划分,串烧这么多知识点,最重要的还是举一反三、顺藤摸瓜的发散思维方式,鄙人觉得这对学习,尤其是学习比较复杂、有无限复杂知识链条的软件开发来说是非常有用的技能。所以才有了这个主题。

 

 

不过有些问题既然发现了,后续还是要学习研究的,技能也需要提升:比如gdb、objdump等工具的使用,windows下栈的增长是向下还是向上,这个内存分配的具体阶段和过程,编译原理相关知识的掌握。

稍后我会进行相关方面的学习和探讨~~~~~~~~~

 

另外,觉得一大串打印有些乱的,可以弄个debug宏。

 

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值