很适合新手(我也是新手),涉及知识点有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是用一个字符串来定义的。
想起来了吗?其实就是栈空间和堆空间的区别~!(先这么算着,是否为堆,后边还会解释)
因为str1本来就不和str2、str3一路,所以出现这个规律: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地址很近才对,可是试过了才知道,
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
这个malloc分配的空间,不仅不和他们沾边,和整个程序中所有变量的地址都相去甚远。
再联想各种单变量被编译器各种组合各种排序,推断各字符数组(之前认为的堆空间和栈空间两种数组),不过就是对定义过长度和没定义(也许算隐含定义)过长度的数组的一种整合排序,总体来讲他们都在栈中,都是自动分配,这样想比较靠谱。
事实上,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宏。