C语言进阶篇(动态内存管理)
一.动态内存函数
1.1 malloc
作用:开辟动态内存空间
・如果开辟成功,则返回一个指向开辟好空间的指针。
・如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
头文件:#include<stdlib.h>
格式:void*malloc(size_t size)
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int main()
{
int arr[10] = {0};//两者都申请了40个字节
int*p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));//当malloc开辟失败时报错
}
int i;
for(i=0;i<10;i++)
{
*(p+i)=i;
}
for(i=0;i<10;i++)
{
printf("%d",*(p+i));
}
return 0;
}
//开辟失败的可能原因:开辟内存过大,没有足够的内存
不用assert()函数的原因:
assert 是调试断言,作用:
- 表达式为假 → 直接终止程序
- 只在 Debug 模式生效
- Release 模式会被直接删掉
它的目的是:开发阶段快速发现 bug,不是处理运行时错误。

malloc的好处:堆区可以调整申请的空间,而栈区的空间是不能调整的
拓展:变长数组(内存大小还是不会改变)x
int n = 5;
scanf("%d", &n);
int arr[n]; // ✅ 变长数组,C99 支持
而普通数组
int n = 5;
int arr[10];
int arr[n]; // 老式C语言不行
1.2 free
作用:释放动态分配的内存空间,必须配合 malloc/calloc/realloc 使用。
使用free()函数的原因:
- 归还内存给系统你用
malloc申请的堆内存,不会自动释放,必须手动调用free归还。 - 防止内存泄漏不调用 free → 内存一直被占用 → 程序运行越久,占用内存越多 → 最后崩溃。
- 让指针可以重复使用释放后,这块内存就不再属于你的程序了。
使用实例:
free(p);
p=NULL;
后面要加上p=NULL的原因:free(p)后将40个字节的还给操作空间,但p的地址还没发生变化,p还能找到这个内存区域,可是这个内存区域已经还给操作系统,这样访问p的话,p就成了野指针,不安全,所以还要加上p=NULL。

上图代码中的free函数什么事都不干。
1.3 calloc
头文件:#include<stdlib.h>
格式:void*calloc(size_t num,size_t size)
例:
int main()
{
int*p=(int*)calloc(10,sizeof(int));
if(p==NULL)
{
printf("%s",strerror(errno));
return 1;
}
int i;
for(i=0;i<10;i++)
{
printf("%d",*(p+i)));
}
return 0;
}
该函数的返回值就是开辟空间的起始地址,并且在返回之前将这个空间初始化成0
1.4 malloc和calloc的选择
- 当我们想初始化时,用calloc
- 不想初始化时,用malloc
可理解为:calloc=malloc+memset
1.5 realloc
作用:可对动态开辟内存大小进行调整
格式:void*realloc(void*ptr,size_t size)
ptr是要调整空间的起始地址
size是调整之后的新大小
realloc函数作用原理:
当我们想扩容时会碰见两种情况
- 情况一:开辟的动态内存如果再往后开辟40个字节会覆盖后面原有的数据

这么做是不可取的,所以realloc函数会在堆区找一个空的内存开辟80个字节,然后将原有的40个字节内存拷贝过来,释放旧的空间,然后返回新的地址
- 情况二:开辟的动态内存后面还有多余的空间能容纳新增的内存

则realloc还会返回旧的地址
使用实例:
int main()
{
int* p = (int*)malloc(40);
if (NULL == p)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
int*prt=(int*)realloc(p,80);
(这里不直接用p=realloc(p,80)是因为防止设置的内存太大,找不到空区域开辟新的内存,导致p变为空指针了)
if(prt!=NULL)
{
p=prt;
}
for(i=0;i<10;i++)
{
printf("%d ",*(p+i));
}
free(p);
p=NULL;
return 0;
}
realloc(NULL,40)和malloc(40)是等效的
二.常见的动态内存错误
2.1 对NULL指针的解引用操作
错误示例:
int*p=(int*)malloc(40);
*p=20;
free(p);
若p为NULL,则此时就会导致问题,所以我们在给*p赋值之前,应加以判断,即:
if(p==NULL)
{
return 1;
}
2.2 对动态开辟空间的越界访问
错误示例:
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
p[i] = i; //此处进行了越界访问
}
free(p);
p = NULL;
只开辟了40个字节的动态内存,却使用了44个字节的空间,这属于越界访问
2.3 对非动态开辟内存使用free释放
错误示例:
int a=10;
int *p=&a;
free(p);
free只能释放动态开辟的内存,是无法释放栈区内存的
2.4使用free释放一块动态开辟内存的一部分
错误示例:
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
这里的p++导致p的起始位置发生变化,free(p)就会释放从p的最终地址到开辟内存结尾的空间,但这种情况是不允许的,会导致程序崩溃,所以应该改为如下代码:
for (i = 0; i < 5; i++)
{
*(p+i) = i;
}
2.5对同一块动态内存多次释放
错误示例:
int*p=(int*)malloc(40)
...
free(p);
...
free(p);
对同一块空间的多次释放会导致程序崩溃,并且为了防止p为野指针,在free释放后,应该将p设置为NULL,即:
free(p);
p=NULL;
free(p);
这样,即使再次释放p也不会发生任何事情
2.6动态开辟内存忘记释放(内存泄漏)
错误示例一:
void test()
{
int* p = (int*)malloc(100);
// ...
int flag = 0;
scanf("%d", &flag);
if (flag == 5)
return;
free(p);
p = NULL;
}
int main()
{
test();
// ...(main函数其他逻辑)
return 0;
}
在执行free函数之前就已经已经返回,导致开辟的动态内存还没来得及释放,那么给变量p的100个字节大小的空间就再也无法找到了,造成了内存泄漏
错误示例二:
int* test()
{
int* p = (int*)malloc(100);
if (p == NULL)
{
return p;
}
return p;
}
int main()
{
// 调用 test(),接收其返回的动态内存指针,存入 ret
int* ret = test();
// 忘记释放了
// 此处没有执行 free(ret); 释放内存
return 0;
}
这也会造成内存泄漏
三.常见的错误代码
3.1题目一
错误代码:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);//p出了函数后就被销毁了,发生了内存泄漏
strcpy(str, "hello world");从strcpy的内部逻辑可知,当str是NULL时,解引用时会崩溃
printf(str);
}
int main()
{
Test();
return 0;
}
补充:上述代码将NULL传给p后,p=(char*)malloc(100)会使p发生改变,为开辟空间的起始地址
而开辟内存后,用p是否为NULL来判断开辟成功与否,是因为若开辟失败(int*)malloc(100)会返回NULL,并不是p为NULL就无法开辟内存
改正后的代码:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
// 打印 str 指向的字符串
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
对printf()函数进行解释:
printf 函数原型:
int printf(const char *format, ...);
printf 的第一个参数,本身就要求是一个「字符串地址」。
str 本身就是一个 char* 类型,完全符合 printf 第一个参数的要求。
但平时不推荐这样写,因为不安全、不规范
3.2题目二
错误代码:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
问题:
- 当
GetMemory函数执行结束后,栈帧会被销毁,p指向的内存就被释放了。 - 此时
Test函数里的str得到的是一个野指针,指向了已经失效的栈内存。 - 后续
printf(str)会导致未定义行为(可能乱码、崩溃,或者偶尔输出 “hello world”)。
注:函数返回值不是绝对不能与局部变量有关,而是有严格限制
- 完全安全、正确的用法:
原理:返回时会拷贝一份值给调用者,局部变量销毁也不影响。int add(int a, int b) { int sum = a + b; // sum 是局部变量 return sum; // ✅ 返回值,没问题 } - 绝对禁止:返回局部变量的地址(指针)
char *GetMemory(void)
{
char p[] = "hello"; // 栈上局部数组
return p; // ❌ 返回 p 的地址(指针)
}
原理:
- 局部变量(数组、变量)存在栈内存
- 函数一结束,栈内存立刻被系统回收
- 你返回的指针指向的是已经被回收的无效内存(野指针)
- 后续使用这个指针 = 程序崩溃 / 乱码 / 未知行
3.3题目三
错误代码:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
当free(str)后,str所存放的hello的地址已经还给操作系统了,而且str不为NULL,就进入了if语句,
此时将"world"强行放进str中就形成了非法访问,str为野指针。
3.4拓展:内存区域的分布


四.柔性数组
定义:结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例:
struct st_type
{
int i;
int a[];//柔性数组成员
}
也可写成int a[0],这里的0就表示不知道数组里有几个成员
4.1柔性数组的特点
• 结构中的柔性数组成员前面必须至少一个其他成员
即上面代码中的 int i
• sizeof 返回的这种结构大小不包括柔性数组的内存。
struct s
{ int n;
int arr[];
};
int main()
{ int sz=sizeof(struct s);
printf("%d",sz);//4
return 0;
}
sizeof只计算了int n的内存,没有计算int arr[]的内存
• 包含柔性数组成员的结构用 malloc() 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
错误用法:
struct s
{ int n;
int arr[];
};
int main()
{ struct s m;
return 0;
}
当结构体存在柔性数组时,不能这样创建变量
正确用法一:
struct s
{ int n;
int arr[];
};
int main()
{
struct s* ps=(struct s*)malloc(sizeof(struct s)+40);
if(ps==NULL)
{ return 1;}
ps->n=100;
int i=0;
for(i=0;i<10;i++)
{
ps->arr[i]=i;
}
struct s* ptr=(struct s*)realloc(ps,sizeof(struct s)+80);
if(ptr!=NULL)
{
ps=ptr;
ptr=NULL;//这里不释放ptr是因为ps和ptr指向的是同一块空间,如果释放了ptr,那么ps也会被释放。
}
free(ps);
ps=NULL;
return 0;
}
正确用法二:
struct s
{ int n;
int *arr;
};
int main()
{
struct s* ps=(struct s*)malloc(sizeof(struct s));//这里是为了将结构体的成员都放在堆上
if(ps==NULL)
{
return 1;
}
ps->n=100;
ps->arr=(int*)malloc(40);//可理解为一个数组,arr为数组名
if(ps->arr==NULL)
{
return 1;
}
int i=0;
for(i=0;i<10;i++)
{
ps->arr[i]=i;//
}
struct s* ptr=(struct s*)realloc(ps,sizeof(struct s));
if(ptr!=NULL)
{
ps=ptr;
ptr=NULL;//这里不释放ptr是因为ps和ptr指向的是同一块空间,如果释放了ptr,那么ps也会被释放。
}
free(ps);
ps=NULL;
return 0;
}
法一和法二ps->arr的区别:
法二 法一

法二是通过int*arr来达到与int arr[]有相同的作用
对方法二中ps->arr[i]=i;进行解析:
在C语言中有一个关键的等价关系:a[i] 等价于 *(a + i)//这里的*是解引用符号
ps->arr指的就是结构体中的int*arr
ps->arr[i]是值,当arr是int*类型时,arr[i]是一个int类型的值!ps->arr + i是地址
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)