操作系统的内存分配问题与内存对齐问题对于地层程序设计来说是非常重要的,对内存分配的理解直接影响到代码质量、正确率、效率以及程序员对内存使用情况、溢出、泄露等的判断力。而内存对齐是常常被忽略的问题,理解内存对齐原理及方法则有助于帮助程序员判断访问非法内存。一般c/c++程序占用的内存主要分为以下五种:
1.栈区(stack):系统自动分配,由程序自动创建、自动释放。函数参数、局部变量以及返回值等信息都存在其中
2.堆区(heap):使用自由,不需要预先确定大小。多少情况下需要程序员手动申请、释放。如果不释放,程序结束后有操作系统垃圾回收机制收回。例如,s = (char *)malloc(10),
3.静态区/全局区(static):全局变量和静态变量的存储区域。程序结束后由系统释放
4.常量区:用于存放常量的内存区域
5.代码区:存放代码
例如:
#include <stdio.h>
int quanju;/*全局变量,全局区/静态区(static)*/
void fun(int f_jubu); /*程序代码区*/
int main(void)/**/
{
int m_jubu;/*栈区(stack)*/
static int m_jingtai;/*静态变量,全局区/静态区(static)*/
char *m_zifum,*m_zifuc = "hello";/*指针本身位于栈。指向字符串"hello",位于文字常量区*/
void (*pfun)(int); /*栈区(stack)*/
pfun=&fun;
m_zifum = (char *)malloc(sizeof(char)*10);/*指针内容指向分配空间,位于堆区(heap)*/
pfun(1);
printf("&quanju : %x/n",&quanju);
printf("&m_jubu : %x/n",&m_jubu);
printf("&m_jingtai: %x/n",&m_jingtai);
printf("m_zifuc : %x/n",m_zifuc);
printf("&m_zifuc : %x/n",&m_zifuc);
printf("m_zifum : %x/n",m_zifum);
printf("&m_zifum : %x/n",&m_zifum);
printf("pfun : %x/n",pfun);
printf("&pfun : %x/n",&pfun);
getch();
return 0;
}
void fun(int f_jubu)
{
static int f_jingtai;
printf("&f_jingtai: %x/n",&f_jingtai);
printf("&f_jubu : %x/n",&f_jubu);/*栈区(stack),但是与主函数中m_jubu位于不同的栈*/
}
堆和栈
1.申请方式
stack:
由系统自动分配。例如,在函数中声明一个局部变量char c;系统自动在栈中为c开辟空间
heap:
需要程序员手动申请,并指明大小,在c中,有malloc函数完成。如p1 = (char *)malloc(10)
2.申请后系统的响应
stack:
只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
heap:
大多数操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free函数才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中
3.申请大小的限制
stack:
在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
heap:
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
4.申请效率的比较
stack:
由系统自动分配,速度较快。是程序员无法控制的
heap:
由程序员手动分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便
5.堆和栈中的存储内容
stack:
在函数调用时,第一个进栈的是函数调用语句的下一条可执行语句的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是函数中的下一条指令,程序由该点继续运行
heap:
一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排
内存对齐问题
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
1.内存对齐的原因
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低 字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈
2.正确处理字节对齐
对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐:
a.数组:按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了
b.联合:按其包含的长度最大的数据类型对齐
c.结构体:结构体中每个数据类型都要对齐
从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,该条件是x % N = 0,并且整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节
3.对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。对齐规则如下:
a.数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值(或默认值)和这个数据成员类型长度中,比较小的那个进行。在上一个对齐后的地方开始寻找能被当前对齐数值整除的地址
b.结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐.主要体现在,最后一个元素对齐后,后面是否填补空字节,如果填补,填补多少.对齐将按照#pragma pack指定的数值(或默认值)和结构(或联合)最大数据成员类型长度中,比较小的那个进行
c.结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员类型长度的时候,这个n值的大小将不产生任何效果
4.有四个概念值:
1.数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。
2.指定对齐值:#pragma pack (value)时的指定对齐值value。
3.结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。
由于各个平台和编译器的不同,我的使用的gcc version 4.1.2 20080704 (Red Hat 4.1.2-52),来讨论编译器对struct数据结构中各个成员如何进行对齐的。例如:
1.struct A {
int a;
char b;
short c;
}; #结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐,所以使用sizeof(strcut A)值为8。
2.struct B {
char b;
int a;
short c;
};
#假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指 定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为 4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐 值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存
放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12
3.#pragma pack(2) /*指定按2字节对齐*/
struct C {
char b;
int a;
short c;
};
#pragma pack() /*取消指定对齐,恢复缺省对齐*/
#们使用预编译指令#pragma pack (value)来告诉编译器。第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1= 0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续 字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合 0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以 C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8
4.#pragma pack (1) /*指定按1字节对齐*/
struct D {
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
#sizeof(struct C)值是7
5.union E{
int a[5];
char b;
double c;
}; #我想的是union中变量共用内存,应以最长的为准,那就是20。可实际不然E中各变量的默认内存对齐方式,必须以最长的double 8字节对齐,故应该是sizeof(E)=24
注意:
1.数组对齐值为:min(数组元素类型,指定对齐长度).但数组中的元素是连续存放,存放时还是按照数组实际的长度.
如char t[9],对齐长度为1,实际占用连续的9byte.然后根据下一个元素的对齐长度决定在下一个元素之前填补多少byte.
2.嵌套的结构体假设
struct A
{
......
struct B b;
......
};
对于B结构体在A中的对齐长度为:min(B结构体的对齐长度,指定的对齐长度).B结构体的对齐长度为:上述2中结构整体对齐规则中的对齐长度.