
文章目录
这里是think的博客
希望可以一起交流知识,一起think
今天我们来学习动态内存管理
一起来think
为什么要有动态内存分配
举例:如果一个班级的学生的人数是36人,但是我事先不知道具体由多少人,所以提供了100个空间,所以导致空间开多了,
那么如果是数组的话,就无法修改了,但是我们需要在开辟空间开多了的情况下,修改空间大小,这个需求数组无法满足(变长数组也是无法满足的),所以动态内存分配就来了。
malloc和free
malloc
void* malloc (size_t size);
功能: malloc 用于在堆区申请一块指定大小的连续可用内存,并返回这块内存起始位置的地址。
参数: size 表示需要申请的内存大小,单位为字节。
返回值:
- 申请成功时,返回指向这块内存起始位置的指针;
- 申请失败时(如可用内存不足),返回 NULL,因此使用 malloc 后通常需要检查返回值是否为空;
- 返回值类型为 void*。这是因为 malloc 只负责分配原始内存,而不知道调用者打算将这块内存解释成什么类型的数据(如 int、double、结构体等),因此返回通用指针 void*,具体的数据类型由使用者在后续使用时决定。
要注意的是:
当 size 为 0 时,malloc 的行为由实现定义,不同编译器或运行库可能返回 NULL,也可能返回一个不可解引用的非空指针。
实际使用:int* p = (int*)malloc(10 * sizeof(int));
free
void free (void* ptr);
功能: free 用于释放之前通过动态内存分配函数(如 malloc、calloc、realloc)申请的堆内存,使这块内存重新变为可用状态。
参数: ptr 是指向待释放内存块的指针,必须是动态内存分配函数返回的地址。
返回值 :
free 没有返回值,其返回类型为 void。
要注意的是:
- ptr 必须指向动态分配的内存,否则调用 free 的行为未定义。
- 同一块动态内存只能释放一次,重复释放(double free)会导致未定义行为。
- 当 ptr 为 NULL 时,free 什么也不做,因此无需在调用前专门判断是否为 NULL。
- free 释放的是指针所指向的内存空间,而不是指针变量本身。释放后,指针仍然存在,只是变成了悬空指针(wild pointer),通常应立即将其置为 NULL。
- 释放成功后,程序不能再通过该指针访问原来的内存,否则会产生未定义行为。
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num];//变长数组
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
else
{
perror("malloc");
return 1;
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}
变长数组和malloc开辟的函数在现阶段好像功能是一样的,都是在运行期间才确定大小,但是在使用realloc的时候就会发现后者可以继续改变大小,而前者不可以,这就动态开辟灵活的地方。
calloc和realloc
calloc
void* calloc (size_t num, size_t size);
功能: calloc 用于在堆区动态申请内存。它会为 num 个大小为 size 字节的元素分配一块连续的内存空间,并在返回之前将这块空间的所有字节初始化为 0。
参数:
- num:需要分配的元素个数;
- size:每个元素的大小,单位为字节。
返回值:
- 分配成功时,返回指向所申请内存块起始位置的指针;
- 分配失败时,返回 NULL;
- 返回类型为 void*,因为 calloc 只负责分配内存,而不知道这块内存将被解释为哪种数据类型,具体类型由使用者决定。
特点:
- 实际申请的内存大小为 num × size 字节;
- calloc 会自动将申请到的内存全部初始化为 0;
- calloc 申请的内存同样需要使用 free 释放。
与 malloc 的区别:
- malloc(size) 只负责分配内存,不会对内存内容进行初始化;
- calloc(num, size) 不仅分配 num × size 字节的内存,还会将这块内存的所有字节初始化为 0。
- 可以认为calloc==malloc+memset。
realloc
void* realloc(void* ptr, size_t size);
功能:
-
realloc 函数用于调整动态内存空间的大小,使动态内存管理更加灵活。
-
在实际开发中,最初申请的空间可能过小,也可能过大。为了更合理地使用内存,可以使用 realloc 对已经申请的内存进行扩容或缩容。
参数:
- ptr:指向之前动态分配内存块的起始地址。
- 如果 ptr == NULL,则 realloc 的行为与 malloc(size) 类似,都可以认为是申请一块内存。
- size:调整后的内存大小,单位为字节。
- 当 size == 0 时,与
malloc(0)、calloc(0, size)一样属于特殊情况,其具体行为由实现决定(实现定义)。但由于 realloc 还涉及原内存块的处理,因此通常不建议依赖 realloc(ptr, 0) 的具体行为,需要释放内存时应直接使用 free(ptr)。
- 当 size == 0 时,与
返回值:
- 调整成功:返回调整后内存块的起始地址(类型为 void*)。
- 调整失败:返回 NULL,且原来的内存块仍然保持有效,不会被释放。
当 realloc 调整内存大小时,通常有两种情况:
情况1:原空间后面有足够的连续空闲内存
如果原内存块后方存在足够大的空闲区域,则直接在原空间基础上扩展(或缩小)内存。
特点 :原有数据保持不变;
不需要搬移数据;
返回地址与原地址相同。
原空间 :
已使用 \]\[ 空闲空间
扩容后 :
已使用 \]\[ 新增空间
情况2:原空间后面没有足够的连续空闲内存
此时无法直接扩容,realloc 会:
在堆区寻找一块满足要求的新空间;
将原空间中的数据复制到新空间;
释放原来的内存块;
返回新空间的起始地址。
特点:原有数据会被保留;
返回地址发生变化;
应使用临时指针接收返回值,在成功后再更新原指针,否则可能丢失原内存地址。
旧空间到新空间的步骤:
- 申请新空间
- 复制数据
- 释放旧空间
注意点:
由于 realloc 可能返回新的地址,因此不要直接将返回值赋给原指针:
ptr = realloc(ptr, new_size);// 不推荐如果扩容失败,返回 NULL,原地址会丢失,导致内存泄漏。
更安全的写法:
c
int* tmp = realloc(ptr, new_size);
if (tmp != NULL)
{
ptr = tmp;
}
这样即使 realloc 失败,原来的内存空间仍然能够通过 ptr 访问和释放。
使用动态内存时的注意点
- 对NULL指针的解引用操作
c
void test()
{
int *p = (int *)malloc(INT_MAX);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
p = NULL;
}
改法:在动态内存申请完成以后加上判断即可。
c
void test()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL)
{
perror("malloc");
exit(1);
}
*p = 20;
free(p);
p = NULL;
}
- 对动态开辟空间的越界访问
c
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(p == NULL)
{
exit(1);
}
for(i = 0; i <= 10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
- 对非动态开辟内存使用free释放
c
void test()
{
int a = 10;
int *p = &a;
free(p);
}
- 使用free释放一块动态开辟内存的一部分
c
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
- 对同一块动态内存多次释放
c
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
- 动态开辟内存忘记释放(内存泄漏)
c
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
return 0;
}
动态开辟的内存最终会被释放,释放方式通常有两种:
- 程序员主动调用 free 释放;
- 程序结束时,由操作系统回收该进程占用的全部内存资源。
-
对于一些运行时间较短的程序,即使忘记释放动态内存,程序结束后操作系统也会将其回收。但在实际开发中,很多程序(例如服务器程序、数据库程序等)需要长期甚至持续运行,不会轻易退出。
-
因此,当一块动态申请的内存不再使用时,应及时调用 free 将其释放。否则,这部分内存会一直被当前进程占用,无法被程序中的其他功能再次利用,从而造成内存泄漏(Memory Leak)。随着泄漏的内存越来越多,程序可用内存会不断减少,严重时甚至可能导致程序运行缓慢、申请内存失败,甚至崩溃。
柔性数组
你可能从没听过柔性数组(flexible array),但它是真实存在的语法特性。
在 C99 标准里,结构体的最后一个成员允许定义长度未知的数组,这种数组就被称作柔性数组成员。
示例写法一:
c
struct st_type
{
int i;
int a[0];//柔性数组成员
};
部分编译器使用 a0 会编译报错,遇到这种情况可以换另一种写法:
c
struct st_type
{
int i;
int a[];//柔性数组成员
};
柔性数组的特点
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
要注意的是如果直接用类型创建变量,创建在栈区的话,柔性数组是无法使用的,只能使用除了柔性数组外的其他成员变量。
c
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4
return 0;
}
柔性数组的使用
c
//代码1
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}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;
}
这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

柔性数组的优势
前面介绍的柔性数组结构(代码1)与下面这种"结构体 + 指针成员"的写法(代码2)都能够实现动态存储数据的功能。
c
//代码2
#include <stdio.h>
#include <stdlib.h>
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(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:
c
type_a* p = malloc(sizeof(type_a));
p->p_a = malloc(100 * sizeof(int));
实际上进行了两次动态内存申请:
一次为结构体对象申请空间;
一次为数据成员申请空间。
因此释放时也必须对应地进行两次释放:
free(p->p_a);
free(p);

要注意的是如果该结构体是在某个库函数内部创建并返回给用户的,用户可能只知道释放结构体本身:
free(p);
而忽略释放 p->p_a 指向的内存,从而造成内存泄漏。
而柔性数组通常采用一次性分配:
struct S* p = malloc(sizeof(struct S) + n * sizeof(int));柔性数组是结构体的一个成员,柔性数组元素与其他结构体成员共同存储在同一块连续内存空间中。因此整个结构体对象只需一次动态内存申请,也只需一次 free 即可完成释放,因此释放时只需:
free(p);即可释放全部空间,降低了内存泄漏的风险,也使内存管理更加简单。
2. 有利于提高访问效率
-
柔性数组的数据与其他结构体成员存放在同一块连续内存中。
-
而指针方案通常需要分别申请内存:
结构体 -------> 数据区
两块内存的位置可能相距较远。
连续存储具有以下优点:
- 更符合 CPU 缓存(Cache)的工作方式;
- 减少缓存未命中的概率;
- 提高数据访问效率;
- 减少内存碎片的产生。
因此,在需要频繁访问大量数据时,柔性数组通常具有更好的性能表现。
柔性数组优点总结
柔性数组相比于"结构体 + 指针成员"的方案,主要有两个优点:
- 一次申请、一次释放,内存管理更加简单。
- 数据连续存储,缓存友好,访问效率更高,同时能减少内存碎片。
因此,在需要存储变长数据且数据生命周期与结构体一致时,柔性数组往往是更优的选择。
总结
谢谢观看!