动态内存函数介绍
malloc函数
void *malloc( size_t size );
#include<malloc.h>或#include<stdlib.h>
功能:
在堆区上申请空间.
返回值为void*的指针
size的单位是字节你输入多少就开辟多少字节的内存
当空间开辟失败的时候返回NULL指针
malloc函数开辟的空间为随机值
在使用的时候直接用指针进行接收(记得对malloc进行强制类型转换(部分编译器不需要))
开辟成功的内存可以当做数组进行使用
free函数
使用完毕后记得归还使用free函数
void free( void *memblock );
直接free(指针)就可归还空间
但我们归还空间后记得要将指针置换成空指针以免未来有人非法访问
free只能归还堆区的内存.
calloc
与malloc相似但是更加严谨一些
void *calloc( size_t num, size_t size );
返回类型依旧为void*指针num表示数量size表示大小
如申请10个int类型的空间就可以写
int* p = (int*) calloc(10,sizeof(int))
并且我们的calloc函数会将申请内存初始化为零
realloc(重要)
void *realloc( void *memblock, size_t size );
重新调整内存空间可以放大可以缩小
memblock为所需调整的指针指向的内存
size是重新开辟的内存空间的大小(单位是字节)
当realloc中的memblock传的是NULL时realloc的作用和malloc相似.
返回void*类型返回地址分三种情况:
1.后续连续空间足够
如图:
直接在后面开辟空间,返回的void*类型的原指针地址
2.后续连续空间不足但其他部分空间足够
返回新开辟的空间开头地址,并把原内存的数据拷贝到新空间内然后将原内存free化还给操作系统
3.堆区没有可以创立的内存空间了,直接返回空指针.
所以我们在使用realloc函数时要先使用了临时指针进行接收来判断是否为空指针后再判断是否要用常用指针进行接收
动态内存开辟常见的错误
对NULL指针的解引用操作
解决方法:
对可能为空指针的指针进行判断
如
int main() { int* p =malloc(100000000*sizeof); if(p==NULL) { return 1; } return 0; }
对动态内存进行越界访问
我们的动态内存类似指针我们数组,越界访问会得到我们不想得到的效果.
使用free释放非动态开辟的空间
我们的free只能释放堆区的内存.
如果我们使用free释放其他部分的内存编译器就会报错.
使用free释放动态内存的一部分
错误示范:
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
我们这个只释放了96个字节的空间
所以我们的p轻易不要改变他的数值.
没有free(内存泄漏)
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
我们在test函数里申请了100个字节的内存但是却没有在函数结束将空间归还回去,每当我们使用一次test函数时,我们就会重新申请一片内存空间但是都没有归还,当我们的程序是24执行的服务器时,我们内存空间就会一直泄漏下去.服务器就会越来越卡,最后只有重启但是重启后如果不更改依旧会慢慢泄漏.
而且这类错误我们的编译器是不会报错的!所以危害极大.
所以我们用完从堆区申请的内存后记得归还且要正确归还
练习题
第一题
void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str);//正确的 }
运行结果为:没有打印代码直接挂掉
分析:我们在GetMemory函数传参的时候知识将str指向的内容传过去所以p值的变化对str没有任何改变
所以str依旧是空指针,我们不能对空指针进行strcpy.
所以程序会挂掉.
而且此程序没有将从堆区申请的空间归还所以还会内存泄漏.
(这道题的printf函数的使用有些不常规但是这也是正确的使用方式)
第二题
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
运行结果: 烫烫烫烫烫
解析:在GetMemory函数中的p指针是一个临时变量在函数运行结束后虽然依旧将地址传给了str但是地址在函数运行结束后就被释放了所以我们打印的只是一个野指针带来的地址故打印出随机值.
(我们称这种题叫返回栈空间地址的问题,在生命周期结束后就自动还给操作系统,在堆上开辟程序就正常)
第三题
void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); }
结果:hello
结果正常但是没有free存在内存泄漏
第四题
void Test(void) { char *str = (char *)malloc(100); strcpy(str, "hello"); free(str); if(str != NULL) { strcpy(str, "world"); printf(str); } }
运行结果: world
但是这依旧是个错误的代码.我们在将指针释放后一定要记得将他赋值成NULL指针.
不然就有可能出现以上错误.
C/C++程序的内存开辟
我们在操作系统篇会对内存进行更加详细的介绍我们现在只需对这些内存空间进行简单的了解即可.
所分区域
栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。
柔性数组
是在C99时引入的可以在结构体末尾放一个大小位置的数组
typedef struct st_type { int i; int a[0];//柔性数组成员,有时编译器会报错我们可以用int a[]; }type_a; //大小为4
柔型数组的特点
前面必须至少有一个其他成员
sizeof计算时不会包括柔型数组的大小如上面的结构体大小就为4
在动态分配时至少要大于结构体的大小以适应柔型数组的预期大小
好处
方便内存的释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。