一、什么是动态内存管理
在C语言中,动态内存管理是指程序运行时动态地分配和释放内存的过程。与静态内存分配(如定义数组时编译器自动分配固定大小的内存)不同,动态内存管理允许程序根据实际需要在运行时灵活地获取和释放内存,以适应不同的内存需求。
动态内存管理的核心函数:malloc函数,calloc函数,realloc函数,free函数。
二、为什么存在动态内存管理(动态内存管理与静态内存分配对比)
| 动态内存管理 | 静态内存分配 | |
|---|---|---|
| 分配时机 | 程序运行时,通过特定函数(malloc、calloc、realloc)手动分配 | 在程序编译时进行,由编译器自动分配 |
| 内存位置 | 位于堆区,由程序员控制 | 变量存储在栈区(局部变量)或静态区(全局变量、静态变量) |
| 生命周期 | 从分配到手动释放,未释放导致内存泄漏 | 由变量的作用域决定,局部变量在函数结束后销毁,全局变量在程序结束后销毁 |
| 特点 | 大小可根据需求动态跳转,灵活性高,能按需使用内存,但管理复杂,需要手动释放,否则可能引起内存泄漏 | 大小固定,编译时确定,无需手动释放,管理简单,但灵活性低,可能浪费内存 |
c
#include <stdio.h>
int main()
{
int a = 10;//申请4个字节空间
char c = 'x';//申请1个字节空间
int arr[10];//申请40个字节的连续空间
return 0;
}
上面代码中,空间开辟大小是固定的。 数组在申请的时候,必须指定数组的长度,数组空间一旦确定就没办法修改和调整了(VS编译器不支持变长数组)。
二、malloc函数

malloc函数向内存堆区申请分配一块连续可用内存空间,大小为size个字节。如果申请成功,malloc函数返回目标空间的起始地址,但是由于不知道内存空间会存什么样的数据,所以返回的指针类型是void*;如果没有申请到内存块(没有可分配的内存),返回空指针。
如果size为0,malloc的行为是标准未定义的,取决于编译器。

2.1 malloc函数的使用
c
#include <stdio.h>
int main()
{
//申请20个字节的空间,存放5个整数
int* p = (int*)malloc(5 * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
else
{
for(int i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
for(int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
}
//释放空间
free(p);
p = NULL;
return 0;
}
运行结果:

malloc函数开辟空间失败:
c
#include <stdio.h>
int main()
{
int* p = (int*)malloc(INT_MAX * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
else
{
for(int i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
for(int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
}
//释放空间
free(p);
p = NULL;
return 0;
}
运行结果:

三、free函数

malloc函数管借不管还,需要free函数对申请的空间进行释放。如果内存空间不是由malloc函数(calloc或realloc函数)申请的,那么free函数的行为是未定义的。如果ptr指向的是空指针,则函数什么都不做。
值得注意的是,free函数不会改变ptr的值,ptr仍然指向地址。
c
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* p = (int*)malloc(n * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
else
{
printf("%p\n", p);
}
free(p);
//释放后,p !=NULL,p就为野指针
p = NULL;
return 0;
}
运行结果:

c
#include <stdio.h>
int main()
{
//申请20个字节的空间,存放5个整数
int* p = (int*)malloc(5 * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
else
{
for(int i = 0; i < 5; i++)
{
*(p + i) = i + 1;
}
free(p);
for(int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
}
return 0;
}
运行结果:

四、calloc函数

calloc函数是为一个num个元素的数组申请分配一块内存空间,数组中每个元素大小为size字节,一共申请的内存大小为num*size个字节。calloc函数也会将内存空间每一位都初始化为0。
如果申请成功,calloc返回值依然是申请空间的起始地址;如果没有申请到内存,会返回空指针NULL。
与函数malloc的区别只在于calloc会把申请的空间的每一位初始化为0。
c
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* p = (int*)calloc(n * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("calloc");
return 1;
}
//开辟成功,使用这块空间
else
{
for(int i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
}
free(p);
p = NULL;
return 0;
}
运行结果:

c
#include <stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* p = (int*)calloc(INT_MAX * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("calloc");
return 1;
}
//开辟成功,使用这块空间
else
{
for(int i = 0; i < n; i++)
{
printf("%d ", p[i]);
}
}
free(p);
p = NULL;
return 0;
}
运行结果:

五、realloc函数

realloc函数是调整ptr指向的内存块大小(内存块是由malloc函数、calloc申请的),可能会将内存块移动到一个新地址,返回值是调整之后的内存起始地址(调整空间成功)或者空指针(调整空间失败)。
ptr是要调整的内存地址,size是调整之后新大小,单位是字节。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间。

情况2:原有空间之后没有足够大的空间。

如果原空间之后没有足够大的空间,会将p指向的空间拷贝到ptr指向的地址处,同时,原来p指向的空间会被释放。

举例:
x86环境:
c
#include <stdlib.h>
int main()
{
//申请一块空间,用来存放1~5的数字
int* p = (int*)malloc(5 * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
for(int i = 0; i < 5; i++)
{
p[i] = i +1;
}
//想要继续存放6
//空间不够,需要扩容
int * ptr = (int*)realloc(p, 6 * sizeof(int));//为了防止realloc函数扩容失败返回空指针,不用p来接收
if(ptr == NULL)
{
perror("realloc");
return 1;
}
p = ptr;
for(int i = 0; i < 6; i++)
{
*(p + i) = i + 1;
}
for(int i = 0; i < 6; i++)
{
printf("%d ", *(p + i));
free(ptr);
ptr = NULL;
}
p = NULL;
return 0;
}
运行结果:

内存布局如下:

情况2(x64环境):
c
#include <stdlib.h>
int main()
{
//申请一块空间,用来存放1~5的数字
int* p = (int*)malloc(5 * sizeof(int));
//判断返回值
if(p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功,使用这块空间
for(int i = 0; i < 5; i++)
{
p[i] = i +1;
}
//想要继续存放6~10
//空间不够,需要扩容
int * ptr = (int*)realloc(p, 6 * sizeof(int));//为了防止realloc函数扩容失败返回空指针,不用p来接收
if(ptr == NULL)
{
perror("realloc");
return 1;
}
p = ptr;
for(int i = 5; i < 10; i++)
{
*(p + i) = i + 1;
}
for(int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
free(ptr);
ptr = NULL;
}
p = NULL;
return 0;
}
运行结果:

内存布局如下:

realloc函数直接申请内存空间:
c
#include <stdio.h>
int main()
{
int * p = (int*)realloc(NULL, 40);//等价于malloc(40)
for(int i = 0; i < 10; i++)
{
p[i] = i +1;
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
运行结果:

六、常见动态内存错误
6.1 对NULL指针解引用操作---没有对malloc、calloc
realloc返回值进行判断
c
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就是非法访问
free(p);
}
6.2 对动态开辟空间的越界访问
c
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
exit(EXIT_FAILURE);
}
for(i = 0; i <= 10; i++)
{
}
free(p);
}
6.3 对非动态开辟的空间使用free进行释放
c
void main()
{
int a = 10;//栈区上,自动开辟,自动回收
int* p = &a;
free(p);//ok?---只能释放malloc、calloc、ralloc申请的空间
return 0;
}
6.4 使用free释放一块动态开辟内存的一部分
c
#include <stdio.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for(i = 0; i < 5; i++)
{
*p = i + 1;
p++;
}
free(p);//free释放空间的时候,一定要给这块空间的起始位置
p = NULL;
return 0;
}
6.5 对同一块动态内存多次释放
c
#include <stdio.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
perror("malloc");
return 1;
}
for(int i = 0; i < 5; i++)
{
*p = i + 1;
p++;
}
free(p);
free(p);//error
return 0;
}
6.6 动态开辟内存忘记释放(内存泄漏)
c
#include <stdio.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if(p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for(i = 0; i < 5; i++)
{
*p = i +1;
p++;
}
return 0;
}
如何规避:①谁申请的空间谁释放;②不使用的空间及时释放;③自己(函数1)不方便释放的空间,要告诉别人(函数2);④一般情况下,malloc函数和free函数成对出现。
七、柔性数组
7.1 什么是柔性数组
在C99标准中,结构体中最后一个元素可以是未知大小的数组,这叫做柔性数组成员。
c
//柔性数组
struct stu
{
int n;
int a[];//柔性数组成员
}
7.2 柔性数组的特点
①结构中的柔性数组成员前面必须至少有一个其他成员。
②sizeof返回的这种结构体大小时,不包括柔性数组大小。
③包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
7.3 柔性数组使用
7.3.1 使用方法一
c
struct stu
{
int n;
int arr[];
};
int main()
{
struct stu* ps = (struct stu*)malloc(sizeof(struct stu) + 10 * sizeof(int));
if(ps == NULL)
{
perror("malloc");
return 1;
}
//直接使用空间
ps->n = 100;
int i = 0;
for(i = 0; i < 10; i++)
{
ps->arr[i] = i + 1;
}
//调整空间
struct stu* tmp = (struct stu*)realloc(ps, sizeof(struct stu) + 10 * sizeof(int));
if(tmp == NULL)
{
perror("realloc");
return 1;
}
//继续使用空间
//释放
free(p);
ps = NULL;
return 0;
}
7.3.2 使用方法二
c
#include <stdio.h>
struct stu
{
int i;
int* n;
};
int main()
{
//创建空间
struct stu* ps = (struct stu*)malloc(sizeof(struct stu));
if(ps == NULL)
{
perror("malloc");
return 1;
}
//使用空间
ps->i = 100;
ps->n = (int*)malloc(ps->i * sizeof(int));
if(ps->n == NULL)
{
perror(malloc);
return 1;
}
for(int i = 0; i< ps->i; i++)
{
*(ps->n + i) = i + 1;
}
//空间不足,调整空间
struct stu* ptr = (struct stu*)realloc(ps, sizeof(struct stu) + ps->i * sizeof(int));
if(ptr == NULL)
{
perror("realloc");
return 1;
}
//使用
//释放空间
free(p);
free(ps->n);
free(ps);
ps = NULL;
return 0;
}
与方法二相比,方法一有2个好处:
1.方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
2. 有益于减少内存碎片,提高内存利用律。因为每次调用malloc函数都需要在内存中申请空间,但是没有办法保证每一块空间都是连续的,它们之间存在空隙(内存碎片)
8、总结C/C++中程序内存划分

①栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
②堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。分配类似于链表。
③数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放。
④代码段:存放函数体(类成员函数和全局函数)的二进制代码。