文章目录
[1. 为什么要有动态内存分配?](#1. 为什么要有动态内存分配?)
[2. malloc和free:动态内存的基础](#2. malloc和free:动态内存的基础)
[2.1 malloc函数](#2.1 malloc函数)
[2.2 free函数](#2.2 free函数)
[2.3 示例代码](#2.3 示例代码)
[3. calloc和realloc:更高级的动态内存管理](#3. calloc和realloc:更高级的动态内存管理)
[3.1 calloc函数](#3.1 calloc函数)
[3.2 realloc函数](#3.2 realloc函数)
[4. 常见的动态内存错误](#4. 常见的动态内存错误)
[4.1 对NULL指针的解引用](#4.1 对NULL指针的解引用)
[4.2 越界访问](#4.2 越界访问)
[4.3 释放非动态内存](#4.3 释放非动态内存)
[4.4 释放部分动态内存](#4.4 释放部分动态内存)
[4.5 重复释放](#4.5 重复释放)
[4.6 内存泄漏](#4.6 内存泄漏)
[5. 动态内存经典笔试题分析](#5. 动态内存经典笔试题分析)
[5.1 题目1:传值问题](#5.1 题目1:传值问题)
[5.2 题目2:返回局部变量地址](#5.2 题目2:返回局部变量地址)
[5.3 题目3:正确传址](#5.3 题目3:正确传址)
[5.4 题目4:释放后使用](#5.4 题目4:释放后使用)
[6. 柔性数组:动态结构体成员](#6. 柔性数组:动态结构体成员)
[6.1 什么是柔性数组?](#6.1 什么是柔性数组?)
[6.2 柔性数组的使用](#6.2 柔性数组的使用)
[6.3 柔性数组的优势](#6.3 柔性数组的优势)
[7. C/C++程序内存区域划分](#7. C/C++程序内存区域划分)
引言
在C语言编程中,动态内存管理是核心技能之一。与静态内存分配相比,动态内存分配提供了更大的灵活性,允许程序在运行时根据需要申请和释放内存。本文将深入探讨动态内存分配的原理、函数使用、常见错误以及高级特性,帮助你全面掌握C语言动态内存管理。
1. 为什么要有动态内存分配?
静态内存分配的局限性
我们已经掌握的静态内存开辟方式有:
cpp
int val = 20; // 栈空间开辟4字节
char arr[10] = {0}; // 栈空间开辟10字节连续空间
这些方式存在两个主要问题:
-
空间大小固定:编译时就必须确定大小
-
无法调整:数组一旦声明,大小无法改变
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知 道,那数组的编译时开辟空间的方式就不能满足了。 C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。
动态内存分配的优势
动态内存分配允许程序在运行时根据需求申请内存,提供了以下优势:
-
灵活性:内存大小可根据需要调整
-
效率:避免预先分配过多或过少内存
-
生命周期可控:手动管理内存的生命周期
2. malloc和free:动态内存的基础
2.1 malloc函数
cpp
void* malloc(size_t size);
功能:向内存申请一块连续可用的空间,返回指向该空间的指针
特性:
-
成功:返回指向开辟空间的指针
-
失败:返回NULL指针,必须检查返回值
-
返回类型为
void*,需要类型转换 -
参数size为0时,行为未定义
2.2 free函数
cpp
void free(void* ptr);
功能:释放动态开辟的内存
注意事项:
-
只能释放动态开辟的内存
-
传递NULL指针时函数无操作
-
释放后指针应设为NULL,避免野指针
malloc和free都声明在 stdlib.h 头文件中。
2.3 示例代码
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
int num;
scanf("%d", &num);
int* p = (int*)malloc(num * sizeof(int));
if (p == NULL)
{
perror(malloc);
return 1;
}
for (int i = 0;i < num;i++)
{
*(p+i) = i;
printf("%d ", *(p+i));
}
free(p);
p = NULL;
return 0;
}
3. calloc和realloc:更高级的动态内存管理
3.1 calloc函数
cpp
void* calloc(size_t num, size_t size);
功能:为num个大小为size的元素开辟空间,并将每个字节初始化为0
与malloc的区别:
-
calloc会自动初始化为0
-
参数形式不同(元素个数和元素大小)
cpp
#include <stdio.h>
#include <stdlib.h>
int main() {
int* p = (int*)calloc(10, sizeof(int));
if (p != NULL) {
for (int i = 0; i < 10; i++) {
printf("%d ", p[i]); // 全部输出0
}
}
free(p);
return 0;
}
3.2 realloc函数
cpp
void* realloc(void* ptr, size_t size);
功能:调整动态开辟的内存大小
两种情况:
-
原地扩容:原空间后有足够空间,直接追加
-
异地扩容:原空间后空间不足,在堆中另找连续空间,数据被复制到新空间
使用注意事项:
-
不要直接将realloc返回值赋给原指针
-
应使用临时指针接收返回值,检查非NULL后再赋给原指针
cpp
#include<stdio.h>
#include<stdlib.h>
int main()
{
int num = 5;
int* p = (int*)calloc(num, sizeof(int));
for (int i = 0;i < num;i++)
{
printf("%d ", *(p + i));
}
printf("\n");
int* pr=(int*)realloc(p, 10 * sizeof(int));
if (pr == NULL)
{
perror(realloc);
return 1;
}
p = pr;
for (int i = 0;i < 10;i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
free(p);
p = NULL;
pr = NULL;
return 0;
}
4. 常见的动态内存错误
4.1 对NULL指针的解引用
cpp
void test() {
int* p = (int*)malloc(INT_MAX/4); // 可能失败返回NULL
*p = 20; // 如果p为NULL,此处崩溃
free(p);
}
4.2 越界访问
cpp
void test() {
int* p = (int*)malloc(10 * sizeof(int));
for (int i = 0; i <= 10; i++) { // i=10时越界
p[i] = i;
}
free(p);
}
4.3 释放非动态内存
cpp
void test() {
int a = 10;
int* p = &a;
free(p); // 错误:p指向栈内存
}
4.4 释放部分动态内存
cpp
void test() {
int* p = (int*)malloc(100 * sizeof(int));
p++; // p不再指向起始位置
free(p); // 错误:只释放部分内存
}
4.5 重复释放
cpp
void test() {
int* p = (int*)malloc(100 * sizeof(int));
free(p);
free(p); // 错误:重复释放
}
4.6 内存泄漏
cpp
void test() {
int* p = (int*)malloc(100 * sizeof(int));
*p = 20;
// 忘记free(p);
}
int main() {
test(); // 内存泄漏
while(1);
}
5. 动态内存经典笔试题分析
5.1 题目1:传值问题
cpp
void GetMemory(char* p) {
p = (char*)malloc(100);
}
void Test(void) {
char* str = NULL;
GetMemory(str); // str仍为NULL
strcpy(str, "hello"); // 崩溃:解引用NULL指针
printf(str);
}
问题:GetMemory参数为指针的副本,修改不影响原指针str,因为p出了函数之后,就会自动进行销毁,str仍然是空指针。
5.2 题目2:返回局部变量地址
cpp
char* GetMemory(void) {
char p[] = "hello world";
return p; // 返回局部数组地址,函数结束栈帧销毁
}
void Test(void) {
char* str = NULL;
str = GetMemory(); // str指向已释放的栈内存
printf(str); // 输出不确定(野指针)
}
这里同样也是被销毁了。
5.3 题目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); // 必须释放
}
5.4 题目4:释放后使用
cpp
void Test(void) {
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str); // 释放内存
if (str != NULL) { // str不为NULL(值不变)
strcpy(str, "world"); // 访问已释放内存(未定义行为)
printf(str);
}
}
6. 柔性数组:动态结构体成员
6.1 什么是柔性数组?
C99中,结构体的最后一个元素可以是未知大小的数组,称为柔性数组成员
cpp
struct st_type {
int i;
int a[]; // 柔性数组成员
};
特点:
-
结构体中必须至少有一个其他成员
-
sizeof返回的结构大小不包括柔性数组内存 -
使用malloc动态分配,分配的内存应大于结构大小
6.2 柔性数组的使用
cpp
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type {
int i;
int a[]; // 柔性数组成员
} type_a;
int main() {
// 分配结构体 + 100个整型的空间
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
p->i = 100;
for (int i = 0; i < 100; i++) {
p->a[i] = i; // 访问柔性数组
}
free(p); // 一次释放所有内存
return 0;
}
6.3 柔性数组的优势
方案2:使用指针
cpp
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));
// 需要两次释放
free(p->p_a);
free(p);
return 0;
}
与指针方案相比,柔性数组有两大优势:
柔性数组的优势:
-
方便内存释放:一次分配,一次释放,避免忘记释放成员内存
-
提高访问速度:连续内存布局,减少内存碎片,提高缓存命中率
7. C/C++程序内存区域划分
内存布局图

各区域详解
-
栈区(Stack)
-
存储局部变量、函数参数、返回地址等
-
由编译器自动分配和释放
-
空间有限,速度快
-
-
堆区(Heap)
-
动态内存分配区域
-
由程序员手动管理(malloc/free)
-
空间较大,分配速度较慢
-
-
数据段/静态区
-
存储全局变量、静态变量
-
程序启动时分配,结束时释放
-
分为已初始化(.data)和未初始化(.bss)两部分
-
-
代码段
-
存储程序代码(机器指令)
-
只读,防止程序被意外修改
-
总结
动态内存管理要点

最佳实践建议
-
始终检查返回值:malloc/calloc/realloc可能返回NULL
-
匹配分配与释放:每个malloc应有对应的free
-
避免内存泄漏:及时释放不再使用的内存
-
防止悬空指针:释放后立即将指针设为NULL
-
考虑使用柔性数组:需要动态结构体成员时优先选择
-
理解内存布局:有助于调试和性能优化
常见陷阱
-
忘记检查malloc返回值
-
释放后继续使用指针
-
越界访问动态分配的内存
-
多次释放同一块内存
-
忘记释放内存导致内存泄漏
掌握动态内存管理是成为合格C程序员的关键一步。通过理解原理、熟悉函数使用、避免常见错误,你可以编写出更健壮、高效的C语言程序。
欢迎在评论区交流讨论,如果觉得有帮助,请点赞收藏支持!
更多C语言技术文章,请访问我的博客主页:我能坚持多久-CSDN博客