目录
一、为什么要动态内存分配
以往的申请内存的方法:
// 定义变量
int a = 10; //一次性开辟一块空间 - 4个字节
// 定义数组
int arr[5]; //一块连续的空间 - 20 个字节
但是这样方法在申请内存的时候,内存的大小就固定了,不能再调整。但在有些时候,比如用 S 类型的结构体存储一个学生的信息:
struct S
{
char name[20];
int age;
};
再定义一个 S 结构体类型的数组,存储一个班级所有学生的信息:
struct S s[30];
但是一个班级的学生,如果是20个,那么分配30个就会浪费;如果是32个,分配30个又会不够。
因此,我们就希望根据键盘的输入申请变量内存大小,或者能在后续的代码中调整变量内存的大小。这种在程序运行时,或者后续编程时,才能确定需要的内存空间大小的情况,就需要用到动态内存分配。让程序员自己申请、调整、释放空间,更灵活,但更容易出错(比如忘记释放内存,程序又一直在运行,就容易导致内存不够,其它程序无法正常运行)。
二、malloc
函数原型:
void* malloc (size_t size);
作用:向内存申请一块连续可用的空间,并返回指向这块空间的指针。
参数:
- size 是需要分配的内存块大小(单位是字节)。
- 如果 size 设置为0,malloc 的行为是未定义的,由编译器决定(避免出错,不要设置为0)。
返回值:
- 申请成功,则返回指向开辟好的空间的指针;申请失败,则返回 NULL。(一定要检查返回值)
- 返回值类型为 void*,具体是什么类型自己指定,因此需要强制转换。
示例:
int main()
{
// 申请 10 个 int 类型变量的大小
int* p = (int*)malloc(40); // 或者 10 * sizeof(int)
// 检查返回值
if (p == NULL)
{
perror("malloc");
return 1;//异常返回
}
//使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
注意:并不是想申请多少内存空间都可以,不能超过内存的大小。如下,VS2019,x86(x64 可申请的内存空间比 x86 大)环境,malloc 申请内存空间,就超过了可分配内存的大小,出现错误:
三、free
就像在图书馆借了书不还,不还太多,那么图书馆就空了。申请了内存空间不释放,内存空间就没有了,无法使用内存就会导致意外情况的发生,即内存泄漏(这是指申请内存的程序一直运行的情况。运行结束,操作系统会自动回收内存)。
C语言中,使用 free 函数回收内存,函数原型如下:
void free (void* ptr);
使用:
- 如果 ptr 指向的内存不是动态分配的,free 的行为则是未定义的。如下,VS 2019 中报错:
- 如果 ptr 指向的 NULL,free 则什么也不做。
- 用 free 释放空间后,将指针设置为 NULL 是必要的(否则是野指针,即指向不可用内存的指针)。应像如下写:
因为 free 释放后, p 实际上还是存储着原来的动态分配的内存空间的地址,厚着脸皮访问也行,但是会出现问题,所以 free 释放后要置 NULL,让指针无法访问被回收的空间:
四、calloc
函数原型:
void* calloc (size_t num, size_t size);
作用:为 num 个大小为 size (单位字节)的元素开辟一块空间,并且把空间的每个字节初始化为0(效率会变低),而 malloc 不会初始化。如下:
malloc,没初始化:
calloc 会初始化为0:
因此,想效率高,选 malloc;想初始化为0,选calloc。
五、realloc
函数原型:
void* realloc (void* ptr, size_t size);
作用:如果想要调整过去申请过的内存空间的大小,就使用 relloc。
参数:
- ptr:要调整的内存地址。
- size:调整之后新的内存空间大小(单位为字节)。
- 如果 ptr 为NULL,则与 malloc 是一样的效果。如下:
//realloc函数也可以实现malloc的功能
int main()
{
realloc(NULL, 20);//等价于 malloc(20)
return 0;
}
返回值:
- 为调整后内存的起始位置。
- 三种情况:
情况1: ptr 指向的原有空间之后,有足够的空间。(返回 ptr)
情况2: ptr 指向的原有空间之后,没有足够的空间,但能找到新的 size 大小的连续空间。(返回新的地址)
情况3: ptr 指向的原有空间之后,没有足够的空间,并且不能找到新的 size 大小的连续空间。(返回 NULL)
示例:
// 申请 20 Byte 空间
int * ptr = (int*)malloc(20);
//20 Byte 空间不够,调整为 40 Byte
int* p = (int*)realloc(ptr, 40);
三种情况图解:
不能把返回值直接赋值给 ptr,而是赋值给一个新的指针变量 p 。因为如果 realloc 失败,会返回 NULL,这时直接把返回值赋值给 ptr,就哪内存空间也用不了了。下面是正确的示例:
注意:旧的空间操作系统会自动回收,所以不需要 free。
malloc、free、calloc、realloc,都包含在头文件 <stdlib.h> 中。
六、常见动态内存分配的错误
(1)解引用NULL 指针
int main()
{
int *p = (int*)malloc(40);
*p = 20;//如果malloc失败,那么p的值是NULL,相当于没有空间,还存值
return 0;
}
解决办法:在 malloc、calloc 申请空间后,一定要检查返回值:
if (ptr == NULL)
{
perror("malloc"); // 或 calloc
return 1;
}
(2)越界访问动态分配内存
int main()
{
//误认为申请了40个空间,就是40个int的大小
int *p = (int*)malloc(40);//10*sizeof(int)
if (p == NULL)
{
perror("malloc");
return 1;
}
//NULL
int i = 0;
for (i = 0; i < 40; i++) //越界访问
{
p[i] = i;
}
return 0;
}
解决办法:malloc 时,最好写成如下形式:
// 申请10个int类型的内存空间大小
int *p = (int*)malloc(10*sizeof(int));
(3)用 free 释放非动态分配内存
int main()
{
int* p = malloc(40);
if (p == NULL)
{
return 1;
}
int arr[5] = {0};//栈区的空间
p = arr;
free(p);//释放栈区的空间,出错
p = NULL;
return 0;
}
(4)用 free 释放动态分配内存的一部分
不能从中间的位置开始释放,只能从头开始释放一整块动态分配的内存。
(5)对同一块动态内存的多次释放
多次释放直接报错:
解决办法,free 后指针置 NULL,free(NULL) 意味着不执行任何操作:
(6)动态分配内存忘记释放(内存泄漏)
错误示范1:
错误示范2:
七、动态内存经典笔试题
(1)题目1
以下代码存在什么问题?
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
图解:
问题1:在空地址存储数据,程序崩溃,没有输出。
问题2:返回主函数后,系统收回 p 形参的空间,但 malloc 的动态内存还存在,没有释放,内存泄漏。
解决方法1(传指针的地址):
解决方法2(返回动态内存首地址):
(2)题目2
以下代码存在什么问题?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
图解:
问题:返回栈空间地址,非法访问内存空间,输出未知的内容。如下图所示:
扩展1:
上面的代码没有问题,虽然 z 被销毁了,但是会先把返回值暂存在寄存器,返回主函数后,再从寄存器返回值。
扩展2:
上面这个代码就是错的,返回值 &a 暂存在寄存器,寄存器再把返回值赋值给 p,但是此时局部变量a的内存空间已经被收回,继续通过指针p访问就是非法的,这就是返回栈空间地址的问题。
扩展2的运行结果:
扩展2输出未知内容的解释:
(3)题目3
下面代码有何问题?
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
问题:free后,指针 str 未置NULL,成为野指针,造成后续进入 if 语句,非法访问。
解决:
(4)题目4:
下面代码有何问题?
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
int main()
{
Test();
return 0;
}
问题:未 free 释放动态内存。
八、柔性数组
(1)柔性数组的概念
C99中,结构体允许最后一个成员是大小未知的数组,叫柔性数组。形式如下:
struct S
{
int n;
//柔性数组
int arr[];//或 int arr[0]
};
但有些编译器只支持 arr[] 的写法。
(2)柔性数组的特点
- sizeof 返回的结构体大小不包括柔性数组的内存。如下:
- 结构中的柔性数组成员前面必须至少有一个其他成员(否则这个结构体就不知道大小了)。
- 包含柔性数组成员的结构体进行动态内存分配,并且分配的内存应该大于结构体的大小(多了柔性数组成员的大小)。
(3)柔性数组的使用
// 柔性数组的使用
struct S
{
int n;
int arr[];//柔性数组
};
int main()
{
//struct S s;//不会这样写
//会这样写
struct S* ps = (struct S*)malloc(sizeof(struct S) + 20);
// 检查
if (ps == NULL)
{
perror("malloc");
return 1;
}
// 使用
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
// 扩大动态内存
struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 40);
// 检查
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps = tmp;
// 使用
for (i = 5; i < 10; i++)
{
ps->arr[i] = i + 1;
}
// 输出
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
// 释放
free(ps);
ps = NULL;
return 0;
}
运行结果:
动态内存分配图解及验证:
(4)柔性数组的优势
使用柔性数组,实现了:
- 结构体的所有内存空间都是在堆上开辟的。
- 数组的大小是可以调整的。
也可以用其它的方法(不使用柔性数组),来实现上面的效果。给结构体变量分配动态的内存空间;把柔性数组成员替换成指针,让指针指向一块动态分配内存空间。代码如下:
struct S2
{
int n;
int* arr;
};
int main()
{
struct S2* ps = (struct S2*)malloc(sizeof(struct S2));
if (ps == NULL)
{
perror("malloc-1");
return 1;
}
ps->n = 100;
ps->arr = (int*)malloc(5 * sizeof(int));
if (ps->arr == NULL)
{
perror("malloc-2");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
else
{
ps->arr = ptr;
}
for (i = 5; i < 10; i++)
{
ps->arr[i] = i + 1;//6 7 8 9 10
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);//1 2 3 4 5 6 7 8 9 10
}
//释放
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
动态内存分配图解及验证:
对比两种方法,柔性数组的优势:
- 内存释放方便:柔性数组只需要释放一次内存,而方法2要分别释放两次(结构体的空间 + arr指针指向的空间。两块不连续的空间,起始地址不同,要分别释放)。
- 内存碎片更少,避免内存空间的浪费:方法2不连续的内存空间更多,内存中不连续的空间太多,会导致剩下的夹缝中的内存很细碎,无法存储其它需要大块连续内存空间的数据,从而造成空间浪费。
- 连续的内存有利于提高访问速度。
扩展阅读:C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell
九、总结 C/C++ 中程序内存区域划分
内存映射段目前不需要知道是什么东西;代码段是存放程序编译之后,代码和常量的二进制指令。
以左边的代码为例子:紫色框出的是全局变量、静态变量,存放在数据段;绿色框出的是局部变量、有关函数调用的信息,存放在栈区;黄色框出来的是动态分配的内存空间,在堆区;红色框出来的是初始化局部变量的常量,存放在代码段。
总结:
- 栈区(stack):① 函数的局部变量、形参、返回值、返回到的地址等函数信息,都在栈区创建。函数执行结束,存储这些信息的空间也会被自动释放。② 栈区的内存分配在处理器的指令集中运算,效率高,但可分配的空间有限。
- 堆区(heap):① 由程序员释放,程序员不释放,程序结束后会被操作系统回收。② 分配方式类似于链表。
- 数据段(静态区):存放全局变量、静态变量,程序结束后由系统释放。
- 代码段:存放函数体的二进制代码。