【C进阶】动态内存管理
家人们欢迎来到小姜的世界,<<点此>>传送门 这里有详细的关于C/C++/Linux等的解析课程,家人们赶紧冲鸭!!!
客官,码字不易,来个三连支持一下吧!!!关注我不迷路!!!
动态内存管理
- 前言
- 一、为什么存在动态内存分配
- 二、动态内存函数的介绍
- (一)malloc和free
- 1.malloc函数介绍
- 2.free函数总结
- 3.p==NULL效果演示
- (二)calloc
- 1、介绍
- 2、应用
- (三)malloc和calloc比较
- (四)realloc
- 1.介绍
- 2.应用
- 3.realloc和malloc互相转化
- 三、常见的动态内存错误
- (一)对NULL指针的解引用操作
- (二)对动态开辟空间的越界访问
- (三)对非动态开辟内存使用free释放
- (四)使用free释放一块动态开辟内存的一部分
- (五)对同一块动态内存多次释放
- (六)动态开辟内存忘记释放(内存泄漏)
- (七)动态开辟内存提前返回
- 四、经典的笔试题
- (一)题1
- 小知识
- (二)题2(返回栈空间的地址)
- (三)题3
- (四)题4
- 五、C/C++程序的内存开辟
- 六、柔性数组
- (一)什么是柔性数组
- (二)柔型数组的特点
- (三)柔性数组的使用
- (四)柔性数组的优势
- 总结
前言
先罗列一下本章节的重点:
本章重点
为什么存在动态内存分配
动态内存函数的介绍
malloc
free
calloc
realloc
常见的动态内存错误
几个经典的笔试题
柔性数组
那在之前我们介绍了可以进行静态内存管理,是可以开辟一个空间供我们进行存放,可是这开辟的空间过于小或者空间过于大呢?是不是不太靠谱,所以就有了动态内存的概念,当我们运用动态内存的时候,是十分靠谱的,它能够根据数量进行开辟合适的空间,所以,接下来跟着我一起看一看吧!
一、为什么存在动态内存分配
我们已经掌握的内存开辟方式有:
但是上述的开辟空间的方式有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这就需要我们引入动态内存开辟的操作了,如下:
二、动态内存函数的介绍
大家看下图,在cpu内核是这么存放的,我们今天要介绍的是堆区里面存放的动态内存管理:
(一)malloc和free
1.malloc函数介绍
C语言提供了一个动态内存开辟的函数malloc,我们打开MSDN看一看这个函数的介绍:
那我们根据这个介绍来写一下简单的打印1~10的malloc函数吧:
在之前我们如果要打印1~10的数的函数很简单,是先创建一整个空间去打印,而malloc是在堆区申请空间去进行打印:
#include<stdlib.h>
#include<stdio.h>
int main() {
//申请40个字节,用来存放10个整型
int* p = (int*)malloc(40);
if (p == NULL) {
perror("p::malloc");
return 1;
}
//存放1~10
//空间是连续存放的
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;
}
似乎好像是写完了,但大家不要忘记我们开头的那张图,malloc是在堆区进行申请空间的,申请了空间,然后用这块空间,但是你没有还给操作系统,这空间不是浪费了吗,就好比图书馆有一本很热门很好的书,我借去了然后看个几天看完了,一直忘了还,那我一直占有这本书,没有还给人家,是不是就浪费了这本书的价值,但当图书管理员发现这本书咋一直在我这边,强行让我还掉,那我就得被动的还,也就是说这个程序结束了以后,强制性地把空间还给操作系统,那既然这么被强迫,图书管理员还跟我说让我下次注意点,盯着我去还书,那我不是很没面子!?那我就想办法了,我主动去还,我不拖欠书,这不就不会被说了吗?那就引进了一个free函数:
所以我们进行优化,加入free函数:
单单加上free函数看起来没什么问题,但倘若有人去再次进行访问以后是非法访问,就相当于一家图书馆搬地方了,但是在导航上还是显示有的,所以你吭哧吭哧去这个图书馆了,发现这个图书馆里面早就已经空了,里面装了一堆其他的东西,你进去以后就是非法访问了,所以需要在导航中显示这个地方改成其他了才OK~~~
2.free函数总结
简单了解完这串代码,我们归纳总结一下free函数:
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
3.p==NULL效果演示
那再给大家看一下当申请字节过于大的情况:
(二)calloc
1、介绍
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
2、应用
那我们根据介绍简单实现一下吧!
#include<stdio.h>
#include<stdlib.h>
int main() {
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL) {
perror("p::calloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d ", *(p + i));
}
//释放
free(p);
p = NULL;
return 0;
}
(三)malloc和calloc比较
不同点1:malloc是只需要一个传递的参数,而calloc是需要两个传递的参数。
不同点2:如下图片:
malloc优势在于这个函数更加高效,因为它不需要空间初始化这个步骤,calloc存放的不是随机值,直接就是初始化。两者需要根据实际情况进行使用,没有优劣之分!!!
(四)realloc
1.介绍
1.realloc函数的出现让动态内存管理更加灵活。
2.有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
我们简单来实现一下:
当我们做到这里的时候,肯定一个字“莽”,直接就写代码:
看似写的没什么问题,但这里报了两个警告,那我们看一下为什么警告:
取消对NULL指针p+i的引用,这里需要解释一下,堆区是有一定的空间的,开辟的动态空间既然是个连续存放的空间,空间必定会有不够用的时候,那就需要重新开辟一块更大的空间去存放这些值,开辟的方法就是换个命名的指针。所以就有了以下的概念(详情见应用):
2.应用
在有了上面的概念以后,进行增容的操作也是比较复杂的,因为我们需要真正了解realloc函数的实现过程,那就是分为三种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况3:增容失败,返回NULL
如下图:
所以就是说,换个指针去做冤大头,要是增容失败,那这个冤大头指针最后销毁了也好,被浪费了也好,都不管,而p能够完美输出;而如果它增容成功,那就是那个冤大头指针受到p的维护。这个堆区可以比作包租婆,你是p指针,你想在她地界上多上几个房间,而你不确定包租婆肯不肯给你增加地盘,你和包租公说,让包租公和包租婆商量商量能不能去增加房子,包租公就是这个冤大头指针ptr,当他和包租婆商量失败了,它就作为那个冤大头被打了,我不管他,我还是享有我的地盘,但如果我直接和包租婆说增加地盘,那我就会被赶出去,没地址了;当包租公和包租婆商量成功以后,包租公就要回来告诉我,我可以增容了,那我就接受包租公的指令,当我想开辟的空间没有影响到别人,我直接施工开干,当我想要的地盘过于大,我就得申请另开辟一块新的空间去扩容,realloc函数内部是这么做的,我们不需要模拟实现,知道原理即可。
代码如下:
#include<stdio.h>
#include<stdlib.h>
int main() {
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL) {
perror("p::malloc");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 5; i++) {
*(p + i) = 1;
}
//增容,增加5个整型空间
int* ptr = (int*)realloc(p, 10 * sizeof(int));
//解释为什么是开辟了ptr指针:
//第二种情况是续的空间不够了,那只能另开辟一块空间
//而如果硬要头铁返回p的指针的地址,导致返回了NULL
//p指针已经被修改了,找不到原本的位置
if (ptr != NULL) {
p = ptr;//ptr那块区域用p去维护
ptr = NULL;
}
//继续使用空间
for (i = 0; i < 10; i++) {
printf("%d ", *(p + i));
}
//释放空间
free(p);
p = NULL;
return 0;
}
3.realloc和malloc互相转化
当realloc在开头出现时,此时不需要扩容,所以就是NULL空指针,相当于malloc了,如下代码:
#include<stdio.h>
#include<stdlib.h>
int main() {
//int* p = (int*)malloc(10 * sizeof(int));
int* p = (int*)realloc(NULL, 10 * sizeof(int));
if (p == NULL) {
perror("p::realloc");
}
else {
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d ", *(p + i));
}
}
free(p);
p = NULL;
return 0;
}
三、常见的动态内存错误
(一)对NULL指针的解引用操作
上面的代码没有对p进行判断是不是空指针,所以会有警告,而我们加上判断以后就是没有警告了,所以我们需要在每次进行使用的时候加上判断是不是空指针。
(二)对动态开辟空间的越界访问
当我们进行越界访问的时候,发现是出现错误了,编译器直接给dubug了,所以大家在进行动态开辟空间操作的时候不要进行越界访问。
//错误代码:
#include<stdio.h>
#include<stdlib.h>
void test(){
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;
}
int main() {
test();
return 0;
}
(三)对非动态开辟内存使用free释放
当我们写了很多动态内存函数,完了,free永远忘不了了,所以就不管三七二十一,都加了free,在静态的变量处也加了个free,那我们看看吧!
代码继续挂,所以不能在静态内存中用free哦!!!
//错误代码:
#include<stdio.h>
#include<stdlib.h>
void test()
{
int a = 10;
int* p = &a;
free(p);
}
int main() {
test();
return 0;
}
(四)使用free释放一块动态开辟内存的一部分
当这个指针发生移动变化了以后,那这个指针指向的不是整个空间,是指向的其他位置,以及指向这个空间的一部分,你去给它释放了,这是很危险的。
//错误代码:
#include<stdio.h>
#include<stdlib.h>
void test(){
int* p = (int*)malloc(100);
if (p == NULL) {
return 1;
}
int i = 0;
for (i = 0; i < 25; i++) {
*p = i;
p++;
}
//p出循环的时候p指向的是末尾数,不是起始位置
//p只有指向起始位置的时候才需要被释放
free(p);//p不再指向动态内存的起始位置
p = NULL;
}
int main() {
test();
return 0;
}
(五)对同一块动态内存多次释放
释放了一次空间以后,p是个野指针了,没法进行再释放,是很危险的。
//错误代码:
#include<stdio.h>
#include<stdlib.h>
void test() {
int* p = (int*)malloc(100);
if (p == NULL) {
return 1;
}
free(p);
free(p);//重复释放
}
int main() {
test();
return 0;
}
//更改代码:
#include<stdio.h>
#include<stdlib.h>
void test() {
int* p = (int*)malloc(100);
if (p == NULL) {
return 1;
}
free(p);
p = NULL;
free(p);//重复释放
}
int main() {
test();
return 0;
}
(六)动态开辟内存忘记释放(内存泄漏)
忘记释放空间,看似是个小事,但是这块空间就一直存在你释放不了它,它就一直占用,只有把计算机重启才能够释放掉,所以我们在进行使用的时候,要进行释放。
//错误代码:
#include<stdio.h>
#include<stdlib.h>
void test()
{
int* p = (int*)malloc(100);
if (p == NULL) {
return 1;
}
//使用...
}
int main()
{
test();
return 0;
}
//正确代码:
//函数内部进行了malloc操作,返回了malloc开辟的空间的起始地址
//记得释放
#include<stdio.h>
#include<stdlib.h>
int* test()
{
int* p = (int*)malloc(100);
if (NULL == p){
return 1;
}
return p;
}
int main()
{
int* ptr = test();
free(ptr);
ptr = NULL;
return 0;
}
(七)动态开辟内存提前返回
当成立满足的时候,test1函数直接返回了,即使这个malloc和free同时出现了,也是会有bug的。
所以:
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:
动态开辟的空间一定要释放,并且正确释放 。
四、经典的笔试题
(一)题1
大家觉得下面这串代码出现的问题是什么呢?
大家可能看这个有点懵逼,那其实有两个问题:
第一个问题是str无法接收hello world而导致系统的崩溃。
第二个问题就是内存泄露。
我们一一解释吧!!!
这串代码本来的意思是将str传参到GetMemory函数中,然后利用在GetMemory函数中开辟的空间进行复制“Hello World"字符串,但是我们知道,形参只是实参的一份临时拷贝,p有自己独立的空间,当我们传参到GetMemory这个函数的时候,在内部申请了空间以后,只是p指针指向开辟的动态内存空间,也就是p存放了新开辟动态内存空间的地址,而真正的str指针并没有改变,依旧是NULL,当strcpy进行拷贝的时候,形参被非法访问,所以会发生错误,写入位置为空指针,无法复制字符串。
第二个问题是在GetMemory函数内部,动态申请了内存,但是没有释放,会内存泄露。
正确代码:
取地址传参,接收用二级指针,传址操作。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main() {
Test();
return 0;
}
//或者:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main() {
Test();
return 0;
}
小知识
#include<stdio.h>
int main() {
char* p = "hehe\n";
printf("hehe\n");
printf(p);
return 0;
}
大家可能对这串代码有点不理解,这个p是char*类型的指针,存的是"hehe\n"的首元素地址,所以printf§;就是从首地址直接打印了。
(二)题2(返回栈空间的地址)
大家先看看下面的代码,感觉是打印的是什么!?
这怎么是烫烫烫了,哎这个好熟悉,出现这个烫烫烫不就是内存中的栈区有点问题吗???
那我们画图分析一下:
我们根据代码一步一步来分析,当GetMemory函数进行操作的时候,我们看,return p;返回给str了,str记住了这个p的地址,看样子似乎没什么问题,但是当GetMemory函数销毁了以后呢???这个"hello world"字符串已经销毁了,你str找到地址以后里面是什么内容啊!?我们不知道的,是属于非法访问。就举一个例子:我今天去酒店租了一间房,我告诉张三啊,我的房间在豪大大酒店302房间,你明天记得来找我玩游戏,可是我第二天跑路了,回家去了,这个房间我也退了,第二天张三吭哧吭哧跑到302房间发现只有清洁的阿姨,好家伙,我已经回家了。也就是说这串空间早就已经销毁了,也有可能被人家占用了。
//修改
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void){
static char p[] = "hello world";
return p;
}
void Test(void){
char* str = NULL;
str = GetMemory();
printf(str);
}
int main() {
Test();
return 0;
}
//第二种:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void){
char* p = "hello world";//常量字符串,字符串在内存中本身就存在,*p只不过也是去访问而已,它也是存的是地址,传给str以后,str也是去找地址找到字符串并打印
return p;
}
void Test(void){
char* str = NULL;
str = GetMemory();
printf(str);
}
int main() {
Test();
return 0;
}
(三)题3
大家看看这串代码的问题出现在哪!?看似没有问题,但好像少了个free();这岂不是内存泄露吗,那我们发现问题了就解决问题:
正确代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p, int num){
*p = (char*)malloc(num);
}
void Test(void){
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = NULL;
}
int main() {
Test();
return 0;
}
(四)题4
相信大家第一眼就看出来了这个问题了,那就是提前释放空间了,当我们提前释放空间了以后这个空间就不被str所维护了,就有可能存放别的值了,但要记住的是这个空间被释放以后str并没有变成NULL,而是变为野指针了,所以当下面操作进行以后就是非法访问了,是很危险的,访问的是不确定的地方。
改造:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void Test(void)
{
char* str = (char*)malloc(100);
if (str == NULL) {
return 1;
}
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main() {
Test();
return 0;
}
五、C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
- 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
- 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
- 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
- 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
关于static关键字修饰局部变量的例子:
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁。
所以生命周期变长。
六、柔性数组
(一)什么是柔性数组
柔性数组(flexible array)在C99 之后出现的,在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
(二)柔型数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。
2.sizeof 返回的这种结构大小不包括柔性数组的内存。
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
(三)柔性数组的使用
大家看代码:
#include<stdio.h>
#include<stdlib.h>
struct S {
int n;
char arr[];//数组大小是未知的 - 柔型数组成员
};
int main() {
//printf("%d\n", sizeof(struct S));
struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(char));//开辟动态内存需要比柔型数组实际内存大
ps->n = 100;
int i = 0;
//使用
for (i = 0; i < 10; i++) {
ps->arr[i] = '1';
}
//打印
for (i = 0; i < 10; i++) {
printf("%c\n", ps->arr[i]);
}
//增容
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(char));
if (ptr != NULL) {
ps = ptr;
}
else {
perror("ps::realloc");
return 1;
}
//使用
int j = 0;
for (j = 10; j < 20; j++) {
ps->arr[j] = 'b';
}
//打印
for (j = 10; j < 20; j++) {
printf("%c\n", ps->arr[j]);
}
//释放
free(ps);
ps = NULL;
return 0;
}
我们知道的是可以用动态内存去开辟一块较大的空间,要是不够就往后继续增容。
(四)柔性数组的优势
既然说到优势,那就需要进行对比,我们写一串代码,是在结构体内用的字符指针:
#include<stdio.h>
#include<stdlib.h>
struct S {
int a;
char* arr;
};
int main() {
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL) {
perror("malloc->ps");
return 1;
}
ps->a = 100;
//为arr开辟10个char
ps->arr = (char*)malloc(10 * sizeof(char));
if (ps->arr == NULL) {
perror("malloc->arr");
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++) {
ps->arr[i] = 'w';
}
//打印
for (i = 0; i < 10; i++) {
printf("%c\n", ps->arr[i]);
}
//增容
char* ptr = (char*)realloc(ps->arr, 20 * sizeof(char));
if (ptr != NULL) {
ps->arr = ptr;
}
else {
perror("realloc->ptr");
return 1;
}
//使用
int j = 0;
for (j = 10; j < 20; j++) {
ps->arr[j] = 'o';
}
//打印
for (j = 10; j < 20; j++) {
printf("%c\n", ps->arr[j]);
}
//释放
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
此图是解释开辟空间:(先在结构体中开辟一块动态内存空间,再将arr指针指向另一块开辟的动态内存空间)
大家将这串代码与前面柔型数组使用进行对比:
我们可以看以下几点:
那我们归纳一下柔型数组的优势:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。总的来讲,就是释放次数多很不方便。
第二个好处是:这样有利于访问速度
连续的内存**(开辟malloc次数少)**有益于提高访问速度,也有益于减少内存碎片。
总结
动态内存开辟实在是太重要了,如果我们在以后的写代码过程中使用了动态内存开辟,那是很好的一种方法,因为节省空间,我需要多少空间你就给我开辟多少空间,我们只需要申请空间即可。
客官,码字不易,来个三连支持一下吧!!!关注我不迷路!!!