1.为什么要存在动态内存管理
int math[30]
像这种内存申请方式,一旦申请好空间,大小就无法调整
比如我只用26,浪费了16个字节
---》
能不能让程序员自己来动态的申请空间!
c/c++能
c语言就涉及到4个函数
malloc
free
calloc
realloc
2.malloc和free
2.1molloc
包含头文件《stdlib.h》
动态内存开辟函数
void* malloc(size_t size);
巷内村申请一块连续的空间,返回空间的起始地址
如果开辟成功,则返回一个只想开辟好空间的指针
如果开辟失败,则返回一个NULL的指针
返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己决定
如果参数是size=0,malloc的行为是标准是未定义的 ,取决于编译器

cpp#include<stdlib.h> int main() { int* p = (int*)malloc(20); if (p == NULL) { perror("malloc"); return 1; } int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i + 1; } return 0; }
这段代码是一个C 语言程序的主函数 ,核心功能是尝试向系统申请堆内存并检查申请是否成功,下面逐行详细解释:
int main()
这是 C 程序的入口函数 ,程序从这里开始执行;int表示主函数执行后会返回一个整数(通常return 0代表程序正常结束,非 0 代表异常)。
int* p = (int*)malloc(20);
-
malloc(20):malloc是 C 标准库函数(需包含头文件<stdlib.h>),作用是向系统的堆区申请 20 字节的连续内存空间。-
如果申请成功,
malloc返回这段内存的起始地址 (类型为void*,无类型指针); -
如果申请失败(比如内存不足),返回
NULL(空指针)。
-
-
(int*):将malloc返回的void*强制转换为int*类型(指向int的指针),因为 C 语言不允许直接将void*赋值给其他类型指针(部分编译器可能兼容,但显式转换更规范)。 -
int* p:定义一个指向int的指针变量p,用来存储malloc返回的内存地址。
if (p == NULL)
检查内存申请是否成功:
-
如果
p等于NULL,说明malloc申请内存失败(比如系统剩余内存不足); -
如果
p不等于NULL,说明内存申请成功,p指向申请到的 20 字节内存的起始位置。
perror("malloc"); return 1;
-
perror("malloc"):是 C 标准库函数(需包含头文件<stdio.h>),作用是打印错误原因 。它会先输出括号里的字符串(这里是malloc),然后自动补充系统的错误描述(比如内存不足时会输出malloc: Out of memory)。 -
return 1:表示程序异常退出(约定俗成用非 0 值表示执行失败)。
return 0;
如果内存申请成功(p != NULL),程序执行到这里,返回0表示正常结束。
补充说明
- 这段代码没有释放申请的内存(需要用
free(p);),会导致内存泄漏(程序结束后系统会自动回收,但良好的习惯是手动释放)。 - 实际编译运行时,需要包含头文件:
#include <stdio.h>、#include <stdlib.h>。
2.2free
专门用来做动态内存的释放,把空间的使用权限还给操作系统
void free(void* ptr);
void*指的是可以接受任意类型的地址
传空间的起始地址
如果传NULL,函数什么事不做
cpp
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
free(p);
p=NULL;
return 0;
}
所以free完了之后,p所指向的空间还给了操作系统,p但还指向那块空间,p成野指针
所以加一个p=NULL;避免成为野指针
cpp
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = p + i;
p++;
}
free(p);
p=NULL;
return 0;
}
至于为什么不采用这种方式
因为p++;
后面free(p)会出现bug,p已经不是其实地址
3.calloc和 realloc
3.1calloc也用来进行动态内存分配
void* callloc(size_t num,size_t size);
功能就是为num大小为size的元素开辟空间,并且把空间的每个字节初始化为0(和malloc的区别)

3.2realloc
让内存动态管理更加灵活活
void* realloc(void* ptr,size_t size);
ptr是要调整的内存地址
size是调整后的大小
返回值为调整之后没存的起始地址
cpp
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror(malloc);
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
*(p + i) = i + 1;
}
realloc(p, 40);
return 0;
}
情况一二:

弱势请款2:
- 在堆区找一块新的空间,并且满足新的大小要求
- 在原来的空间的数据copy一份到新的空间
- 释放旧的空间
- 返回新的内存空间的起始地址
情况三:空间调整失败返回空指针
所以需要指针来接realloc
cpp
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror(malloc);
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
*(p + i) = i + 1;
}
p=(int*)realloc(p, 40);
return 0;
}
如果使用p来接受并不太靠谱
因为万一空间调整失败返回的是NULL;
p之前还存着20个空间的起始地址就是没了
int* ptr
cpp
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror(malloc);
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
*(p + i) = i + 1;
}
int* ptr=(int*)realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
}
else
{
perror("realloc");
}
//调整失败后p还是旧的空间
return 0;
}
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror(malloc);
return 1;
}
int i = 0;
for (i; i < 5; i++)
{
*(p + i) = i + 1;
}
int* ptr=(int*)realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
int i = 0;
for (i = 5; i < 10; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
free(p);
p = NULL;
}
else
{
perror("realloc");
free(p);
p = NULL;
}
//调整失败后p还是旧的空间
return 0;
}
完整代码
realloc可以完成和malloc一样的功能
cpp
int main()
{
realloc(NULL, 20);
return 0;
}
4.常见动态内存管理的错误
4.1对NULl指针的解引用操作
cpp
int main()
{
int* p = (int*)malloc(INT_MAX);
*p = 20;
return 0;
}
判断p是否为空指针
cpp
int main()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL)
{
perror("malloc");
return 1;
}
*p = 20;
return 0;
}
也可以用assert(p)
4.2对动态开劈空间的越界访问
cpp
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);
}
i<=10,11个了放生月结访问
4.3对⾮动态开辟内存使⽤free释放
cpp
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
4.4 使⽤free释放⼀块动态开辟内存的⼀部分
cpp
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
4.5 对同⼀块动态内存多次释放
cpp
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
cpp
void test()
{
int *p = (int *)malloc(100);
free(p);
p=NULL;
free(p);//重复释放
}
p=NULL可以避免
4.6 动态开辟内存忘记释放(内存泄漏)
cpp
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
因为p的创建时临时变量,出了void后不释放,后面在main里也释放不了,造成内存泄露
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
内存是一种资源,申请后不用记得回收
其实mallloc和calloc/realloc申请的内存,如果不想使用的话,可以使用free释放
如果没有使用free来释放,当程序运行结束后,也会有操作系统回收
但是如果程序一直嗯不退出,空间就会一直站着
尽量做到谁(函数)申请的空间谁释放,如果不能释放,要告诉使用的人记得释放
那么要做到malloc/free成对出现
5.动态内存经典笔试题的分析
5.1
cpp
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
//P是形参,出来之后p销毁,空间还给操作系统
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
//1.内存泄漏
//2.程序崩溃
1.str传值调用给getmemory,用一个char*接受,则p=NULL
2.malloc开辟100个字节的空间,p只想该控件的首字节的地址
3.但是出了void后p销毁,申请的100字节空间还在,但只有p知道该控件的位置
4.str还是NULL
5.strcpy肯定要对str解引用,NULL解引用,程序崩溃
想要修改的话
方式1
cpp
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
//P是形参,出来之后p销毁,空间还给操作系统
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
//1.内存泄漏
//2.程序崩溃
str传地址调用;
使用char**二级指针来接
则可以改变str;
后free释放
图示:
原来:p是指向str的

后来:str换成100的地址
方式2
cpp
char* GetMemory(char* p)
{
*p = (char*)malloc(100);
return p;
}
//P是形参,出来之后p销毁,空间还给操作系统
void Test(void)
{
char* str = NULL;
str=GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
//1.内存泄漏
//2.程序崩溃
返回p,同时getmemory也可以不传参。
cpp
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
//P是形参,出来之后p销毁,空间还给操作系统
void Test(void)
{
char* str = NULL;
str=GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
5.2
cpp
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
打印不出也可能打印出
因为p是函数的局部变量,出函数后空间还给操作系统,返回的p是野指针,使用str指针取放位p数组属于非法访问
cpp
int* test()
{
int n = 10;
return &n;
}
int main()
{
int* p = test();
return 0;
}
也是返回临时变量的地址的问题,
但局部变量的内容是可以传的
5.3
cpp
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str=MULL;
}
int main()
{
Test();
return 0;
}
代码一气呵成,唯一的问题就是忘记free,加上就行
5.4
cpp
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
6.柔性数组
1.建立在结构体中,最后一个成员
2.最后一个成员是数组,数组没有指定大小
这个数组才是柔性数组
cpp
struct s
{
char c;
int arr[];
};
有一些编译器支持这种
cpp
struct s
{
char c;
int arr[0];
};
放0下去也是没知名大小
特点
柔性数组前面得有至少一个成员;
sizeof返回的这种结构大小不包括柔性数组的内存,因为其大小没指定
cpp
struct s
{
char c;
int arr[];
};
int main()
{
printf("%zd", sizeof(struct s));
return 0;
}
包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤
⼩,以适应柔性数组的预期⼤⼩。

后面的arr也可以用realloc进行柔性调整;
cpp
struct s
{
char c;
int arr[];
};
int main()
{
struct s* pf=(struct s*)malloc(sizeof(struct s) + 5 * sizeof(int));
if (pf = NULL)
{
perror(malloc);
return 1;
}
pf->c = 'a';
int i = 0;
for (i = 0; i < 5; i++)
{
pf->arr[i] = i;
}
//调整空间
struct s* ptr=(struct s*)realloc(pf, sizeof(struct s)+10 * sizeof(int));
if (ptr != NULL)
{
pf = ptr;
}
//释放空间
free(pf);
pf = NULL;
return 0;
}
return 0;
}
另外一种方案:
cpp
struct s
{
int n;
int* arr;
};
int main()
{
struct s* ps=(struct s*)malloc(sizeof(struct s));
if (ps == NULL)
{
perror(malloc);
return 1;
}
ps->arr =(int*) malloc(5 * sizeof(int));
if (ps->arr == NULL)
return 1;
//使用
ps->n = 100;
int i = 0;
for (i; i < 5; i++)
{
ps->arr[i] = i;
}
//调整数组的大小
int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
if (ptr != NULL)
{
ps->arr = ptr;
}
//使用
//释放
free(ps->arr);//先释放arr,因为先释放ps的话就找不到arr了
free(ps);
return 0;
}
显然第一种使用柔性数组的方案更加
第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤
⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能
指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返
回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你
跑不了要⽤做偏移量的加法来寻址)