目录
一.动态内存管理的存在原因:
动态内存管理让我们得以自己来维护自己内存空间的大小(即申请空间大小由自己来定)
首先,我们需要知道在计算机中,数据是连续存放的,即存放的地址是连续的;比如数组,假设第一个元素放在地址1上,那么第二个元素就放在了地址2上,以此类推……。而需要开辟的空间时大时小,那么具体类型申请内存(如int型、double型、数组等等)时,会有申请空间小于需要空间或大于需要空间的情况发生,此时就出现了动态内存管理来解决这个问题
内存被划分为了三个区域,分别是栈区、堆区和静态区
栈区主要存放局部变量、形式参数等临时开辟的数据,在此申请的空间,大小无法随意调整,作用域是函数内部
静态区主要存放全局变量、静态变量,静态变量作用域是整个文件,全局变量作用域是整个可执行程序
堆区主要是用malloc()、calloc()、realloc()和free()来进行内存开辟与释放,空间大小可以进行调整在,整个文件中,只有当程序结束了动态内存才会自动销毁、释放和回收
二.动态内存函数的介绍:
动态内存函数共有4种函数:
1.malloc
以下是cplusplus网站对malloc函数的解释
由于malloc函数开辟空间以后,使用者是要使用整型数据还是浮点型数据malloc函数无法判断,因此返回的是无类型指针,使用者在后续使用中需要强制转换成自己需要的类型
有时也会出现malloc函数开辟失败(开辟空间过大)的情况,此时返回的是空指针(NULL)
malloc函数使用前需要引用头文件<stdlib.h>
相关函数(了解即可):strerror()函数和errno
strerror(errno)是将错误代码的错误原因转换成错误信息打印出来,使用前需要引用<string.h>、<errno.h>两个头文件
如下代码便能实现将错误信息打印出来的功能
printf("%s", strerror(errno));
当动态内存开辟失败时,需要在主函数里用 return 1 这条语句作为结束标志
return 0 和 return 1 实际上都是可以的,但是为了更好的区分,人们把 return 0 看作是正常返回,把 return 1 看作是异常返回
下面是开辟成功和开辟失败的两种代码演示:
1.成功版本:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i; //指针用法与数组相同,后续会详细介绍
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
return 0;
}
输出结果:
0123456789
2.失败版本:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
int*p = (int*)malloc(INT_MAX); //INT_MAX叫做整型最大,代表了 2147483647 这个整数
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
return 0;
}
输出结果:
Not Enough Space
//笔者只有在X86环境下才会出现内存不够
对于 0 这个参数,malloc(0)的结果是未被标准化的,因此malloc(0)的结果取决于编译器
2.free
在讲解free()函数以前,首先需要知道内存泄漏的概念
根据知乎上的一个定义:
内存泄漏是指你向系统申请分配内存进行使用,可是使用完了以后却不归还,结果你申请到的那块内存你自己也不能再访问(也就是你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序
联想记忆:
我借给小明一本书,小明放在家里不看,我让他还回来他又不肯还,最终导致了我想看却看不上,而小明又不看。(“我”指的是电脑,“小明”指的是代码编写者,“书”指的是内存空间)
而free()函数就是专门用来释放和回收内存空间的一种函数,然而在使用完free()函数以后,指针所指向的地址并没有被消除,因为free()函数只会释放内存空间,不会消除指针所指向的地址。因此在执行完free语句后,指针就变成了一个野指针
例:假设现在有一个指针*p,且*p所指向的地址为0x011e9940,在执行完free(p)这一条语句后,*p所指向的地址依然为0x011e9940,此时访问*p,*p就作为一个野指针存在。因此需要在执行完free语句后加上p = NULL ,让指针p变成一个空指针
因此如果要对*p进行内存释放操作,代码如下:
free(p);
p = NULL;
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i; //指针用法与数组相同,后续会详细介绍
}
for (i = 0; i < 10; i++)
{
printf("%d", *(p + i));
}
return 0;
}
对于讲解malloc()函数时所出现的示范代码,没有free()函数会不会造成内存泄漏?
答案是不会,没有free()函数并不是说内存空间就不回收了,当程序退出时,系统会自动回收内存空间
free()函数注意事项:
1.如果函数参数指向的空间不是动态开辟的,那free函数的行为是未定义的
2.如果函数参数是NULL指针,则函数什么事情也不做
3.free声明在<stdlib.h>文件中
3.calloc
以下是cplusplus网站对calloc函数的解释
size_t num:要开辟几个元素
size_t size:每个元素的字节大小
特殊点:calloc会在返回值前,将calloc函数所操作的空间初始化
例如开辟了10个整型空间赋给了指针*p,那么此时直接输出p得到的结果为
0 0 0 0 0 0 0 0 0 0
除此以外,calloc函数与malloc函数并无二异
4.realloc
以下是cplusplus网站对realloc函数的解释
void* ptr:指向一个先前由malloc、calloc或者realloc开辟的内存空间
size_t size:希望调整成多大的空间大小
realloc函数的特点:realloc(NULL, 40) = malloc(40)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
int*p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
//0123456789
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//扩容
realloc(p, 80); //*p所指向的空间大小从40个字节修改为了80个字节
return 0;
}
realloc函数的工作原理:
realloc函数开辟新空间共有2种情况
1.对于realloc所指向的需要修改的空间,开辟了新的空间以后会影响其他已申请使用的空间
注:p为指针*p,40个字节红色框为*p指向的空间,绿色框为已申请使用的空间,蓝色框为新开辟的空间,绿色数字80意为需要通过malloc函数将*p指向的空间大小增至80字节
因此此时realloc函数会自动调整扩容的位置,完成扩容的需求
即realloc函数先在内存中开辟一个80字节的空间,然后将原本存放在*p所指向空间的内容复制过来,最后释放回收原本*p所指向的内存空间
2.对于realloc所指向的需要修改的空间,开辟了新的空间以后并未影响其他已申请使用的空间,此时realloc函数返回的首地址依旧是*p所指向的地址,即直接在原内存空间后扩容
注:p为指针*p,40byte红框为*p所指向的内存空间,绿色部分是新开辟的40字节空间,其余红色小框是其他已申请使用的空间
由上述的情况1我们可以得知,realloc函数返回的地址不一定是原地址,因此realloc函数再扩容以后不能使用原指针接收,要创建一个新指针接收;并且,当realloc函数扩容以后的内存大小过大,会导致realloc函数扩容失败,返回NULL指针
因此realloc函数的正确使用方式如下:
//扩容
int* ptr = realloc(p, 80); //*p所指向的空间大小从40个字节修改为了80个字节
if (ptr != NULL)
{
p = ptr;
}
…… //使用
free(p);
p = NULL;
return 0;
注:上述代码中,当p指针释放回收以后,ptr也一同释放回收了,因此无需再对ptr进行释放回收操作;为了让ptr不在指向*p所指向的空间,可以在p = ptr语句后加上一句ptr = NULL,使其成为一个空指针
三.常见的动态内存错误汇总:
1.对NULL指针的解引用操做
代码示例:
int* p = (int*)malloc(40);
//……(进行一系列操做使得*p变成了一个空指针)
*p = 20;
上述代码对空指针进行了解引用操做,所以程序会报错;因此需要在对*p进行解引用操作前,加上一条语句:
if (p == NULL)
{
return 1;
}
2.对开辟空间的越界访问
代码示例:
int*p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
*(p + i) = i;
}
上述代码中,*p指向的空间只有40个字节的大小,总共可以存入10个整型数据;但是本例在for语句循环中,总共循环了11次,即存放了11个整型数据,超过了开辟的空间大小。因此出现了开辟空间的越界访问
3.对非动态开辟内存使用free释放
代码示例:
int main()
{
int a = 10;
int* p = &a;
//……
free(p);
p = NULL;
return 0;
}
只有动态开辟的内存才能使用free语句!!!要不然会报错
4.使用free释放动态开辟内存的一部分(有时会完全不释放回收)
代码示例:
int*p = (int*)malloc(40);
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(p);
p = NULL;
上述代码中,*p在执行完for语句以后,就已经指向了原开辟空间以外的地址,此时free语句不进行任何操做;如果*p在执行完for语句后,指向了开辟空间的当中位置,则free语句只对后一半的空间进行释放回收操做
因此,在使用free语句进行释放操作前,应确保*p一直指向初始地址
5.对同一块动态内存多次释放
代码示例:
int main()
{
int* p = (int*)malloc(40);
//……
free(p);
//……
free(p);
return 0;
}
上述代码对*p指向的地址进行两次释放回收,因此程序在运行时会报错
可以如下修改:
int main()
{
int* p = (int*)malloc(40);
//……
free(p);
p = NULL; //*p重新开辟一个新的空间
//……
free(p);
return 0;
}
6.动态开辟内存未完成释放(内存泄漏)
代码示例:
void test()
{
int* p = (int*)malloc(100);
//……
int flag = 0;
scanf("%d", &flag);
if (flag == 5)
{
return;
}
free(p);
p = NULL;
}
int main()
{
test();
//……
return 0;
}
上述代码中,当用户输入5时,free(p)与p = NULL两条语句就直接跳过了,此时造成了内存泄漏
注意事项:
1.p(NULL) = (char*)malloc(100) 是在创建一个空间赋给空指针p,而不是在对空指针p进行解引用
2.动态开辟的内存出了某个函数依然存在,但是形式参数(指针类型)自动销毁
3.动态开辟内存赋给指针以外的操作,大多可以叫做指针的解引用
例如:*p = 20 strcpy(*p, "hello world")
4.字符串的打印可以直接是printf(某个字符串变量)
假设str = "hello world",那么要想打印hello world就直接printf(str)即可
5.当我们返回栈区空间的地址时,由于栈区空间存放的都是临时变量,因此很容易就被其他内容所覆盖;也有一定概率会出现返回后的临时变量还能用的情况,但只需几句代码就会被覆盖取代
四.柔性数组介绍
概念介绍:结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员
柔性数组的初始化方式:a[ ] 或者 a[0](具体用哪个看编译器)
柔性数组的特点:
1.结构体中的柔性数组成员前必须至少一个其他成员
2.sizeof返回的这种结构体大小不包括柔性数组的内存
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
4.一个结构体里,柔性数组只能出现一个
柔性数组的使用:
1.柔性数组的内存开辟:
struct S
{
int n;
int arr[];
};
int main()
{
//……
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
return 1;
}
//……
return 0;
}
上述代码,ps指向的空间先是存在了一个整型变量,占据4字节空间;然后再加上40字节的内存空间,作为柔性数组的使用;同时,ps指向的依旧是这44个字节的首地址,当44字节不够时,可以使用realloc函数更改大小
realloc函数的使用如下:
struct S
{
int n;
int arr[];
};
int main()
{
//……
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
return 1;
}
//……
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
if (ptr != NULL)
{
ps = ptr;
}
//……
return 0;
}
即使是结构体中的柔性数组,依然需要释放回收
因此完整代码如下:
int main()
{
//……
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
return 1;
}
//……
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
if (ptr != NULL)
{
ps = ptr;
}
//……
free(ps);
ps = NULL;
return 0;
}
2.*arr 的柔性数组开辟方法:
*arr的柔性数组开辟具体含义如下
struct S
{
int n;
int* arr;
};
对于这种创建方式,即构造了一个结构体,其中存放了整型变量n,以及一个整型指针变量arr,这两个变量可以看作是不相干的,详见下图
而对于结构体中出现了*arr,可以看成是一个数组首地址的指针变量,我们可以通过在mian函数中,加上一条 ps ->arr = (int*)malloc(40) ,等同于开辟了40字节的柔性数组
然后由于*arr与n是不相干的,所以malloc函数应该使用两次,一次是指向结构体S整体的指针,使用malloc函数,开辟 struct S 字节大小的空间;然后指针中的指针,即*arr,对其再次使用malloc函数,开辟所需要的柔性数组的内存空间大小。第二次开辟的内存空间与第一次开辟的内存空间通过上图不难看出,应该是没有联系的
最后,就是对两个指针的内存释放
首先我们需要知道*ps包含了*arr,*ps指向整个结构体开辟的空间,*arr只是柔性数组的开辟空间;因此,应该先对*arr进行内存释放,再对*ps进行内存释放,先后关系不能颠倒,具体代码如下:
free(ps->arr);
free(ps);
ps = NULL;
*arr被包含在了*ps里,因此在将*ps变成空指针以后,arr也就变成了空指针的一部分,因此不需要再对其进行这一步操作
扩容时依旧使用malloc函数,假设要扩容成80字节大小,依旧用int* ptr接收,则代码如下
int* ptr = (int*)realloc(ps->arr, 80);
该方法的问题:malloc函数用了较多,断断续续访问会降低访问速度,并会导致大量内存碎片;同时,也更容易导致内存泄漏,代码编写者的思考量增加。因此,笔者在此推荐使用方法1开辟柔性数组空间更加好。