C语言内存浅谈

操作系统的内存分配问题与内存对齐问题对于地层程序设计来说是非常重要的,对内存分配的理解直接影响到代码质量、正确率、效率以及程序员对内存使用情况、溢出、泄露等的判断力。而内存对齐是常常被忽略的问题,理解内存对齐原理及方法则有助于帮助程序员判断访问非法内存。一般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中结构整体对齐规则中的对齐长度.

时间: 2024-10-27 08:32:40

C语言内存浅谈的相关文章

【C语言】浅谈strtok()与NULL

一.strtok(  )函数 strtok(  )函数包含于头文件string.h 语法:char *strtok( char *str1, const char *str2 ); 功能:函数返回字符串str1中紧接"标记"的部分的指针, 字符串str2是作为标记的分隔符.如果分隔标记没有找到,函数返回NULL.为了将字符串转换成标记,第一次调用str1 指向作为标记的分隔符.之后所以的调用str1 都应为NULL 二.代码 #include<stdio.h> #inclu

【C语言】浅谈可变参数与printf函数

一.何谓可变参数 int printf( const char* format, ...); 这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用"-"表示). 而我们又可以用各种方式来调用printf,如: printf( "%d ",value); printf( "%s ",str); printf( "the number is %d ,string is:%s ",

关于Android应用中图片占用内存浅谈

从事过移动端应用开发的童鞋应该都清楚,内存是非常宝贵的资源.如果能很好的利用有限的内存,对应用性能的提升会有很大的帮助.在实际应用开发中图片内存占整个应用非常大的比重,我们只有了解图片是如何加载到内存中,才能更好的优化图片所占的内存. 那么对于图片在Android应用中的使用进行探讨一下. 图片所占内存大小=内存中图片高 * 内存中图片宽 * 每个像素所占字节数   1,那么图片的物理高宽和加载到内存中的高宽是否是一样的呢? 在应用中关于图片的加载一般分为两种情况,其加载到内存中的大小也是不一样

【C语言】 浅谈指针

指针是就是地址,是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址. 要搞清一个指针需要搞清指针的四方面的内容:指针的类型.指针所指向的 类型.指针的值或者叫指针所指向的内存区.指针本身所占据的内存区.让我们分别说明. 首先,先罗列出几种常见的类型: int p;   //这是一个普通的整型变量int *p;  //首先从P处开始,先与*结合,所以说明P是一个指针,然后再与int结合,说明指针所指向的内容的类型为int型.所以P是一个返回整形数据的指针 int p[3];  //首先从

C语言中的位拷贝与值拷贝浅谈(转载)

注:C语言实现的PHP变量的赋值过程中,就涉及到了 深拷贝和浅拷贝 位拷贝拷贝的是地址(也叫浅拷贝),而值拷贝则拷贝的是内容(深拷贝).深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝. 位拷贝,及"bitwise assignment"是指将一个对象的内存映像按位原封不动的复制给另一个对象,所谓值拷贝就是指,将原对象的值复制一份给新对象. 在用"bitwise assig

浅谈C语言中的联合体(转载)

联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union).在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构: 2)它的所有成员相对于基地址的偏移量都为0: 3)此结构空间要大到足够容纳最"宽"的成员: 4)其对齐方式要适合其中所有的成员: 下面解释这四条描述: 由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的.为

【内存类操作】浅谈内存拷贝异常

结合本人在实际项目中所积累的经验,以及曾经犯过的错误,堆内存操作类函数做一个简单的剖析,抛砖引玉,欢迎大家吐槽. 首先,讲一下内存使用异常发生的几种场景. 1.野指针的使用,使用已经释放的指针,如果向野指针中写内容,就极有可能导致设备重启或任务挂死.因为,正在运行的任务的地址被意外的改写. [避免策略]函数入参要判空,指针使用(包括释放)之前一定要释放. 2.内存函数的错误使用: void *memset(void *s, int ch, size_t n); c语言中在<memory.h>或

Web服务器和动态语言如何交互--CGI&amp;FastCGI&amp;FPM浅谈

一个用户的Request是如何经过Web服务器(Apache,Nginx,IIS,Light)与后端的动态语言(如PHP等)进行交互并将结果返回给用户的呢? 本文浅谈个人观点,可能有误,欢迎拍砖,共同学习. 一. 首先明确几个概念,以便后续说明 CGI:(Common Gateway Interface)Http服务器与后端程序(如PHP)进行交互的中间层. 工作原理及处理方式(fork-and-execute模式): 1.当Web Server有Request到达 2.fork一个CGI进程或

关于分布式程序 java的内存管理浅谈

关于分布式程序 java的内存管理浅谈 标签(空格分隔): 分布式 内存管理 java Preface 当前全球正处于互联网时代,是个信息大爆炸时代.对于商家来说,每一天信息都是宝贵的,都可以转换成money的.所以对数据的处理要求也变的越来越严格,从以前的hadoop/MapReduce 的离线处理,到现在的准实时和实时处理,都是由数据需求而引起的技术革命.数据的处理快慢取决于很多因素.现在主流的解决方法,像Spark,Flink,Pular,包括腾讯,阿里,百度的诸多为开源的框架都是基于分布