文章目录
C语言梦开始的地方19:动态内存管理
在了解动态内存管理的知识之前我们先看一下这个:
C/C++中程序内存区域划分:

为什么要有动态内存分配
我们之前掌握的内存开辟方法:
int value = 20;
int arr[20] = {0};
但是这种开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
- 申请内存:
malloc,calloc和realloc - 释放内存:
free
malloc 和 free
malloc
C语言提供了一个动态内存开辟的函数:
void malloc(size_t size);
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个
NULL指针,因此malloc的返回值一定要做检查。 - 返回值的类型是
void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size为0,malloc的行为是标准是未定义的,取决于编译器。
free
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void* ptr);
free函数用来释放动态开辟的内存。- 如果参数
ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。 - 如果参数
ptr是NULL指针,则函数什么事都不做。
- 如果参数
malloc和free都声明在stdlib.h 头文件中。
malloc 和 free搭配使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d",&num);
int *ptr = NULL;
ptr = (int* ) malloc(num * sizeof(int)); // 申请num个int大小的内存空间
if(ptr == NULL)
{
perror("malloc");
return -1;
}
for(int i = 0; i < num;++i)
{
*(ptr + 1) = 0;
}
free(ptr);
ptr = NULL; // 都free了,这一步是否还有必要
return 0;
}
最后代码都释放了ptr申请的内存空间,还有必要再将ptr置为NULL吗?
有必要,这一步是为了防止释放后对指针的二次使用进行的冗余操作。因为释放内存后原指针就成了野指针,再将其置空是为了避免使用野指针。
calloc 和 realloc
calloc
C语言还提供了一个函数叫calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_1 t num, size_t size);
- 函数的功能是为
num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。 - 与函数
malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d",&num);
int *ptr = NULL;
ptr = (int*)calloc(num,sizeof(int));
if(ptr == NULL)
{
perror("calloc");
return -1;
}
for(int i = 0; i < num;++i)
{
printf("%d ",*(ptr + i));
}
printf("\n");
free(ptr);
ptr = NULL;
return 0;
}
realloc
relloc函数的出现让动态内存管理更加灵活。- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那
realloc函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
ptr是要调整的内存地址size调整之后新大小- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:- 情况1:原有空间之后有足够大的空间
- 情况2:原有空间之后没有足够大的空间

情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
所以为了避免后面没有足够多的空间时可以这样处理:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = NULL;
ptr = (int *)malloc(100 * sizeof(int));
if(ptr == NULL)
{
perror("malloc");
return -1;
}
// 如果直接赋值,若失败则丢失原内存
// ptr = (int*)realloc(ptr, 1000);
// 正确写法:
int *temp = NULL;
temp = (int* )realloc(ptr,1000 * sizeof(int));
if(temp != NULL)
{
ptr = temp;
}
else{
free(temp);
temp = NULL;
perror("realloc");
return -2;
}
free(ptr);
ptr = NULL;
return 0;
}
上述就是C语言提供的开辟内存函数和释放内存函数的常见使用方法。
常见的动态内存的错误
对 NULL 指针解引用
void test()
{
int *p = (int *)malloc(INT_MAX / 4); // 极可能失败
*p = 10; // 若 p==NULL,程序崩溃
free(p);
}
所以每次申请内存时都要进行判断,这样不仅能防止上述错误出现并且在后期调试时能快速定位到错误。
对动态开辟空间的越界访问
void test()
{
int i = 0;
int *p = (int *)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i; // 当i是10的时候越界访问
}
free(p);
}
释放非动态内存
void test()
{
int a = 10;
int *p = &a;
free(p);
}
只释放部分内存
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
p++ 指向了第二个元素,再进行free释放时指向的就不是起始地址了
重复释放同一块内存
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p); // 重复释放
}
这里和free后指针置NULL是一个道理,free后ptr就成了野指针,对野指针进行操作就会发生未定义行为,所以每次free后最好置NULL
内存泄漏(忘记释放)
void test()
{
int *p = (int *)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
return 0;
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放。
动态内存经典题目
题目1:
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
结果:程序崩溃。
原因:C 函数参数是值传递,p 是 str 的副本,修改 p 不会改变 str。所以这里应该传二级指针或者返回指针。
题目2:
char* GetMemory() {
char p[] = "hello world";
return p;
}
void Test() {
char* str = GetMemory();
printf(str);
}
结果:打印乱码或崩溃(悬垂指针)。
原因:p开辟在栈上生命周期随着return p; 就被编译器收回了,此时接收就不知道是什么了。
题目3:
void GetMemory(char** p, int num) {
*p = (char*)malloc(num);
}
void Test() {
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str); // 打印 "hello"
}
结果:内存泄漏。
原因:未释放申请的内存
题目4:
void Test() {
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL) {
strcpy(str, "world");
printf(str);
}
}
结果:未定义行为,可能崩溃也可能正常打印。
原因:free 后str 成为野指针。free 不会修改 str 的值,所以 if(str != NULL) 恒真,但访问已释放内存是不安全的。解决方法就是free后置NULL
柔性数组
在我们了解数组章节时讲到过一个知识点就是 柔性数组 ,柔性数组是C99推出的新语法。
C99 允许结构体的最后一个成员是未知大小的数组,这就叫柔性数组成员。
例如:
struct st_type {
int n;
int a[0]; // 柔性数组成员
};
有些编译器会报错无法编译可以改成:
struct st_type {
int n;
int a[];
};
柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
简单的理解就是sizeof(st_type);时柔性数组不参与计算,使用malloc时额外分配空间给数组使用。
示例:
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
} type_a;
int main()
{
printf("%d\n", sizeof(type_a)); // 输出的是4
return 0;
}
柔性数组的使用
那么在动态内存管理中有什么用呢?我们看下面这个示例:
typedef struct st_type
{
int i;
int a[0]; // 柔性数组成员
} type_a;
int main()
{
int i = 0;
type_a *p = (type_a *)malloc(sizeof(type_a) + 100 * sizeof(int));
// 业务处理
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
return 0;
}
那么上述代码的优势在哪里呢?我们再看一下这个代码:
typedef struct st_type
{
int i;
int *p_a;
} type_a;
int main()
{
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i * sizeof(int));
// 业务处理
for (int i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
// 释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
我们对比两个代码,我们会发现代码1申请内存更方便并且释放时也只需要释放一次。
代码2内存申请了2次这样就可能导致内存不连续,而代码1申请了1次那么代码1的内存一定是连续的,这样在后续搜索时性能代码1会更优。
完

2343

被折叠的 条评论
为什么被折叠?



