动态内存管理
目录
1. 为什么要有动态内存分配
在 C/C++ 编程中,程序的内存主要分为以下几个区域:
-
栈区(stack):存放局部变量、函数参数等,由编译器自动分配和释放。
-
静态区(static):存放全局变量、静态变量,程序结束时释放。
-
堆区(heap):由程序员手动分配和释放,也就是动态内存分配的核心区域。
静态分配和自动分配的局限性:
-
大小固定,缺乏灵活性
数组长度必须在编译时确定(C99 之前),或者即便支持变长数组(VLA),其大小也受限于栈空间,无法在运行时随意调整。 -
生命周期受限于作用域
局部变量在函数返回后即销毁,全局变量则全程占用内存,不能按需创建和释放,造成内存浪费。 -
栈空间有限
栈空间通常只有几 MB,分配大块内存(如大数组、图像数据)极易导致栈溢出。 -
程序真正需要的数据量往往运行时才能确定
例如读取用户输入、网络数据流、文件内容、动态数据结构(链表、树、图)等,大小事先未知。
动态内存分配正是为了解决上述问题:
-
在堆上申请任意大小的内存块,
-
生命周期由程序员控制,按需分配,及时释放,
-
可随时调整大小(通过
realloc), -
适合构建灵活的数据结构。
C语言标准库提供四个动态内存管理函数,声明在<stdlib.h>中
2. malloc和free
2.1 malloc - 分配未初始化的内存
void* malloc(size_t size);
| malloc | |
|
功能 |
向内存的堆区申请一块连续可用的空间,并指向这块空间的起始地址 |
| 参数 | size:要分配的内存块的字节数 |
| 返回值 |
开辟成功返回这块空间的起始地址 开辟失败返回NULL指针,所以malloc的返回值一定要检查 |
| 注意事项 |
1.内存中的内容是未初始化的(随机值) 2.若size为0,malloc的行为是标准未定义的,取决于编译器 |
int* p = (int* )malloc(20);//在堆区上申请20个字节的空间
if (p == NULL)
{
perror("use malloc:");
return 1;
}
2.2 free - 释放动态内存
void free(void* ptr);
| free | |
| 功能 | 释放之前通过动态内存分配函数(如malloc,calloc,realloc)开辟的空间 |
| 参数 |
ptr:指向要释放的内存块的指针
|
| 注意事项 | 释放空间后应立即将指针置为NULL |
int main()
{
int* p = (int* )malloc(20);//在堆区上申请20个字节的空间
if (p == NULL)
{
perror("use malloc:");
return 1;
}
free(p);//释放内存
p = NULL;//置为空
return 0;
}
3. calloc和realloc
3.1 calloc - 分配并初始化为零的内存
void* calloc(size_t num,size_t size);
| calloc | |
|
功能 |
分配 num*size 字节的内存,并把每一位都初始化为0 |
| 参数 |
size:每个元素的大小 num:元素个数 |
| 返回值 |
开辟成功返回这块空间的起始地址 开辟失败返回NULL指针 |
calloc初始化为0:
malloc初始化为随机值:
calloc使用举例:
int main() { //使用calloc开辟空间 int* p = (int*)calloc(5, sizeof(int)); //检查 if (p == NULL) { perror("use calloc"); return 1; } //使用 int i = 0; for (i = 0; i < 5; i++) { p[i] = i + 1; } //释放 free(p); p = NULL; }
3.2 realloc - 调整已分配内存的大小
void* realloc(void* ptr, size_t new_size);
| realloc | |
| 功能 | 重新调整之前分配的内存块的大小,它可以在不丢失原有数据的情况下,扩大或缩小动态分配的内存块 |
| 参数 |
ptr:是要调整的内存空间的起始地址,如果ptr是NULL指针,realloc功能相当于malloc函数 new_size:调整之后新的大小,单位是字节 |
| 返回值 |
成功:返回一个指向重新分配的内存块的void*类型指针。这个指针可能与原来的指针不同 失败:如果内存重新分配失败,返回NULL,并且原来的内存块保持不变 |
| 注意 |
realloc开辟空间示有两种情况: 1.要拓展的内存在原来内存后直接追加空间,返回的地址还是旧地址 2.原来空间不够大,会在内存的堆区寻找新的满足要求的空间,返回新的起始地址,此时会: ▪ 寻找新的满足要求的空间 ▪ 将旧空间的数据拷贝到新空间,保证数据不会丢失 ▪ 释放旧空间,返回新空间的起始地址 |
//用realloc开辟新空间
int* p2 = (int*)realloc(p1, 10 * sizeof(int));//p1是原来空间的地址
if (p2 == NULL)
{
perror("realloc");
return 1;
}
else
{
p1 = p2;//p1的地址变为新空间地址,旧的空间会自动释放
p2 = NULL;
}
注意以下事项:
4. 常见的动态内存的错误
1 - 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(100*INT_MAX);
*p = 20;//p的值是NULL,就会有问题
free(p);
p = NULL;
}
2 - 对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
for (i = 0;i <= 10;i++)
{
p[i] = i + 1; //当i是10的时候越界访问
}
free(p);
p = NULL;
}
3 - 对非动态开辟内存使用free释放
int main()
{
int a = 10;
int* p = &a;
free(p);//err,free只能释放由 malloc,calloc,realloc 动态分配的内存
p = NULL;
return 0;
}
4 - 使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;//p本身发生了变化
free(p);//p不再指向动态内存的起始地址
}
5 - 对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
6 - 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int* )malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
//... 没有释放内存
while (1);
return 0;
}
注:动态申请的内存空间,应该用free释放,若果没用free释放,程序结束时,也会被操作系统回收
5. 题目练习
1 - 下面代码有什么问题
void GetMemory(char* p) { p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str);//str仍然是NULL } int main() { Test(); return 0; }解析:
1.GetMemory(str);无法改变str的值(p是形参)
2.p开辟空间后没释放
修改上述代码:
1.使用二级指针(传递指针的地址)
void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str); free(str); str = NULL; }2.通过返回值传递
char* GetMemory(char* p) { p = (char*)malloc(100); return p; } void Test(void) { char* str = NULL; str = GetMemory(str); strcpy(str, "hello world"); printf(str); free(str); str = NULL; }
总结:C语言函数传参永远传的是值的副本。想要修改函数外部的变量,必须传入该变量的地址(即一级指针修改变量,二级指针修改一级指针)。
2 - 下面代码有什么问题
char* GetMemory(void)
{
char p[] = "hello world";//返回了局部变量的地址,导致野指针
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();//str是野指针
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
这段代码的核心问题是返回了局部变量的地址(栈内存被收回)
char p[] = "hello world";栈内存是随函数生命周期自动管理的。当
GetMemory函数执行完毕并返回时,这块栈内存会被立即释放(销毁),系统会将这块区域标记为“可用”,随时可能被其他函数调用覆盖掉。
修改方法:
1.static修饰
char* GetMemory(void) { static char p[] = "hello world"; return p; }2.直接返回字符串字面量的指针
char* GetMemory(void) { char *p = "hello world"; // 常量字符串,指向静态区,而不是栈上的数组 return p; // 安全 }3.动态内存开辟
char* GetMemory(void) { char* p = (char*)malloc(100); if (p) strcpy(p, "hello world"); return p; // 堆内存不会随函数销毁,但需要手动 free } void Test(void) { char* str = GetMemory(); printf(str); free(str); } int main() { Test(); return 0; }
总结:不要返回指向“局部栈上数组”的指针。
3 - 下面代码有什么问题
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//释放了内存
if (str != NULL)//但是str本身的值没有改变,它仍然存储着那块已释放内存的地址,换言之,str变成了野指针
{
strcpy(str, "world");//对野指针的访问,非法访问内存
printf(str);
}
}
int main()
{
Test();
return 0;
}
解析:
1.
free(str)只是把堆内存归还给系统,但指针变量str本身的值(那块内存的地址)没有被改变。此时str变成了野指针(悬空指针)。修改:
free(str) 后立即加上str = NULL;
总结:释放 ≠ 清空数据
6.柔性数组
C99中,结构体中有多个成员的情况下,最后一个元素允许是未知大小的数组,这就叫“柔性数组”成员,如:
struct st_type
{
int i;
int a[0];//柔性数组成员
//也可以写成:
int a[];
};
特点:
- 结构体中不能只有一个成员
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
struct st_type
{
int i;
int a[];
};
int main()
{
printf("%d ", sizeof(struct st_type));//4
return 0;
}
柔性数组的使用:
typedef struct st_type
{
int i;
int arr[];//柔性数组
}type_a;
int main()
{
type_a* pa = (type_a*)malloc(sizeof(type_a) + 5 * sizeof(int));
if (pa == NULL)
{
perror("malloc");
return 1;
}
pa->i = 100;
for (int i = 0;i < 5;i++)
{
pa->arr[i] = i + 1;
}
free(pa);
pa = NULL;
return 0;
}
柔性数组的优势:
如果不用柔性数组,代码是这样写的:
typedef struct st_type { int i; int* arr; }type_b; int main() { type_b* pb = (type_b*)malloc(sizeof(type_b)); if (pb == NULL) { perror("malloc"); return 1; } pb->i = 100; pb->arr = (int*)malloc(20); if (pb->arr == NULL) { perror("malloc arr"); free(pb); // 内部失败也要释放结构体 return 1; } for (int i = 0;i < 5;i++) { pb->arr[i] = i + 1; } free(pb->arr);//释放时要释放两次 free(pb); pb = NULL; return 0; }总结:柔性数组(
int arr[]):必须作为结构体最后一个成员,且前面必须有其他成员,开辟空间时只用一次。普通指针成员(
int *arr):内存分成两块(结构体一块,数据一块),缓存性能差,且释放时必须手动先后释放两次。
7.总结C/C++中程序内存区域划分

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐






所有评论(0)