从C语⾔结构对齐重谈变量存放地址与内存分配
【@.1 结构体对齐】
@->1.1
如果你看过我的,⼀定会对字节的⼤⼩端对齐⽅式有了重新的认识。简单回顾⼀下,对于我们常⽤的⼩端对齐⽅式,⼀个数据类型其⾼位数据存放在地址⾼位,地位数据在地址低位,如下图所⽰↓
这种规律对于我们的基本数据类型是很好理解的,但是对于像结构、联合等⼀类聚合类型(Aggregate)来说,存储时在内存的排布是怎样的?⼤⼩⼜是怎样的?我们来做实验。
*@->我们会经常⽤到下⾯⼏个宏分别打印变量地址、⼤⼩、格式化值输出、⼗六进制值输出↓
#define Prt_ADDR(var) printf("addr: 0x%p \'"#var"\'\n",&(var))
#define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
#define Prt_VALU_F(var,format) printf(" valu: "#format" \'"#var"\'\n",var)
#define Prt_VALU(var) Prt_VALU_F(var,0x%p)
*@->如果你没有C语⾔编译环境可以参考我的博客配置⼀个编译环境,或者。
考虑下⾯代码,
#include <stdio.h>
#define Prt_ADDR(var) printf("addr: 0x%p \'"#var"\'\n",&(var))
#define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
#define Prt_VALU_F(var,format) printf(" valu: "#format" \'"#var"\'\n",var)
#define Prt_VALU(var) Prt_VALU_F(var,0x%p)
typedef struct{
char a;
char b;
char c;
char d;
} MyType,*pMyType; //含有四个char成员的结构
int main()
{
pMyType pIns; //结构指针实例
int final; //拼接⽬标变量
pIns->a=0xAA;
pIns->b=0xBB;
pIns->c=0xCC;
pIns->d=0xDD;
final = *(unsigned int *)pIns; //拼接结构到int类型变量
Prt_VALU(final);
return0;
}
上⾯代码定义了⼀个含有4个char成员的结构,MyType和其指针pMyTYpe。新建⼀个实例pIns,赋值内部的四个成员,再将整体拼接到int类
型的变量final中。MyType中只有四个char类型,所以该结构⼤⼩为4Byte(可以⽤sizeof观察),⽽32位CPU中int类型也是4Byte所以⼤⼩正好合适,就看顺序,你认为最终的顺序是“0xAABBCCDD”,还是“0xDDCCBBAA”?
下⾯是输出结果(我⽤的eclipse+CDT)。
为什么?
结构体中地址的⾼低位对齐的规律是什么?
我们说,局部变量都存放在栈(stack)⾥,程序运⾏时栈的⽣长规律是从地址⾼到地址低。C语⾔到头来讲是⼀个顺序运⾏的语⾔,随着程序运⾏,栈中的地址依次往下⾛。遇到⾃定义结构MyType的变量Ins时(我们程序⾥写的是指针pIns,道理⼀样),⾸先计算出MyType所需的⼤⼩,这⾥是4Byte,在栈⾥开辟⼀⽚4Byte的空间,其最低端就是这个结构的⼊⼝地址(⽽不是最上端!)。进⼊这个结构后,依次往上放结构中的成员,因此结构中第⼀个成员a在最下⾯,d在最上⾯。联系到我们的⼩端(little-endian)对齐,因此最后输出的结果是按照⾼位到低位,d-c-b-a的顺序输出⼀个完整的数。因此最终的final=0xDDCCBBAA。
IN A NUTSHELL
结构体中的成员按照定义的顺序其存储地址依次增长。
@->1.2
之前我们提到⼀句,遇到⼀个结构体时⾸先计算其⼤⼩,再从栈上开辟相应区域。那么这个⼤⼩是怎么计算的?
typedef struct{
char a;
int b;
char c;
char d;
} T1,*pT1;
typedef struct{
char a;
char b;
char c;
int d;
} T2,*pT2;
现在计算上⾯定义的两个结构体T1,T2的⼤⼩是多少?可以通过下⾯代码打印
#include <stdio.h>
#define Prt_ADDR(var) printf("addr: 0x%p \'"#var"\'\n",&(var))
#define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
#define Prt_VALU_F(var,format) printf(" valu: "#format" \'"#var"\'\n",var)
#define Prt_VALU(var) Prt_VALU_F(var,0x%p)
typedef struct{
char a;
int b;
char c;
char d;
} T1,*pT1;
typedef struct{
char a;
char b;
char c;
int d;
} T2,*pT2;
int main()
{
T1 Ins1;
T2 Ins2;
Prt_SIZE(Ins1);
Prt_SIZE(Ins2);
}
其结果如下↓
,总结结构对齐原则是:
原则1、数据成员对齐规则:结构(struct或联合union)的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩的整数倍开始(⽐如int在32位机为4字节,则要从4的整数倍地址开始存储)。
原则2、结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。
(struct a⾥存有struct b,b⾥有char,int,double等元素,那b应该从8的整数倍开始存储。)
原则3、收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤成员的整数倍,不⾜的要补齐。
很明显按照以上原则,分析之前T1,T2结构的存储⽅式如图所⽰,打X的是按照规则之后的补充位↓
好了,现在可以考虑将结构T2改为:
typedef struct{
char a;
char b;
char c;
int d;
T1 e; //T1类型成员e
}T2, *pT2
结构T2的⼤⼩是多⼤?(20Byte)
⽽如果改为:
typedef struct{
char a;
char b;
char c;
int d;
pT1 e; //pT1类型成员esizeof结构体大小
}T2, *pT2
结构T2的⼤⼩是多⼤?(12Byte)
这些情况均可以⽤上⾯三原则进⾏分析。
因此,按照上⾯原则可以总结出⼀条经验性的习惯:将结构中数据类型⼤的成员往后放可以节省空间。
【@.2 变量存放地址,堆、栈,及内存分配】
我们先考虑⼀下局部变量在内存中的分布及顺序,考虑如下代码:
#include <stdio.h>
#define Prt_ADDR(var) printf("addr: 0x%p \'"#var"\'\n",&(var))
#define Prt_SIZE(var) printf("Size of \'"#var"\': %dByte\n",(int)sizeof(var))
#define Prt_VALU_F(var,format) printf(" valu: "#format" \'"#var"\'\n",var)
#define Prt_VALU(var) Prt_VALU_F(var,0x%p)
int ga=32;
int gb=777;
int gc;
int gd;
int main()
{
int a=23;
int b;
const char c='m';
static int ss1;
static int ss2=0;
static int ss3=81;
int * php1 = (int*)malloc(8*sizeof(int));
int * php2 = (int*)malloc(sizeof(int));
int hp3=malloc(sizeof(int)); //不好的写法
char _pause;
Prt_ADDR(a);
Prt_ADDR(b);
Prt_ADDR(c);
Prt_ADDR(ss1);
Prt_ADDR(ss2);
Prt_ADDR(ss3);
Prt_ADDR(php1);Prt_ADDR(*php1);
Prt_ADDR(php2);Prt_ADDR(*php2);
Prt_ADDR(hp3); Prt_VALU(hp3); //hp3内部存放分配的地址值
Prt_ADDR(ga);
Prt_ADDR(gb);
Prt_ADDR(gc);
Prt_ADDR(gd);
_pause=getchar();
}
这段代码⽤于测试变量所分配的地址值,其中包含了局部变量(a,b,c),静态局部变量(ss,ss2),全局变量(ga,gb,gc,gd)。变量_pause仅仅⽤于在VC中调试⽅便。
参考⾥的解释,内存通常可分为如下⼏块:
BSS段:BSS段(bss segment)通常是指⽤来存放程序中未初始化,或初始化为0的全局变量,静态局部变量的⼀块内存区域。BSS是英⽂Block Started by Symbol的简称。BSS段属于静态内存分配。
数据段:数据段(data segment)通常是指⽤来存放程序中已初始化为⾮0的全局变量的⼀块内存区域。数据段属于静态内存分配。
代码段:代码段(code segment/text segment)通常是指⽤来存放程序执⾏代码的⼀块内存区域。这部分区域的⼤⼩在程序运⾏前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含⼀些只读的常数变量,例如字符串常量等。
堆(heap):堆是⽤于存放进程运⾏中被动态分配的内存段,它的⼤⼩并不固定,可动态扩张或缩减。当进程调⽤malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利⽤free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
栈(stack):栈⼜称堆栈,是⽤户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调⽤时,其参数也会被压⼊发起调⽤的进程栈中,并且待到调⽤结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别⽅便⽤来保存/恢复调⽤现场。从这个意义上讲,我们可以把堆栈看成⼀个寄存、交换临时数据的内存区。
highest address
=========
| stack |
| vv |
| |
| |
| ^^ |
| heap |
=========
| bss |
=========
| data |
=========
| text |
=========
address 0
另外,栈(stack)的增长⽅向往地址低⽅向⾛,具有先进先出特点,栈顶指针位于低地址,随着程序运⾏在不断变化。堆(heap)的增长⽅向往地址⾼⽅向⾛,堆是⼀个类似于链表的结构,因此并不见得是个连续的空间。
好了,我们通常的理解就到此为,运⾏上⾯代码结果如下(前者Visual Studio,后者eclipse调⽤gcc的编译结果如下)。
image
每次程序运⾏这些变量的绝对地址可能变化,所以分析时我们注重观察变量的相对地址变化。
变量a,b,c均为局部变量,不管初始化与否,都被分配在栈上,⽽且顺序是按照从低⾄⾼向地址低分配的。其中变量c我添加了⼀个const是想说明,在修饰变量时,const对于地址分配⽆关,仅仅表⽰此变量是readonly的。另外,在VS系的编译器中,这些局部变量所占的空间⼤⼩⽐本⾝数据结构⼤,⽽gcc编译时的每个变量是地址上⼀个接着⼀个排,并且对齐⽅式也可以⽤前⾯的结构体对齐规律解释。
变量ss1,ss2,ss3就有区别了。ss1是未初始化的静态局部变量,ss2初始化为0,将被分配到BSS区,⽽且⼆者在gcc或VC编译后都是紧挨着的⽽不是像栈时有区别(后⾯会解释)。ss3初始化了的静态局部变量,分配在data段。
接下来的php1,php2和hp3变量⽤于演⽰堆(heap)操作。堆是由程序员⾃⼰控制并释放的,⼀般由malloc()等内存函数进⾏申请,最后需要⽤free进⾏释放(我在程序中没有⽤free了,最后将由系统释放)。对内存操作有较详细的描述(这也是⼀篇⽐较优秀的在线C教程,⽽且是⼀页流)。mallloc()返回void*类型的指针,指向在堆中开辟的⼀⽚区域。注意并没有初始化这⽚区域,所以其中的值可能是任意的。
我这⾥之所以打印了php1和*php1的地址是想说明,php本⾝是指针,其本⾝存在于栈中,⽽通过malloc分配之后,保存了⼀块分配好⼤⼩的堆的地址值。⽐较上⾯VS和gcc的编译结果,堆中的*php1和*php2分配的地址并不连续,⽽且地址增长⽅向也不同。虽然说堆是按照地址从低到⾼增长的,但是实际使⽤上堆相当于链表,⼀块链下⼀块,所以堆的地址增长⽅式我们可以不⽤太纠结。
hp3演⽰了⼀个⾮常规的堆的申请,malloc本⾝返回⼀个void*类型指针,赋值给int类型的hp3,严格意义上即使强制转换也不允许的。那
么int hp3=malloc(sizeof(int)); 这句话做了什么?通过后⾯Prt_VALU()打印其值可知,由于void*类型的特殊性,hp3中保存了分配好的堆的地址值。
全局变量,ga,gb初始化为⾮0,分配在data段,⽽gc,gd未初始化,分配在BSS段。以上可以通过观察打印出来的地址理解。
最后,总结⼀些有趣的实验现象如下:
@-> 栈的地址位于所有区域的地址最下⾯,跟理论上栈位于地址⾼位有出⼊。
@-> 堆的增长⽅向不见得是从地址低到⾼。gcc中是低到⾼,⽽VS中是⾼到低。
@-> 在BSS区域,未初始化(或初始化为0)的全局变量(gc,gd)按地址从⾼到低分配,⽽静态局部变量(ss1,ss2)按地址从低到⾼分配。
@-> 初始化的全局变量和静态局部变量(ga,gb,ss3)分配在Data段,从低到⾼分配,且地址上连续。
那么,为什么堆栈(stack、heap)上的地址分配并不见得是⼀个挨着⼀个(VS编译下的局部变量a,b,c),⽽DATA段,BSS段往往是⼀个挨着⼀个的呢?这个问题我想其实很多新⼿并没有太深究(⽐如我),包括关于所谓静态区域和⾮静态区域到底意味着什么。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论