动态内存管理
什么是动态内存分配
个人认为:初学者最需要掌握的是内存方面的芝士。
那么内存分配有动态和静态之分。
什么是静态的?
如下:
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
这里就是静态的内存分配,很常见就不展开讲了。
但是静态开辟的空间有些缺点:
- 空间大小是固定的,定义了之后就不能再改变了。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是有时候我们需要的空间随着程序的进行发生变化的,这个时候如果还定义的是静态的话就会导致最后需要的空间满了,不能再继续使用了。
这时候就需要动态内存开辟了。
那么什么是动态的?
我先给个例子再来讲:
int* p = (char*)malloc(sizeof(int));
这就是一个动态的。
程序内存的开辟
内存大概分为下面这几个区:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
如果看你看起来模模糊糊的,不是很懂这些的区别,那么就说明你在内存空间这块学的不是很扎实,没有关系,好好学,总能搞懂的。
有什么用呢?下面细🔒:
动态内存函数的介绍
下面的这些函数使用时都要引头文件stdlib.h
malloc
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
注意事项:
- 参数size代表的是你所需要在内存中开辟的字节数,为无符号整型,当然传参时候也可以传整型。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
给个例子看一下:
//创建
int* p = (char*)malloc(sizeof(int));
if(p == NULL)
{
exit(-1);
}
//使用
//。。。。
//销毁
free(p);
在我上面给的例子中,malloc只是创建出来一块空间,创建完之后还要检查一下是否创建成功了就是判断指向空间是否为空,若为空了,直接退出程序(exit(-1)),若不为空就使用所开辟的空间,使用完之后一定要记得销毁那片空间,不然可能会导致内存泄漏等问题。
那么最下面的函数free是干啥用的呢?
free
void free (void* ptr);
这个函数是用来释放动态开辟的内存的。
注意事项:
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
这两点要记清,有些题会涉及到。
再来给一个简单的例子:
//开辟
int* p = (int*)malloc(sizeof(int) * 10);
//判断是否开辟成功
if (p == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//使用
for (int i = 0; i < 10; i++)
{
//这里的p[i]和*(p+i)是等价的
p[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//释放
free(p);
结果如下:
calloc
void* calloc (size_t num, size_t size);
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
这个函数比malloc多了一个功能,开辟的同时可以把所开辟的空间内部的值全设置为0。
注意事项:
这里是num×size才是总共的字节数。指的是num个size的大小
给个例子:
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (NULL != p)
{
//使用空间
}
free(p);
p = NULL;
return 0;
}
所以当我们想要申请空间并进行初始化时就可以用calloc,想要申请空间但是不需要初始化时就可以用malloc。
realloc
void* realloc (void* ptr, size_t size);
注意事项:
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
这个函数更🐂一些,可以将原空间中的数据保存并且在其后面延长申请一段空间。
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
realloc申请空间时分三种情况:
第一种:
所申请的空间在原空间和其后的空间够用,此时开辟就是直接在原空间后方延续上一段空间。并且返回总共开辟的空间的首地址。
第二种:
所申请的空间在原空间和其后的空间不够用,就在内存中找一块够用的空间,然后将原空间中的数据拷贝到新开辟的空间,然后释放掉原空间,最后返回新开辟空间的首地址。
第三种:
在内存中找不到所需要的足够的新的空间大小,此时直接返回空指针NULL。
例子:
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
//业务处理
}
else
{
exit(-1);
}
//扩展容量
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
free(ptr);
return 0;
}
上面的例子中ptr = (int*)realloc(ptr, 1000);
这条语句是不能用的,因为当realloc时,若内存中找不到足够的空间开辟的话,此时会返回NULL,但是我们本身的ptr是有指向空间的,这里若返回了NULL就会导致我们找不到本身的那片空间。
所以一般用reallo时需要用一个临时的指针来接收新开辟的空间,这样就算开辟空间不足的时候也不会直接将ptr改成NULL,而时将那个临时的指针改为NULL。
再给一个正确的例子:
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
//业务处理
}
else
{
exit(-1);
}
int* p = NULL;
p = (int*)realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
//业务处理
free(ptr);
return 0;
}
常见的动态内存错误
动态开辟的空间,要么由程序员手动释放,要么程序结束后系统自动回收,但也存在有些内存没有手动释放而且系统无法自动回收的。
下面是一些常见的错误,要理解清楚。
对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(sizeof(int));
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
这里没有对p是否开辟成功进行判断,就可能会导致出现p为空指针的情况,空指针是不能解引用并改值的,这种写法在编译器上是会报警告的,而且程序会直接崩掉。
所以正确的写法应该是这样:
void test()
{
int *p = (int *)malloc(sizeof(int));
if(p == NULL)
{
printf("malloc fail\n");
exit(-1);
}
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
警告就消失啦。
对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(-1);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
这里的for循环总共有11次,但是p只开辟了10个int,所以当i等于10的时候就会导致越界访问,从而导致程序崩掉。
正确的写法是将i <= 10改为i < 10;
对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
这里的a只是一个局部变量,并不是在堆区上的,释放掉a就会将a的生命周期强制结束,导致程序错误。
使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
假如说p原本指向的地址为0x0012ff40,那么p++,p指向的地址就变成了0x0012ff44,这时候释放空间就没法将所有的空间全部释放,程序就直接崩掉了。
程序会直接崩掉。
对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
这里当我们释放掉了p后,p所指向的空间被回收。
我们再次释放p,p可以找到原先的空间,但是会导致非法访问。
但如果我在释放了之后就直接将指针置空就不会出现错误。
void test()
{
int *p = (int *)malloc(100);
free(p);
p = NULL;
free(p);//这里就不会重复释放,因为free传空指针时会什么也不干。
}
动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
这里程序没有终止,一直在循环体while中,这样就会导致无法释放p。
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:
动态开辟的空间一定要释放,并且正确释放 。
几道经典的笔试题
题目1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这道题目结果是什么也没打印,直接挂掉了。
看图解:
这里还导致了内存泄漏,如果想要通过p来改变str的话,就得要将p的类型改为char**的。
void GetMemory(char **p)
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这样就成功了。
题目2:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这里GetMemory函数中的p只是一个局部的变量,p中放的是字符串hello world中首元素的h的地址,return这个地址之后,p所指向的空间就销毁了,但是在销毁之前还是把p指向的地址传给了str,这就导致了str变成了野指针,打印出来的结果就是乱码。
题目3:
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
这道题的问题是没有释放掉malloc所开辟的空间,导致内存泄露了。
题目4:
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
这道题的问题是非法访问,释放掉str后,内存已经还回去了,但是str还指向的是这块空间,str是不为NULL的,所以进去了if中,虽然程序能进行,但是还是导致了非法访问。
所以说要养成好习惯,free完一个空间后,一定要将指向那块空间的指针置为NULL。
上面的这四个例子,在开辟了空间之后都没有在后面判断是否开辟成功,但这不是什么大问题,最主要的问题还是上面解释中的。
柔性数组
可能你听说过变长数组,但是柔性数组这个名字可能你一次都没听过,那么只是什么呢?
给个例子:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
这里有些遍历器会报错,可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
我们挨个细🔒:
先看第二个:
2. sizeof 返回的这种结构大小不包括柔性数组的内存。
含有柔性数组的结构体在计算结构体大小的时候还是符合结构体内存计算的龟腚的。
只是变长数组不计在结构体的总大小的。
上面的结构体中没有记柔性数组的大小。
上面的结构体的大小是4,计算了内部那个数组的大小。
1. 结构中的柔性数组成员前面必须至少一个其他成员。
柔性数组一定是在结构体的最下面的。
3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
结构体最后一个放指针与柔性数组的区别:
我们先把指针的放出来:
struct MyStruct
{
int b;
char c;
int* a;
};
首先,这个结构体的大小是12,因为最后一个指针是记大小的。
再把这个的使用整出来:
struct MyStruct
{
int b;
char c;
int* a;
};
int main()
{
struct MyStruct* ms = (struct MyStruct*)malloc(sizeof(struct MyStruct));
if (ms == NULL)
{
printf("malloc fail\n");
exit(-1);
}
ms->b = 10;
ms->c = 'w';
ms->a = (int*)malloc(sizeof(int) * 10);
if (ms->a == NULL)
{
printf("malloc fail\n");
exit(-1);
}
for (int i = 0; i < 10; i++)
{
ms->a[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ms->a[i]);
}
printf("\n");
return 0;
}
二者的图解:
对于柔性数组:
对于最后一个为指针的结构体:
我们对比二者的图解就可以发现,最后一个为指针的结构体多malloc了一次,那么这样会导致操作麻烦了一点,当我们malloc次数越多的话,就会导致内存的堆区中的内存碎片变多。
那么这样对比下来我们就可总结出柔性数组的优点:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。
第二点讲起来的话就讲远了,记住就行。
到此结束。