目录
秃头侠们好呀,今天来聊聊动态内存管理
本章重点
- 为什么存在动态内存分配
- 动态内存函数的介绍
malloc
calloc
realloc
free - 常见的动态内存错误
- 柔性数组
为什么存在动态内存分配
我们以往学过的内存开辟无非是
int a=10;//在栈上开辟4个字节
char arr[10]={0};//在栈上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1、空间开辟的大小是固定的
2、数组在声明时必须指定数组长度,它所需要的内存在编译的时候分配
但是对于空间的需求,我们不仅仅满足上述情况。有时我们需要的空间大小在程序运行时才能知道,那数组在编译时开辟空间的方式就不能满足了,这时就只能试试动态内存开辟了。
动态内存函数的介绍
malloc / free
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,返回一个指向开辟好空间的指针
- 如果开辟失败,返回一个NULL,所以malloc的返回值要做检查,看是否开辟成功
- 返回值是void* 所以malloc开辟的空间不知道类型,由开辟者自己决定
- 如果size为0,则malloc行为的标准是未定义的,取决于编译器
void free (void* ptr);
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的
- free用来释放动态开辟的内存
- 如果ptr指向的空间不是动态开辟的,则free的行为是未定义的
- 如果ptr是NULL指针,则啥也不做
- 如果动态内存不释放会造成内存泄漏(后面具体说)
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
//开辟失败
if (p == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//开辟成功
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
printf("%d ", p[i]);
}
printf("\n");
free(p);
p = NULL;
return 0;
}
1、如果你开辟的空间过大有可能会开辟失败,所以必须检查。
2、开辟成功了,因为是连续的空间,所以p相当于一个数组。
3、最后记得释放动态开辟的空间,你拿的就要还回去,防止内存泄漏。
4、最后要把该指针置空NULL,有什么必要?
指针被free后,指针指向的还是原来的区域,但是这片区域已经不归自己使用了,这片区域可以被别人用,被别人覆盖了,所以你已经变成野指针了,如果你不置为空,你去访问这个地方了,就造成非法访问了,会有不安全因素。且可以防止对一个已经释放的指针多次释放,造成程序崩溃,但我们可以对NULL指针多次释放。
举个例子:
比如你有一个女朋友,有一天你和她分手啦,这里相当于free,她已经不属于你了,你们之间已经没有关系了,她现在可以成为别人的女友了,但是你脑子里还记者她,还记着她的联系方式,这里相当于p指针还指向原来的内容,这样对她是不好的,因为你还能根据联系方式去打扰她的生活,置为NULL,就是清除你对她的记忆,喝下忘情水。(当然祝大家都幸福哦)
calloc
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
realloc
void* realloc (void* ptr, size_t size);
- 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整
- ptr是要调整的内存地址
- size是调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
- realloc在调整内存空间的时候有两种情况
情况1:原有空间之后有足够大的空间
要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化
情况2:原有空间之后没有足够大的空间
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,前面的数据会拷贝下来,这样函数返回的是一个新的内存地址,之前的realloc会自己free掉
常见的动态内存错误
1、对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(4);
*p = 10;//如果p的值是NULL,就会有问题
free(p);
p=NULL;
}
所以使用动态内存分配,需要判断是否开辟成功,如果成功再使用,否则不能使用,返回NULL,但是不能对NULL解引用
2、对动态开辟空间的越界访问
int*p=(int*)malloc(200);
for(int i=0;i<80;i++)
{
//....
}
总共申请了200÷4=50个元素,而你的for循环的判断条件到80了,所以当大于50的时候,会出现越界访问。
3、对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//可以吗?
}
显然是不可以的,因为a是在栈上开辟的空间,不是堆上
free只能释放堆上动态开辟的空间!
4、使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//可以吗?
}
当然是不可以的,因为自增后,p指向的位置改变了,而free释放必须是释放全部的动态开辟的空间(起始位置),不能是部分。
5、对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
不可以对一个同一块动态内存重复释放,但是可以这样
free(p);
p=NULL;
free(p);
p=NULL;
6、动态开辟内存忘记释放(内存泄漏)
首先在堆上申请的空间有两种回收方式
1、free
2、程序退出时,申请空间自动回收
如果不对开辟的空间进行释放,则会造成内存泄漏,你的电脑就会越来越卡
所以当使用完动态开辟的内存,一定要记得释放
看几个笔试题
题一:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
该程序结果是什么?为什么会有这样的结果?
结果是:程序崩溃,什么也不打印
原因:
str传给p的时,是值传递,p是str的临时拷贝,当malloc开辟空间的起始位置放到p中时,str并没有改变还是NULL;
当str是NULL,strcpy要把hello world拷贝到str指向的空间时,因为str是NULL,所以不知道拷到哪里,程序崩溃。
那我们应该怎么更改呢?
很简单,我们应该传str地址,这样*p就是str
题二:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
结果是啥?
为啥是随机值?
这里需要一点函数栈帧的知识
函数在栈上开辟栈帧,函数返回,销毁栈帧,当栈帧销毁后,里面的东西都还给操作系统了,return p,这里的返回值p是通过寄存器(eax)传回来的,把p的地址通过寄存器赋给str,虽然str拿到了p的地址,但是栈帧已经销毁,p地址指向的空间已经不属于p了,这时候p其实已经算是野指针了,如果你再去访问这个空间,就造成了非法访问了。如果空间内容没有被覆盖,还有可能打印出来,如果被别人使用了,就打印随机值了。
题目三:
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;
}
这个代码结果是什么?有什么问题?
看似打印出来了,其实已经出现了问题
char* str = (char*)malloc(100);
strcpy(str, "hello");
这两句没有问题,str指向malloc出来的空间,strcpy把hello拷贝到这片空间
free(str);
if (str != NULL)
{
strcpy(str, “world”);
printf(str);
}
str被free,则malloc出来的空间还给操作系统,不属于自己了,但是str指向的地址没有变,只是变成野指针了,if判断进去,strcpy(str, “world”);这里就出现问题了,因为这片空间已经不属于自己了,你又使用了,所以造成非法访问了,虽然最后打印出来了了,但是早已出错了。
我们应该在free完之后就应该把str置空,养成好习惯,才不容易出错
C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
现在我们是否就理解了static关键字修饰局部变量的例子了,为啥生命周期会改变
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁 所以生命周期变长
柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
例如:
struct S
{
int i;
int a[0];//柔性数组成员
};
有些编译器会报错,无法编译可以改成下面
struct S
{
int i;
int a[];//柔性数组成员
};
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少有一个其他成员
- sizeof 返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S
{
int i;
int a[];//柔性数组成员
};
printf("%d\n",sizeof(struct S));//结果是4
柔性数组的使用
//代码1
struct S
{
int i;
int a[];
};
int i = 0;
struct S*ps= (struct S*)malloc(sizeof(struct S)+100*sizeof(int));
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
p=NULL;
这样柔性数组成员a,相当于获得了100个整型元素的连续空间
柔性数组的优势
//代码2
struct S
{
int i;
int *a;
};
struct S*p = (struct S*)malloc(sizeof(struct S));
p->i = 100;
p->a = (int *)malloc(100*sizeof(int));
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p->a);
p->a = NULL;
free(p);
p = NULL;
上述 代码1 和 代码2 可以完成同样的功能,但是谁更好呢?代码一更好!
原因:
1、方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了两次内存分配,并把整个结构体返回给用户。用户free一次可以释放结构体,但是他不知道结构体里成员也是分配出来的,也需要free释放,我们不能指望用户自己发现这个事。如果我们用代码一这种,结构体内存和成员内存一起只分配一次,返回给用户一个指针,那么用户free一次就可以释放所有内存。
2、有利于访问效率
连续的内存有利于提高访问速度,且减少内存碎片(这点效率没有提升很高,都要用偏移量的加法来寻址)
这期就到这里啦,感谢阅读,我们下期再见
如有错 欢迎提出一起交流
关注周周汪哦
关注三连么么么哒