前言:为什么要有动态内存分配?
我们已经掌握的内存开辟方式有:
① int val =10; //通过在栈上开辟4个大小的空间
② char arr[10]={0}; //通过在栈空间上开辟10个字节的连续空间
通过上述代码,我们发现两个特点:
• 空间开辟的大小是固定不可变的。
• 数组在声明的时候,必须指定数组的长度,数组的空间一旦确定便不可调整和更改。
但是通常对于空间的需求,不仅仅是固定的空间,有时候我们需要根据程序的需求,不断更改内存空间的大小,而上述的方式已经不在能够满足我们的需求,此时我们需要动态内存开辟。
动态内存分配的核心作用:
①静态分配(如数组)必须提前确定大小,无法适应运行时变化的需求。
②动态分配允许程序运行时按需申请内存(例如用户输入决定数据量)。
③突破栈空间限制
栈内存(局部变量)大小有限(通常几MB),大内存需求(如处理图像、大型矩阵)必须用堆内存(动态分配)。
④灵活控制生命周期静态变量(全局/局部)生命周期固定,动态内存可手动管理(malloc申请后,直到free才释放),适合长期存储数据(如链表节点)。
⑤节省内存避免静态分配“按最大可能”预占内存的浪费(如声明int arr[1000]但只用了10个元素)。
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,灵活的掌控内存空间,体验掌握内存空间的魅力。

一、malloc函数
1.malloc的简介
参数分析:
void* malloc (size_t size);
参数: size_t size 表示开辟内存空间的字节大小
返回类型: void * 返回开辟后,指向空间的大小。
函数用途:开辟 size 大小字节的空间
2.malloc函数的使用
代码示例:
//malloc
void test1()
{
//申请20个字节空间,并用整形指针接收
int* p = (int*)malloc(20);
//判断是否开辟成功,如果失败打印错误信息
if (p == NULL)
{
perror("malloc");
}
//开辟成功,给开辟的整形空间赋值
else
{
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
}
}
温馨提示:
①头文件包含:malloc的使用需要包含头文件<stdlib.h>
②如果开辟成功,则返回⼀个指向开辟好空间的指针。
③如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
④返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
⑤如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。
二、free函数
1.free函数的简介
参数分析:
void free (void* ptr);
参数:由动态内存开辟的空间的起始地址
返回类型:void(空)
函数用途:释放由动态内存开辟的空间
2.free函数的使用
由动态内存开辟的空间最好手动释放,防止内存泄漏,导致程序出现问题,通过学习free这个函数,对于上面的代码我们可以进行一定的完善。
//malloc
void test1()
{
//申请20个字节空间,并用整形指针接收
int* p = (int*)malloc(20);
if (p == NULL)
{
perror("malloc");
}
else
{
for (int i = 0; i < 5; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
}
free(p); //若是采用*p=i; p++; 会导致p不是起始地址
// free函数⽤来释放动态开辟的内存,p必须为起始位置
p = NULL; //因为内存已经还给操作系统了,所以p已经是野指针了,需要置空。
}
温馨提示:
①我们对动态内存开辟的空间,不采用 p++ 这种改变起始地址的形式进行赋值,这样会导致free的时候出现问题,所以我们一般采用(p+i) 的形式进行移动指针,从而进行赋值操作。
②对于已经free的动态内存地址p,其p指针需要进行置空,因为操作系统已经收回了该空间的使用权,而p仍然指向该空间,这样的p就成了野指针,不能进行操作使用。
三、calloc函数
1.calloc函数的简介
1.参数分析
void* calloc (size_t num, size_t size)
函数用途:为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
温馨提示:函数calloc与函数 malloc 的区别,只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
2.calloc函数的使用
代码示例:申请5个int类型字节大小的空间
void test2()
{
int* p = (int*)calloc(5,sizeof(int));
if (p != NULL)
{
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p+i) );
}
}
free(p);
p = NULL;
}
四、realloc函数
1.realloc函数的简介
①:realloc函数的出现让动态内存管理更加灵活
②:有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。
参数分析:
void* realloc (void* ptr, size_t size);
参数一:ptr 是要调整的内存地址
参数二:size 调整之后新⼤⼩
返回值:返回值为调整之后的内存起始位置
函数功能:这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。
注意事项:realloc在调整内存空间的是存在两种情况
◦ 情况1:原有空间之后有⾜够⼤的空间
①拥有足够的空间进行调整,此时整形指针tmp的地址仍然为ptr所开辟的起始地址。
◦ 情况2:原有空间之后没有⾜够⼤的空间
②缺乏足够的空间进行调整,此时整形指针tmp就会在内存中,另找一块连续且充足的空间,原空间所存储的值不会因为,地址重新分配而发生改变,仍然为原来的值,此时的tmp将不同于ptr,发生改变。
2.realloc函数的使用
对于calloc开辟的空间我们可以,通过realloc进行扩充,但需要注意扩充失败。
代码示例:
//realloc 用于调整内存空间的大小
void test3()
{
int* p = (int*)calloc(5, sizeof(int));
if (p != NULL)
{
for (int i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
}
// p = (int*)realloc(p, 40);
// 如果无法扩充空间,“realloc”可能返回 null 指针。
// 将 null 指针赋给“p”, 导致不能访问到原来开辟的空间,从而将导致原始内存块泄漏
//通过使用新的指针ptr进行接收,并判断是否为空指针。
//如果为空指针则打印错误信息,反之将ptr赋值给p,继续利用p进行维护开辟的空间
int *ptr= (int*)realloc(p, 40);
if (ptr != NULL)
{
p = ptr; //开辟空间成功,继续利用p指针维护
ptr = NULL;
//调整成功,使用开辟的空间
for (int i = 5; i < 10; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
}
else
{
perror("realloc");
//调整失败,通过p指针使用调整前的空间
}
free(p);
p = NULL;
}
五、常⻅的动态内存的错误
1.对NULL指针的解引⽤操作。
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
由于malloc 申请了过大的字节空间,导致动态内存开辟失败,从而p接收到了NULL(空指针)。
对空指针进行解引用操作,从而导致程序崩溃。
2.对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("p");
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
}
因为该程序仅向内存申请了10个字节大小的空间,当i=10的时候,p指针已经指向了未知空间,对其解引用操作将导致越界访问的同时,还修改了内存中存储的其他值。
如下图所示:
3.对⾮动态开辟内存使⽤free释放
void test()
{
int a = 10;
int* p = &a;
free(p);
}
free函数只能对动态内存开辟的空间进行释放,若对非动态内存开辟的空间进行释放,将会导致程序崩溃。
4.使⽤free释放⼀块动态开辟内存的⼀部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
由于p指针发生偏移,导致p指针不在指向,动态内存开辟的地址,若此时对其free,会导致仅释放动态内存的一部分,而导致内存出现泄露。
5.对同⼀块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
}
当第一次调用
free(p)后,p所指向的内存空间已经被释放,此时p成为了野指针。再次对野指针执行free操作是非法的,会破坏内存管理系统的状态。
6.动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
当
test()函数执行结束后,指针p的作用域消失(局部变量),但它指向的动态分配内存并未被释放。这部分内存会一直被程序占用,无法被系统回收,造成内存泄漏。尤其在长期运行的程序中,内存泄漏会逐渐消耗系统资源,可能导致程序性能下降甚至崩溃。
六、练习题
1.练习一:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
① str传递给Getmemory函数的时候,采用的值传递,形参变量p是str的一份拷贝,当我们把malloc申请的空间的起始地址放在p中时,我们不会修改str,str仍然是NULL,所以当getmemory函数返回后,再去调用strcpy函数,将“hello world"拷贝到str指向的空间时,程序崩溃。
②未使用free, 及时的对开辟的动态内存及时释放。
那么,如何对这段代码进行修改呢?
①这段代码的核心问题是出现在值传递,如果Getmemory函数通过地址传递,就能正确的指向动态内存所开辟的空间。
② 及时进行free,释放动态开辟的内存。
//修改程序:将传值调用改为传址调用,形参的改变可以影响实参
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
练习二:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
解析:
由于GetMemory函数中创建的数组是在栈区上创建的,当调用完Getmemory函数后,p数组将会被操作系统回收,这块空间将不可被访问,而str所接收的地址,虽然指向了这块空间,但是我们不能进行访问,导致其变成了野指针。
那么,如何对这段代码进行修改呢?
这段代码的核心是:函数中的数组p被操作系统所回收,如果将其变为静态变量,则该数组在函数调用完后,不会被操作系统所回收,通过str所接收的地址,仍然能访问该空间,从而打印出正确的结果。
方法一:
//修改代码:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
static char p[] = "hello world";//static修饰的变量是放在静态区中的
//所以函数栈桢销毁的时候,这块空间仍然保留
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
方法二:
//修改代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char * p)
{
//将字符串复制到p变量中
strcpy(p,"hello world");
}
void Test()
{
char str[20]; //定义在栈区的缓冲区
GetMemory(str);
printf(str);
}
int main()
{
Test();
return 0;
}
七、柔性数组
1.柔性数组的简介:
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。
代码示例:
struct st_type
{
int i;
int a[0];//柔性数组成员
};
有些编译器会报错⽆法编译可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
2.柔性数组的特点
• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
• sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。
• 包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构⼤⼩,以适应柔性数组的预期⼤⼩。
思考一下为什么柔性数组成员前面必须至少一个其他成员?
如果在结构体中只有一个柔性数组的话,那么sizeof(结构体名)的大小又会是多少呢,显然无法计算,因而柔性数组前至少有一个其他成员。
此外,sizeof(结构体名)不会计算柔性数组,而计算的是除它外其他成员的大小。
代码示例:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));//输出的是4,不包括柔性数组的大小
return 0;
}
3.柔性数组的使用
结构体的大小并不会包括柔性数组的大小,这也就意味着,我们在使用含有柔性数组的结构体时,不会按照下面的方法创建变量:
#include<stdio.h>
struct S
{
int i;
int arr[];
};
int main()
{
struct S s;//一般不会这么写:这样写的话并不会为数组分配空间
return 0;
}
通常需要利用malloc函数为结构体开辟空间,并用结构体指针进行维护。
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
int main()
{
int i = 0;
//分为两部分,第一部分为结构体其他成员开辟空间,第二部分为柔性数组开辟100个整形大小的空间
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;
}
其实可以不通过柔性数组的方式,也能达到相同的效果.
代码示例:
#include<stdio.h>
#include<stdlib.h>
typedef struct S
{
int i;
int* arr;
}type_s;
int main()
{
type_s* p = (type_s *)malloc(sizeof(type_s));
if (p == NULL)
{
perror("p");
return 1;
}
p->i = 100;
//为arr指针 开辟动态内存分配
p->arr = (int *)malloc(10 * sizeof(int));
//判断是否开辟成功
if (p->arr == NULL)
{
perror("p->arr");
return 2;
}
//开辟成功,给开辟的空间进行赋值操作
for (int i = 0; i < 10; i++)
{
*(p->arr + i) = i;
printf("%d ", *(p->arr + i));
}
//利用realloc 进行调整空间
int* tmp = (int*)realloc(p->arr, 20* sizeof(int));
if (tmp == NULL)
{
perror("tmp");
return 3;
}
p->arr = tmp;
//进行free操作
//优先对arr进行释放,若提前对结构体指针进行,结构体指针释放后,将找不到arr指针。
//以从 里 向 外 的方式进行释放
free(p->arr);
p->arr = NULL;
free(p);
p = NULL;
return 0;
}
八、C/C++中程序内存区域划分
对于C/C++程序内存的区域,大致如下分布:

温馨提示:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。 栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配⽅式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。




720





