PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

C语言基础(15)_内存分配

 

今天学习C语言的内存分配。

 

首先,要了解C语言程序的内存分配图

一个由C/C++编译的程序占用的内存分为以下几个部分:

从最低内存地址开始到最高内存地址,分为代码区(code area)、数据区(data area)、堆区(heap area)、栈区(stack area)、命令行参数区

 

栈区(stack area):由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构的栈。

堆区(heap area):一般是由程序员分配与释放,若程序员不释放的话,程序结束时可能由操作系统回收,注意它与数据结构的堆是两回事。

程序代码区(code area):存放函数体的二进制代码。

 

数据区(data area):

bss段(又叫ZI(zero initial)段):存放初始化为0的全局变量/(局部)静态变量。(有些地方把它独立于数据区);bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。初始化为0,有两种情况,一种是显式初始化为0,另外一种是没有显式初始化(C语言规定未显式初始化的全局变量/(局部)静态变量值默认为0)。

显式初始化为非零的全局变量/(局部)静态变量区:也叫静态数据内存空间,存储全局变量和(局部)静态变量,全局变量和静态变量的存储是放一块的,初始化为非零的全局变量和(局部)静态变量放一块区域,而没有初始化或初始化为零的在相邻的另一块区域(bbs段),程序结束后由系统释放。

文字常量区:常量字符串就是放在这里,程序结束后由系统释放;

 

例子:

int   a  =  1;  // 显式初始化为非零的全局变量/(局部)静态变量区

char   *p1;  // bss段

main()

{

int   b;  // 栈

char   s[ ]   =   “abc”;  // s在栈,”abc”在文字常量区(这个好像不对?)

char   *p2;   // 栈

char   *p3   =   “12345”;  // p3在栈,”12345″在文字常量区

static   int   c = 0;// bss段

p1   =   (char*)malloc(10);  //p1在bss(前面定义的),分配的10字节内存在堆

p2   =   (char*)malloc(20);  //p2 在栈,分配的20字节内存在堆

strcpy(p1, “12345”); // “12345”在文字常量区,编译器可能会将它与p3所指向的”12345″优化成一个地方。

}

内存申请方式

栈:由系统自动分配。例如,声明在函数中一个局部变量int a; 系统自动在栈中为b开辟空间(4字节)。

堆:需要程序员自己申请,并指明大小,在c中常用malloc函数,例如p1 = (char*)malloc(10); 在C++中用new运算符,例如p1 = new char[10]; 注意p1本身是在栈中,它指向的内存在堆中。

 

申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的free/delete语句才能正确的释放本内存空间。还有,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

 

申请大小的限制:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是:栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

 

申请效率的比较:

栈:由系统自动分配,速度较快。但程序员是无法控制的。

堆:是由malloc/new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。另外,在Windows下,最好的方式是用VirtualAlloc分配内存(以后学习),他不是在堆,也不是在栈是,直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

 

堆和栈中的存储内容:

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。(注意静态变量是不入栈的。不在这里存)

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

 

存取效率的比较:

char   s1[]   =   “aaa”;

char   *s2   =   “bbb”;

aaa是在运行时刻赋值的;而bbb是在编译时就确定的;

但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

比如:

#include

void   main()

{

char   a   =   1;

char   c[]   =   “1234567890”;

char   *p   =”1234567890″;

a   =   c[1];

a   =   p[1];

return;

}

对应的汇编代码

10:   a   =   c[1];

00401067   8A   4D   F1   mov   cl,byte   ptr   [ebp-0Fh]

0040106A   88   4D   FC   mov   byte   ptr   [ebp-4],cl

11:   a   =   p[1];

0040106D   8B   55   EC   mov   edx,dword   ptr   [ebp-14h]

00401070   8A   42   01   mov   al,byte   ptr   [edx+1]

00401073   88   45   FC   mov   byte   ptr   [ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。

 

C语言的动态内存分配,也就是堆内存的申请:

malloc/free函数

原型:void *malloc(size_t size);   //返回类型为空指针类型

void free(void *ptr);

malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。

由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源,所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。

 

这两个函数的库头文件为:stdlib.h 或者 malloc.h

 

例子:

int *p1,*p2;

p1=(int *)malloc(10*sizeof(int));

p2=p1;

……

free(p2) ;      //或者free(p1)也可以,但是只能是一个

p1=NULL; p2=NULL;

malloc()函数返回值赋给p1,又把p1的值赋给p2,所以此时p1,p2都可作为free函数的参数。使用free()函数时,需要特别注意下面几点:

 

(1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后,很有可能该指针仍然指向该内存单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。

 

(2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。

 

(3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。

 

例子:

int main(){

int count;

int* p;

if(NULL==(p=(int *)malloc(10*sizeof(int))))  //分配空间 10个int=40字节

{

printf(“malloc memory unsuccessful\n”);

return 1;

}

for (count=0;count<10;count++)      // 赋值

{

*p=count;

++p;

} //注意这里的 p 的指向已经变化了

for(count=9;count>=0;count–)         //打印,反向输出的

{

–p;

printf(“%4d”,*p);

}

printf(“\n”);

free(p);        //释放空间

p=NULL;       //将指针置为空,避免野指针

return 0;

}

//输出 9 8 7 6 5 4 3 2 1 0

 

realloc函数:更改已经配置的内存空间

原型:void *realloc(void *ptr,size_t size); 参数ptr为先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小

realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉。如果内存不足,重新申请空间失败,则返回NULL。

当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置,返回新内存的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间,原来的指针变为不可用(即不需要再释放,也不能再释放),因此,一般不使用以下语句:

ptr=realloc(ptr,new_amount); //这样使用不好,假如失败,ptr也没了

而习惯这样使用:

p2=(int *)realloc(p1,10*sizeof(int));  //用 2个不同的指针 p1表示原来的,p2用来接收

if (NULL==p2) {  //p2 ==NULL 表示申请失败

printf(“realloc error!\n”);

}

如果内存减少,realloc仅仅改变索引信息,但并不代表被减少的部分还可以访问,这一部分内存将交给系统内存分配程序。

 

calloc函数

void *calloc(size_t nmemb,size_t size);

calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似malloc()函数。只是参数有所变化,总字节数等于2个参数的乘积。

如:ptr=(struct data *)calloc (count,sizeof(strunt data)) //申请并初始化空间

 

下面是这个函数的实现描述:

{

void *p;

size_t total;

total=nmemb *size;

p=malloc(total);         //申请空间

if(p!=NULL)

memset(p,’\0′,total);      //将其实始化为\0

return p;

}

 

alloca函数:(没明白)

extern void *alloca (size_t __size) __THROW;   //从栈中申请空间

返回值:若分配成功返回指针,失败则返回NULL。

alloca()函数用来在栈中分配size个字节的内存空间,因此函数返回时会自动释放掉空间。

它与malloc()函数的区别主要在于:

alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中,最终需要函数free来释放。

 

(2017-02-27 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » C语言基础(15)_内存分配

分享到:更多 ()

评论 76

评论前必须登录!