动态内存管理(C语言)
为什么存在动态内存分配
我们已经掌握了在内存开辟的方式
int n = 10;
int arr[10];
但是这样的开辟方式存在两个特点:
1.空间开辟大小是固定的
2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
我们将内存划分为3个部分,分别为:栈区、堆区、静态区。栈区用来存放局部变量,函数形式参数,堆区用来存放动态内存,静态区用来存放全局变量,静态变量。
动态内存函数的介绍
malloc()函数——申请空间
形式:void *malloc( size_t size );
解释:malloc返回一个指向已分配空间的空指针,如果可用内存不足,则返回NULL。要返回指向void以外类型的指针,请对返回值使用类型强制转换。返回值指向的存储空间保证适当对齐以存储任何类型的对象。
例子:
#include<stdio.h>
int main()
{
int* p = (int*)malloc(40);
return 0;
}
malloc()函数是向内存中申请一块连续的空间,“40”表示申请的内存空间大小(单位:字节),申请完后mallco()函数会返回一个void类型的指针,因此,如果我们想用来存放整型,我们可以强制类型转化为int,并用int指针接收。
注意点:
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
free()函数——释放空间
形式:void free( void *ptr );
解释:free函数释放以前通过调用calloc、malloc或realloc分配的内存块(ptr)。释放的字节数等于分配块时请求的字节数(如果是realloc,则为重新分配)。
例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//释放空间
free(p);
p = NULL;
return 0;
}
当malloc()函数开辟出空间时,我们对空间一系列操作后,当我们最后不在使用时,需要释放掉该空间,因此引入free()函数,我们将指针p作为参数传给free()函数,操作系统会将申请的40个字节的空间释放掉,但是此时的p指针仍然指向该内存,会造成野指针,因此我们需要将指针置为NULL。
注意点:
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
calloc——申请空间并初始化
形式:void* calloc (size_t num, size_t size);
解释:calloc返回一个指向已分配空间的指针。返回值指向的存储空间保证适当对齐以存储任何类型的对象。(这个函数是很malloc()函数及其的相似的,不同就在于calloc()函数在申请内存空间后同时会初始化为0。)
例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
return 0;
}
在这个例子中,calloc()函数就是申请10个字节的int类型的空间并且将所有的整型变为0。
注意点:
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
realloc()函数——调整动态内存空间
形式:void* realloc (void* ptr, size_t size);
解释:realloc返回一个指向重新分配(可能已移动)内存块的空指针。如果大小为零且缓冲区参数不为NULL,或者如果没有足够的可用内存将块扩展到给定大小,则返回值为NULL。在第一种情况下,原始块被释放。在第二种情况下,原始块保持不变。返回值指向一个存储空间,该存储空间保证为存储任何类型的对象而适当对齐。
例子:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
return -1;
}
else
{
printf("申请成功\n");
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
//释放空间
//增加空间至20个int
int* ptr = (int*)realloc(p, 20 * sizeof(int));
free(p);
p = NULL;
return 0;
}
我们可以看到,我们先用calloc()函数申请10个整型的空间,在后续的过程中,我们发现空间不够时,我们需要拓宽空间,我们就可以使用realloc()函数进行拓宽,同样的,该函数也会返回首个类型的地址,我们也需要用指针接收该地址才能进行相关的操作。
注意点:
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
常见的动态内存的错误
1、对NULL指针的解引用操作
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
*p = 0;
return 0;
}
我们直接对p指针解引用是有风险的,原因就在于malloc()函数开辟空间失败,那么返回的指针就是空指针,这就会导致对空指针进行解引用,引起程序错误。
正确代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
return -1;
}
else
{
*p = 0;
}
return 0;
}
我们在解引用时先进行判断,如果返回是NULL指针,那么就结束,如果不是那么可以进行后续的操作。
2、对动态空间的越界访问
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(200);
if (p == NULL)
{
return -1;
}
else
{
int i = 0;
for (i = 0; i < 80; i++)
{
*(p + i) = i;
}
for (i = 0; i < 80; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
}
return 0;
}
乍一看似乎没有错误,其实不然,我们创建200个字节的动态内存,也就是50个整型,但是我们用了80次循环,这就会导致越界行为,导致程序错误。所有我们在开辟动态内存后要关注是否会导致越界。
正确代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(200);
if (p == NULL)
{
return -1;
}
else
{
int i = 0;
for (i = 0; i < 50; i++)
{
*(p + i) = i;
}
for (i = 0; i < 50; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
}
return 0;
}
使用50次循环,也就用到了50个整型,200个字节,不会导致空间的越界访问。
3、对非动态开辟内存使用free释放
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
我们在前面介绍free()函数介绍到,free()函数是用于动态内存开辟的释放,也就是说不是动态内存的空间是不能用free()函数释放的,因此会报错。
4、使用free释放一块动态开辟内存的一部分
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return -1;
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
*p++ = i;
}
free(p);
p = NULL;
}
return 0;
}
我们知道当动态内存不在需要时,要释放还给操作系统,但是我们会释放一部分,上面的代码中*p++ = i已经改变了p,最后p指向的是“5”这个地址位置,因此free(p),只是释放掉“5”后面的位置,前面的并没有改变。因此程序错误。
正确代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return -1;
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
*(p+i) = i;
}
free(p);
p = NULL;
}
return 0;
}
这样写代码,并不会导致p发生变化,因此free(p)能够完全将动态内存够给释放掉。
5、对同一块动态空间多次释放
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return -1;
}
else
{
//....(代码)
//....(代码)
free(p);
//....(代码)
//....(代码)
free(p);
}
return 0;
}
我们开辟一块动态内存空间使用后我们用free()函数释放掉,在进行操作后,有重复释放掉上一次的空间,会产生错误。
正确代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return -1;
}
else
{
//....
//....
free(p);
p = NULL;
//...
//....
free(p);
p = NULL;
}
return 0;
}
我们在释放动态内存空间后,要将p指针置为NULL指针,我们上面说到过,free(NULL)什么事情都不干,因此不会报错,所以,我们要养成动态内存释放后将指针置为NULL的好习惯。
6、动态内存空间忘记释放
错误代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return -1;
}
return 0;
}
我们开辟了40个字节的动态内存,但是并没有释放,当程序没有结束时,这40个字节一直被占用,如果有其他的一直需要开辟动态内存空间,而不释放已经用过且不会再用的空间,就会导致内存越来越少,最后导致程序没有空间继续进行,程序崩溃。因此,当我们使用完动态内存,且不会在使用时,需要我们使用free()函数释放掉动态内存。
柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。 C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
举个例子:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
};
a[ ]数组是结构体st_type中的最后一个成员,a[ ]数组并没有指定大小,因此a[ ]数组叫做柔性数组。
柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
对于第三点,举个例子:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
//包含柔性数组成员的结构体的使用,要配合malloc()函数等使用
struct st_type* ps = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
if (ps == NULL)
{
printf("%s\n", strerror(errno));
return -1;
}
return 0;
}
根据第二点,我们知道此时的sizeof的大小为4,因此用传统的创建结构体变量会导致结构体大小为4,数组a[ ]就无法存放元素,所以柔性数组需要跟动态内存开辟结合起来,例子中,我们利用malloc()函数开辟动态空间,sizeof(struct st_type) 是结构体的大小,也就是4个字节,而10 * sizeof(int)是10个整型的大小,也就是10个整型数组元素的大小。这样创建我们就可以利用到了柔性数组的特点,后期也可以通过realloc()函数进行调整。