一、动态内存为何存在
在动态内存管理之前,我们已经学过了在栈空间开辟内存的方式:
int a = 4;(在栈区开辟四个字节的空间)
char arr[ 10 ];(在栈区开辟10个字节的连续空间)
这些开辟空间的方式有两个特点:
(1)空间开辟的大小是固定的;
(2)数组在申明的时候,必须指定数组的长度,数组空间大小一旦确定后续就不能调整了。
但是我们平常难免会面对这种情况,就是需要开辟多大的空间是程序开始运行时才知道的,那常量开辟空间的方式就不适用了,变量和数组的方式还不够灵活。因此C语言引入了动态内存开辟,使用动态内存分配可以自己来维护内存的使用生命周期,让程序员可以自己申请想要的空间,以及不用这部分空间后及时释放。这种动态内存开辟就相对来说非常灵活了。
本期就主要介绍动态内存开辟的方法以及注意事项。
二、malloc和free函数
malloc和free函数的使用都需要包含头文件<stdlib.h>。
1、malloc函数
void* malloc(size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
(1)如果开辟成功,则返回一个指向开辟好的空间的指针;
(2)如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查;
(3)返回值的类型是 void*,所以 malloc 函数并不知道要开辟的空间的类型,具体类型在使用的时候由使用者自己来决定;
(4)如果参数 size 为 0,malloc 的行为是标准是未定义的,取决于编译器。
2、free函数
free函数是专门用来做动态内存的释放和回收的。
void free ( void* ptr ) ;
(1)free函数的参数部分要传递的是要释放的空间的起始地址;
(2)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;
(3)如果参数 ptr 是 NULL 指针,则函数什么事都不做。
程序退出的时候,即使没有free,操作系统也会主动回收这块内存空间,最坏的情况是:程序不退出,动态开辟的空间也不free,申请到的内存又不实用。
三、calloc和realloc函数
calloc和realllc函数的使用也需要包含头文件<stdlib.h>。
1、calloc函数
void* calloc (size_t num, size_t size);
(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为 0。
(2)与 malloc 函数的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。
2、realloc函数
realloc函数的优势:realloc 函数的出现让动态内存管理更加灵活。有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小进行调整。
void* realloc (void* ptr, size_t size);
(1)ptr 是要调整的内存地址;
(2)size 是调整之后的新大小;
(3)返回值为调整之后的内存起始位置;
(4)这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc 在调整内存空间的是存在两种情况:
【1】原有空间之后有足够大的空间;
当是情况【1】的时候,要扩展内存就直接在原有内存之后追加空间,原来空间的数据不发生变化。
【2】原有空间之后没有足够大的空间;
当是情况 【2】 的时候,原有空间之后没有足够多的空间,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,原来内存中的数据也会移动到新的空间中。
注:如果realloc调整内存空间失败,就会返回NULL。
四、常见的动态内存的错误
1、对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;
free(p);
}
如果p的值是NULL,就发生了对NULL(空)指针的解引用,就会出现问题。
2、对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
exit(EXIT_FAILURE);
}
for (i = 0; i <= 10; i++)
{
*(p + i) = i;
}
free(p);
}
当i==10时,会发生越界访问,导致错误。
3、对非动态开辟内存使用free
void test()
{
int a = 10;
int* p = &a;
free(p);
}
这里的p指向的内存空间不是动态开辟的,如果对p使用free,就是对非动态开辟的内存使用free,会导致错误出现。
4、使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);
}
p++后,p所代表的地址不是动态开辟的空间的首地址,p不再指向动态内存的起始位置,对p使用free会出现问题。
5、对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);
}
这里对p多次使用free,重复释放,会导致问题出现。
6、动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
这里忘记释放不再使用的动态开辟的空间,会造成内存泄露。动态开辟的空间一定要释放,并且要正确释放。
五、柔性数组
C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员。
1、柔性数组的特点
(1)结构中的柔性数组成员前面必须至少有一个其他成员。
(2)sizeof 返回的这种结构大小不包括柔性数组的内存。
(3)包含柔性数组成员的结构用 malloc () 函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
2、柔性数组的使用
typedef struct st_type
{
int i;
int a[0];
}type_a;
int main()
{
int i = 0;
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;
}
经过上述代码操作之后,柔性数组成员a,相当于获得了100个整型元素的连续空间。
3、柔性数组的优势
先介绍一种不使用柔性数组也能达到同样效果的方案:
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));
for (int i = 0; i < 100; i++)
{
p->p_a[i] = i;
}
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
return 0;
}
那与使用柔性数组的方案进行对比,不难得出使用柔性数组的优势:
(1)方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存给释放掉。
(2)这样有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片。
六、总结C/C++中程序内存区域划分
C语言程序内存分配的几个区域:
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS(操作系统)回收。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。