C语言再学习 -- 内存管理

参看:malloc()和free()

参看:百度百科 -- malloc函数

malloc ( )函数:

malloc ( ) 向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以通过类型转换强制转换为任何其它类型的指针。

所在头文件:

stdlib.h

函数声明:

void *malloc(size_t size);

备注:void* 表示未确定类型的指针,void *可以指向任何类型的数据,更明确的说是指申请内存空间时还不知道用户是用这段空间来存储什么类型的数据(比如是char还是int或者其他数据类型)。

函数返回值:

如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象。


free ( )函数:

释放之前调用 calloc、malloc 或 realloc 所分配的内存空间

所在头文件:

stdlib.h

函数声明:

void free(void *ptr)

备注:ptr -- 指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。


函数用法:

type *p;
p = (type*)malloc(n * sizeof(type));
if(NULL == p)
/*请使用if来判断,这是有必要的*/
{
    perror("error...");
    exit(1);
}
.../*其它代码*/
free(p);
p = NULL;/*请加上这句*/


函数使用需要注意的地方:

1、malloc 函数返回的是 void * 类型,必须通过 (type *) 来将强制类型转换

2、malloc 函数的实参为 sizeof(type),用于指明一个整型数据需要的大小。

3、申请内存空间后,必须检查是否分配成功

4、当不需要再使用申请的内存时,记得释放,而且只能释放一次。如果把指针作为参数调用free函数释放,则函数结束后指针成为野指针(如果一个指针既没有捆绑过也没有记录空地址则称为野指针),所以释放后应该把指向这块内存的指针指向NULL,防止程序后面不小心使用了它。

5、要求malloc和free符合一夫一妻制,如果申请后不释放就是内存泄漏,如果无故释放那就是什么也没做。释放只能一次,如果释放两次及两次以上会出现错误(释放空指针例外,释放空指针其实也等于啥也没做,所以释放空指针释放多少次都没有问题)。


基础知识讲完了,下面开始进阶阶段:

1、malloc 从哪里得来的内存空间

malloc 从堆里获得空间,函数返回的指针指向堆里的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后寻找第一个空间大于所申请空间的堆结点,然后将该结点链表删除,并将该结点空间分配给程序。


2、free 释放了什么

free 释放的是指针指向的内存。注意,释放的是内存,而不是指针。

指针是一个变量,只有程序结束时才被销毁,释放内存空间后,原来指向这块空间的指针还是存在的,只不过现在指针指向的内容是未定义的,所以说是垃圾。因此,释放内存后要把指针指向NULL,防止指针在后面不小心又被解引用了。


/*
 	动态分配内存演示
 */
#include 
#include 
int *read(void)	//指针做返回值
{
	int *p_num=(int *)malloc(sizeof(int));
	if(p_num)
	{		//不可以在这个释放动态内存
		printf("请输入一个整数:");
		scanf("%d",p_num);
	}
	return p_num;
}
int main()	//动态分配内存可以实现跨函数存储区,动态分配内存被释放之前可以让任何函数使用
{
	int *p_num=read();
	if(p_num)
	{
	printf("数字是%d\n",*p_num);
	free(p_num);
	p_num=NULL;
	}
	return 0;
}


思考一个问题,上面的例子为什么不能在 int * read (void);函数里 释放和置空?

该例子说明,函数返回时函数所在的栈和指针被销毁,申请的内存并没有跟着销毁。因为申请的内存在堆上,而函数所在的栈被销毁跟堆完全没有啥关系,所以使用完后记得释放、置空。


3、工作机制

malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,因此在调用malloc动态申请内存块时,一定要进行返回值的判断。


进程中的内存区域划分
(1)代码区 存放程序的功能代码的区域,比如:函数名
(2)只读常量区 主要存放字符串常量和const修饰的全局变量
(3)全局区 主要存放已经初始化的全局变量和static修饰的全局变量
(4)BSS段 主要存放没有初始化的全局变量和static修饰的局部变量,BSS段会在main函数执行之前自动清零
(5)堆区 主要表示使用malloc/calloc/realloc等手动申请的动态内存空间,内存由程序员手动申请和手动释放
(6)栈区 主要存放局部变量(包括函数的形参),const修饰的局部变量,以及块变量,该内存区域由操作系统自动管理


内存地址从小到大分别是:
代码区 只读常量区 全局区 BSS段 堆 栈
其中堆区和栈区并没有明确的分割线,可以适当的调整

图示1:可执行程序在存储器中的存放


//进程中的内存区域划分
#include 
#include 
#include 
int i1=10;//全局区
int i2=20;//全局区
int i3;//BSS段
const int i4=40;//只读常量区
void fn(int i5)//栈区
{
	int i6=60;//栈区
	static int i7=70;//全局区
	const int i8=80;//栈区
	int* p1=(int*)malloc(4); //p1指向堆区 p1本身在栈区
	int* p2=(int*)malloc(4); //p2指向堆区 p2本身在栈区
	char* str="good";   //str 指向只读常量区
	//strs 在栈区
	char strs[]="good";
	printf("只读常量区:&i4=%p\n",&i4);
	printf("只读常量区:str=%p\n",str);
	printf("----------------------------\n");
	printf("全局区:&i1=%p\n",&i1);
	printf("全局区:&i2=%p\n",&i2);
	printf("全局区:&i7=%p\n",&i7);
	printf("----------------------------\n");
	printf("BSS段:&i3=%p\n",&i3);
	printf("----------------------------\n");
	printf("堆区:p1=%p\n",p1);
	printf("堆区:p2=%p\n",p2);
	printf("----------------------------\n");
	printf("栈区:&i5=%p\n",&i5);
	printf("栈区:&i6=%p\n",&i6);
	printf("栈区:&i8=%p\n",&i8);
	printf("栈区:strs=%p\n",strs);
}
int main()
{
	printf("代码区:fn=%p\n",fn);
	printf("----------------------------\n");
	fn(10);
	return 0;
}
输出结果:

代码区:fn=0x8048494
---------------------
只读常量区:&i4=0x80486e0
只读常量区:str=0x80486e4
---------------------
全局区:&i1=0x804a01c
全局区:&i2=0x804a020
全局区:&i7=0x804a024
---------------------
BBS段:&i3=0x804a034
BBS段:&i9=0x804a030
---------------------
堆区:p1=0x88e8008
堆区:p2=0x88e8018
---------------------
栈区:&i5=0xbfbcc060
栈区:&i6=0xbfbcc04c
栈区:&i8=0xbfbcc048
栈区:strs=0xbfbcc037

//字符串存储形式的比较
#include 
#include 
#include 

int main()
{
	//pc存储字符串的首地址,不能存内容
	//pc指向只读常量区,pc本身在栈区
	char* pc="hello";
	//str存储字符串的内容,而不是地址
	//str指向栈区,str本身在栈区
	//将字符串内容拷贝一份到字符串数组
	char str[]="hello";
	printf("字符串地址:%p\n","hello");
	printf("只读常量区 pc=%p\n",pc);
	printf("栈区 &pc=%p\n",&pc);
	printf("栈区 str=%p\n",str);
	printf("栈区 &str=%p\n",&str);
	printf("------------------------\n");
	//修改指向
	pc="1234";
//	str="1234";//数组名是个常量不可改变  error
	//修改指向的内容
//	*pc='A';//error  指针不可修改
	str[0]='A';
	printf("--------------------------\n");
	//在堆区申请的动态内存
	char* ps=(char*)malloc(10);
	//修改指向的内容
	strcpy(ps,"hello");
	//修改指向
	char* p=ps;
	ps="Good";
	//释放内存
	printf("堆区 ps=%p\n",ps);
	printf("堆区 p=%p\n",p);
	free(p);
	p=NULL;
	return 0;
}
输出结果:
字符串地址:0x80486e0
只读常量区 pc=0x80486e0
栈区 &pc=0xbf9499a8
栈区 str=0xbf9499b6
栈区 &str=0xbf9499b6
------------------------
--------------------------
堆区 ps=0x804877a
堆区 p=0x8eb2008


什么是堆、栈?

参看:堆栈详解

参看:栈,堆,全局,文字常量,代码区总结

:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间,堆在操作系统对进程初始化的时候分配,运行过程中也可以像系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏


:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。每个C++对象的数据成员也存在在栈中,每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候回自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要再高级语言里面显式的分配和释放。


示例:

//main.cpp 
int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 
{ 
int b; 栈 
char s[] = "abc"; 栈 
char *p2; 栈 
char *p3 = "123456"; 123456在常量区,p3在栈上。 
static int c =0; 全局(静态)初始化区 
p1 = (char *)malloc(10); 
p2 = (char *)malloc(20); 
分配得来得10和20字节的区域就在堆区。 
strcpy(p1, "123456"); 123456放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
}


堆和栈的理论知识:

1、申请方式:

stack 栈 :遵循 LIFO后进先出的规则,它的生长方向是向下的,是向着内存地址减小的方向增长的,栈是系统提供的功能,特点是快速高效,缺点是有限制,数据不灵活。由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。

heap 堆:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向。需要程序员自己申请,并指明大小。

在 C 中 malloc 函数,如: P1 = (char *)malloc (10);

在 C++ 中用 new 运算符,如: P2 = new char[10];

它申请的内存是在堆中,但是注意P1、P2本身是在栈中的。


2、申请后系统的响应

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

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


3、申请大小的限制

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

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


4、申请效率的比较

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

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


5、堆和栈中的存储内容

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

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

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


6、存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa"; 
char *s2 = "bbbbbbbbbbbbbbbbb"; 
aaaaaaaaaaa是在运行时刻赋值的; 
而bbbbbbbbbbb是在编译时就确定的; 
但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。 
比如: 
#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读取字符,显然慢了。


7、小结

堆和栈的区别可以用如下的比喻来看出: 
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

你可能感兴趣的:(C语言再学习,C语言再学习)