1.动态内存管理存在的意义
在前面的C语言的学习中,我们已经掌握的空间开辟的方式有如下两种
int i = 0;
//开辟了4字节大小的空间存放i
int arr[5] = {0};
//开辟了20字节的空间存放数组arr
这样开辟空间有两个显著的特点:
1、每次开辟空间的大小都是固定好的。
2、数组在创建时就已经确定好了大小,这样使用起来灵活性不足。
在实际的编程中,对空间的需求不仅仅是以上的情况,当需要的空间大小在程序运行时才能明确的情况下,数组的编译时开辟空间的方式就不能满足了。这时就需要引入一个新的概念,动态内存管理。
2.动态内存函数的介绍
这里介绍的动态内存函数均包含于C标准库中的#include <stdlib.h>
2.1malloc和free
2.1.1malloc函数
malloc是C语言标准库中提供的一个动态内存开辟的函数。
它的声明如下:void* malloc(size_t sz);
malloc函数会向内存申请一块连续可用的空间,并返回指向该空间的起始地址的指针。如果malloc函数申请空间成功,会返回一个指针,该指针用于存放申请空间的起始地址。若malloc函数申请空间失败,将返回空指针NULL。所以在使用malloc函数申请内存空间时,需要对函数的返回值做一个判断。由于malloc函数返回值为 void* ,所以开辟的空间所存数据的类型是由使用者定的,所以具体的类型由使用者自己决定。如果参数部分sz为0的话,malloc函数的是否开辟内存空间,这是C语言标准未定义的,也是有点为难编译器的,这样写是不可取的。
2.1.2free函数
free函数是专门用于释放并回收动态开辟的内存空间的一个库函数。
它的声明如下:void free(void* ptr);
free函数用于释放动态开辟的内存的。如果free参数不是指向动态内存函数开辟的起始空间的指针,那么free的执行结果,也是C语言标准未定义的。在当前作者使用的VS2019环境下会造成程序直接崩溃。若free函数的参数为NULL,那么调用free函数将啥事都不发生。
2.1.3malloc函数、free函数的使用
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* pa = (int*)malloc(40);
//这里动态开辟40字节的连续空间
//pa指针指向的是动态开辟空间的起始地址
if(NULL == pa)//判断是否开辟成功
{
return 1;
}
int i = 0;
for(i = 0; i < 10; i++)
{
*(pa+i) = i + 1;
}
for(i = 0; i < 10; i++)
{
printf("%d ",*(pa+i));
}
printf("\n");
free(pa);//使用后释放内存
pa = NULL;
//释放后手动将指针置空
return 0;
}
上面代码动态开辟了40字节的连续空间用于存放10个1-10的数。需要注意的是,使用动态开辟函数malloc需要判断是否动态开辟成功。使用free函数释放动态开辟空间内容,需要手动将指针置为NULL,这样可以避免野指针的产生。因为free后,内存空间将还给操作系统,而pa指针依旧指向那块还给操作系统的空间。若有一个糊涂的程序员访问了这块空间,必将导致指针越界访问,导致程序错误。所以,释放动态内存后,请将指针手动置空。
2.2calloc函数
上面介绍了malloc函数,它是用来动态开辟内存空间的。下面要介绍的calloc函数也是用来动态开辟内存空间的。
calloc函数将为num个大小为sz的元素动态开辟空间。
声明如下:void* calloc(size_t num, size_t sz);
calloc函数的返回类型是void*,若动态开辟成功,返回一个指针指向动态开辟空间的起始地址。若开辟失败,则返回一个NULL指针。当然返回的指针类型需要使用者自己来决定。参数部分,第一个参数size_t num,指的是开辟空间的元素个数,size_t sz表示开辟空间的大小,单位是字节。
2.2.1 malloc和calloc的区别
下面将通过代码举例分析malloc和calloc的区别。
int main()
{
int* pa = (int*)malloc(40);
if (NULL == pa)
{
return 1;
}
int* pb = (int*)calloc(10, 10 * sizeof(int));
if (NULL == pb)
{
return 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ",*(pa+i));
}
printf("\n");
for (int i = 0; i < 10; i++)
{
printf("%d ", *(pb + i));
}
printf("\n");
free(pa);
free(pb);
pa = NULL;
pb = NULL;
return 0;
}
通过上例代码,可知calloc动态开辟内存后,会将num个元素给初始化成0,而malloc并没有初始化这一步骤。所以,当一些不需要初始化所有元素个数的场景下,应使用malloc函数动态开辟空间,这样将是程序的性能更好。反之,则使用calloc函数动态开辟空间。
2.3realloc函数
有时在申请内存空间时,我们觉得申请的空间多余了。有时在申请内存空间时,我们觉得申请的空间不足了。此时不免需要合理的对申请的空间进行调整,realloc函数便可以对动态开辟空间的大小进行调整。realloc函数使得动态内存管理的方式更加灵活。
函数声明:void* realloc(void* ptr, size_t sz);
ptr参数表示所需要调整的空间的起始地址。 sz参数表示,所需要调整的空间的调整后大小。该函数会返回一个调整后的空间的起始地址。realloc函数会将原本空间的数据,拷贝到调整后的空间内。
2.3.1realloc函数调整的两种情况
情况一:调整前的内存空间后,还有连续可用的内存空间。那么realloc函数会在原空间后面的空间直接进行追加扩容。
情况二:调整前的内存空间后,连续可用的内存空间不足。此时realloc函数会找到一块符合要求的连续空间进行开辟并使用。
2.3.2realloc函数使用注意事项
基于上面两种情况,我们在使用realloc函数时,应该这么做。
int main()
{
int* pa = (int*)malloc(40);
if (NULL == pa)
{
return 1;
}
int* pb = (int*)realloc(pa, 100);
if (NULL != pb)//判断是否开辟成功
{
pa = pb;
pb = NULL;//手动置空避免野指针
}
free(pa);
pa = NULL;
return 0;
}
//错误的例子
int main()
{
int* pa = (int*)malloc(40);
if (NULL == pa)
{
return 1;
}
pa = (int*)realloc(pa, 1000);//可能存在申请失败的问题
for(int i = 0; i < 10; i++)
{
printf("%d ",*(pa+i));
}
free(pa);
pa = NULL;
return 0;
}
如果realloc申请失败,此时,函数返回空指针,那么前面malloc申请的空间的起始地址将被赋为NULL,这必将内存的泄露问题,以及后续的访问权限冲突问题。所以我们应该避免这样使用realloc函数调整空间。
3.常见的动态内存管理的错误案例
3.1对NULL指针进行解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//对NULL指针进行解引用操作
free(p);
}
此时malloc函数参数为INT_MAX/4,这将是一个很大很大的数。在大部分的机器下,都无法动态申请到如此庞大的空间。这里malloc函数返回的是NULL。对NULL指针进行解引用操作,会造成访问权限冲突,进而导致程序直接崩溃。
3.2越界访问动态开辟的空间
int main()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
return 1;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
reutrn 0;
}
上面在循环体编写时,把控制条件写成了i<=10,这不可避免地导致了越界访问了。这也是一个经典的指针越界形成野指针的问题。我们在编写循环时,应注意控制变量是否会造成越界访问的问题,编译器是无法做出对越界访问的检查的。
3.3对非动态开辟的内存空间进行free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
这里我简单从语言层面上描述一下内存空间的分布
上面代码中的&a,取出的是栈区上的地址。对此地址进行free释放会导致程序直接崩溃。需要注意,free函数只能释放动态开辟内存。
3.4使用free函数释放一块动态内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
由于p++使得p的位置指向起始位置后一个指针变量大小的位置。此时对p++后的p指针进行free释放,那么会导致程序的崩溃。
3.5对同一块动态内存空间多次free释放
void test()
{
int* p = (int*)malloc(40);
if(NULL == p)
{
return;
}
free(p);
free(p);//重复释放
}
第一次free释放后,p指向的空间便会还给操作系统。由于没有手动对P置空,此时p依旧存放着p源动态数组起始地址,再次释放便会造成访问权限冲突,导致程序崩溃。所以在free释放空间后,应该手动将指针置空。
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int* p = (int*)malloc(100);
if (NULL == p)
{
return;
}
}
int main()
{
test();
return 0;
}
每一次调用test函数,都会想内存申请100字节空间,但是由于test执行完后,函数栈帧就销毁了。此时在test函数内申请的空间由于没有被标记,这回导致内存的泄露。我们使用free函数释放时,要正确合理的进行释放动态内存。
4.经典试题解析
4.1试题一
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
解析:运行这段代码后,程序会直接崩溃。本题错误为对NULL进行访问操作而导致访问权限冲突。在test函数中,创建字符指针了str。并以参数形式传给GetMemory函数。需要注意的是本次调用为传值调用,对形参部分的修改并不能影响实际的str。此时,程序进入GetMemory函数内部,开辟了100字节的空间并付给了形参p。然而,p开辟的空间随着GetMemory函数的执行结束后被销毁,我们再也无法找到开辟的100字节的空间。此时程序回到test函数中,将"hello world"拷贝到str指向的空间中,并对其进行输出。str指向的是0地址处,所以会造成访问权限的冲突,导致程序崩溃。本题的问题有两个,一、GetMemory函数存在内存泄露问题,二、对NULL指针进行了访问操作。
4.2试题二
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
解析:运行这段代码,屏幕上会输出一段有随机值组成的字符串内容。首先,test函数内部创建了字符指针str并初始化为空指针,将GetMemory函数返回值赋给str。然后,我们进入GetMemory函数内部,创建字符数组p并将数组内容初始化成"hello world"。问题也就出现在这里,由于是以数组的形式初始化。在函数执行结束后,函数会将空间还给操作系统,那么p数组的值便被销毁成随机值。此时虽然在test函数内得到了p数组首元素地址,但是地址处的内容已经被操作系统初始化成随机值了。这里打印的内容便是一串随机值组成的字符串。故本题错误的地方就是GetMemory函数内部是以数组的形式初始化字符串。如果以字符指针的形式来初始化字符串,那么该字符串将存放在内存中的只读常量区内,并不会随着函数栈帧销毁而销毁。
4.3试题三
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
解析:运行这段代码的结果是在屏幕上输出了hello。但是,这并不代表这段代码没有问题。首先Test内部调用GetMemory函数动态开辟内存空间。本次采取的是传址调用,所以,这里成功开辟了100字节空间。然后开心地进行了打印。可惜忘记了对于动态内存的释放,这会导致内存泄漏问题。这里应该free释放开辟的动态内存。
4.4试题四
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
解析:运行这段代码结果是world,这是一个经典的野指针问题。首先,动态申请了100字节空间并将起始地址赋给str指针。将hello字符串拷贝到str指向的空间中。然后,free释放str指针指向的内容。错误就错在没有在free后手动将str指针置为空指针。这导致了str变成野指针,对野指进行访问操作是一个及其危险的行为。所以我们应当在free释放动态内存后,将指针手动置空避免形成野指针。
5.C/C++程序的内存开辟
C/C++程序的内存分配的几个区域:
1、栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
2、堆区:一般由程序员分配如(malloc、calloc等)和释放(free), 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
3、数据段(静态区):存放全局变量、静态数据。程序结束后由系统释放。
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码,常提及的常量字符串就是存放在这里。
6.柔型数组
也许你从未听过这个概念,下面让我简单介绍一下这个概念。该标准定义于C99标准。所以它在一些不兼容C99环境下可能无法使用。
柔型数组:指的是结构体中最后一个成员为未知大小的数组。但是在这个数组前至少还要有一个成员。否则将无法开辟起始的内存空间。
//格式一
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
//格式二
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
两种格式都是柔型数组的定义,具体是用哪一个得具体看编译器。因为在不同编译器下柔型数组的使用还是略有差别的。
6.1柔型数组的特点
#include<stdio.h>
struct S
{
int i;
char ch[];
}s;
int main()
{
printf("%u\n",sizeof(struct s));
return 0;
}
从上面代码可以看到,sizeof计算的结构大小是不包含柔型数组的。所以柔型数组前至少需要有一个结构体成员。包含柔性数组的结构体用动态内存开辟函数申请空间时,所申请的动态空间至少要大于成员体的大小,这样才能适应柔型数组。
6.2柔型数组的使用与优点
//代码1
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
char ch[];
}s;
int main()
{
char* pc=(char*)malloc(sizeof(s.i) + 10 * sizeof(char));
if (NULL == pc)
{
return 1;
}
s.i = 20;
for (int i = 0; i < 10; i++)
{
*(pc + i) = 'a';
}
for (int i = 0; i < 10; i++)
{
printf("%c ", *(pc + i));
}
printf("\n");
free(pc);//释放
pc = NULL;
return 0;
}
//代码2
#include<stdio.h>
#include<stdlib.h>
struct S
{
int i;
char* pc;
}s;
int main()
{
struct S* ps = (struct S*)malloc(sizeof(s));
ps->i = 100;
ps->pc = (char*)malloc(10 * sizeof(char));
int j = 0;
for (j = 0; j < 10; j++)
{
*(ps->pc + j) = 'a';
}
for (j = 0; j < 10; j++)
{
printf("%c ", *(ps->pc + j));
}
free(ps->pc);//释放
s.pc = NULL;
free(ps);//释放
ps = NULL;
return 0;
}
上述代码1和代码2的功能完全一致。但是代码1的优势有以下两点:
1、动态内存管理操作量较少,不容易出错。人难免出错,1次动态内存申请和一次动态内存释放可以有效减少失误概率,也方便掌控。
2、柔性数组可以有效减少内存碎片,使得内存的利用效率更加的高。