C语言动态内存分配详解
文章目录
- 前言
- 一、为什么存在动态内存分配
- 1、已掌握的内存开辟方式
- 2、上述开辟空间方式的特点
- 3、为什么存在动态内存分配
- 二、动态内存函数的介绍
- 1、malloc
- 2、free
- 3、calloc
- 4、realloc
- 三、常见的动态内存错误
- 1、对NULL指针的解引用操作
- 2、对动态开辟内存的越界访问
- 3、对非动态开辟内存使用free释放
- 4、使用free释放动态开辟内存的一部分
- 5、对同一块动态内存的多次释放
- 6、动态开辟内存忘记释放(内存泄漏)
- 四、几个经典的笔试题
- 1、题目1
- 2、题目2
- 3、题目3
- 4、题目4
- 总结
前言
自从前两次博客写完以后,感觉对于我本人来说,收获很大,尤其是将学过的知识再度温习一遍,感觉基础扎实了很多,所以就养成了一个习惯,每学完一个模块,都会写一篇博客,不仅仅是写个我自己的,也是想通过这篇博客,与大家分享一些我的见解。本来周四就学完了动态内存分配,但是周末博主去玩了,于是忘记了写博客,现在加班奉上。
一、为什么存在动态内存分配
1、已掌握的内存开辟方式
在C语言中,我们将内存分为了4个区间:
代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store)。
为了方便大家理解,图解如下:
通过之前的学习,我们了解了一些内存的使用方法:
(1)创建一个变量
当我们想要使用单一变量的时候,我们可以通过创建一个变量,来使用内存。
int a = 10;//局部变量 - 栈区
int g_a = 10;//全局变量 - 静态区
(2)创建一个数组
当我们需要使用多个相同类型变量的时候,我们可以通过创建一个数组,来使用内存。
int arr[10];//局部变量 - 栈区
int g_arr[10];//全局变量 - 静态区
2、上述开辟空间方式的特点
以上两种使用内存的方式是我们学过的,也是常用的,但是在某些情况下,仅仅有这两种方法是不足的。
例如:我们需要创建一个数组来存放一个班级的学生信息的时候。
我们在创建这个arr数组的时候,当我们直接给定数组的长度arr[50]的时候,这样是很简单,但是这样合理吗?
例1:
#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{struct s arr[50];// 50个struct s 类型的数据// 30 :不够// 60 :浪费return 0;
}
假设这个班级只有30个人,那么我们是不是就浪费了一部分的空间;假设这个班级有60个人,那么我们给定的50又不够。所以说这里给定多少都是不合理的。
这里有人又会说了,很简单啊:要多少给多少就好了嘛!就像这样
例2:
#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{int n = 0;scanf_s("%d", &n);struct s arr[n];//错误(活动) E0028 表达式必须含有常量值return 0;
}
运行结果为:报错
事实证明,我们的想法很美妙,但是现实却很残酷:
这里的错误名称叫:表达式必须含有常量值;
说明对于 struct s arr[n]; 这里的n是变量,那就不行了。
这里延伸一下:例2这种代码的写法叫做变长数组。
对于变长数组这种写法目前仅对于C99是可运行通过的。
总结:上述开辟空间方式的特点
(1)开辟空间的大小是固定的;
(2)数组在声明的时候,必须制定数组的长度,它所需的内存在编译时分配。
3、为什么存在动态内存分配
我们对于内存开辟空间的需求,不仅仅局限于这些方式,有时候我们需要的空间大小在程序运行的时候才能知道,这时上述方式就不能达成目的了,所以动态内存分配就应运而生了。
二、动态内存函数的介绍
1、malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
(1)如果开辟成功,则返回一个指向开辟好空间的指针;
(2)如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
(3)返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定;
(4)如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。
举一个例子
例3:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误 C2440 “初始化” : 无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
在例3中,我们如果采用 “ int* p = malloc(10 * sizeof(int)); ” 的方式来开辟空间,在大部分检测严格的编译器中,会报错,这是因为变量类型的不同,从这里我们也可以看出,malloc 开辟空间的返回值是 void* 类型;
上面我们也提到了:如果 malloc 开辟失败,则返回一个NULL指针,所以malloc 的返回值一定要做检查,所以我们用了一种特殊的方式来打印错误原因——“ printf("%s\n", strerror(errno)); ” ,这样如果开辟失败,编译器就不会报错了,而是在运行后将错误的原因打印出来。
易错提示:
因为我们计算机的内存也是有限的,所以我们不能为所欲为的开辟空间,当我们需要开辟的空间不够时,打印错误就会出现“Not enough space”。
2、free
紧接上文,我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还,我们在前边向系统借用了这么多内存,当我们用完以后,我们应该把这块内存还给系统,那么怎么还呢?这里就需要用到我们的 free 函数了。
C语言为我们提供了另外一个函数,专门用来做动态内存的释放和回收的:
void free(void *ptr)
free函数用来释放动态开辟的内存:
(1)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;
(2)如果参数 ptr 是NULL指针,则函数什么操作都不进行。
先来看一个例子
例4:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误 C2440 “初始化” : 无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
对比例3和例4,例4只是多了两行代码:
free( p );
p = NULL;
有人会疑惑了,例3和例4的运行结果明明是一样的啊,那为什么我们还要多此一举,加上这两行代码呢?
没错看上去运行结果是一样的,但这仅仅只是对于我们代码量很少的情况下,我们申请的内存够用了,所以目的达到了;但是假设我们要做一项任务量巨大的工程的时候,我们只借不还,系统的内存在不断减少,我们还能继续写程序吗?所以应该从现在养成一个习惯,申请的内存,用完以后一定要进行 free()操作。
这里有人又有疑问了,那我们用完了内存,释放了不就好了吗?为什么还要把这个指针p置为空指针呢?
其实当我们free(p)操作结束以后,这块空间是释放了,但是p的值并没有改变,如果有人找到了这个p,进行了破坏,我们的程序就有可能出问题,所以我们不妨主动将p置为空指针,让有非分之想的人断绝这些念想。
光说不练,是学习编程语言的大忌,我们趁热打铁,来做一道练习题:
正确答案为:
例5:
#include "string.h"
#include <stdio.h>
#include<stdlib.h>
int main()
{char* src="hello,world"; char* dest=NULL;int len=strlen(src);dest=(char*)malloc(len+1);// 要为\0分配空间char* d=dest;char* s=src+len-1;// 指向最后一个字符while(len--!=0){ *(d++)=*(s--);// 注意不要丢掉*号*d ='\0';// 字符串的结尾不要忘记'\0'} printf("%s",dest);free(dest);// 使用完要释放空间,避免内存泄露dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略return 0;
}
3、calloc
C语言还提供了一个函数叫 calloc ,calloc 函数也用来动态内存分配:
void* calloc(size_t num,size_t size)
(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节初始化为0;
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0.
举个例子:
例6:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{int* p = (int*)calloc(10 , sizeof(int));// calloc -> #include<stdlib.h>if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}
运行结果为:
0 0 0 0 0 0 0 0 0 0
由此可见:calloc 函数会将动态开辟空间的每个字节初始化为0
4、realloc
回归今天的核心问题,如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?
C语言同样为我们提供了一个函数叫 realloc ,realloc 函数可以让动态内存管理更加灵活:
void* realloc(void* ptr, size_t size);
(1)ptr 是要调整的内存地址;
(2)size 是调整后的新大小;
(3)返回值为调整之后的内存起始位置;
举个例子:
例7:
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);for (int i = 0; i < 5; i++){*(p + i) = i;}for (int i = 0; i < 5; i++){printf("%d ", *(p + i));}int* p2 = (int*)realloc(p, 40);for (int j = 5; j < 10; j++){*(p + j) = j;}for (int j = 5; j < 10; j++){printf("%d ", *(p + j));}free(p);p = NULL;return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
(4)这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间;
(5)realloc 在调整内存空间的过程中存在两种情况
①原有空间之后有足够大的空间
此时,直接在原有内存之后追加空间,原来空间的数据不发生变化。
②原有空间之后没有足够大的空间
在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
图解如下:
三、常见的动态内存错误
1、对NULL指针的解引用操作
例8:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 1.对NULL指针解引用操作int* p = (int*)malloc(40);// 万一malloc失败了,p就被赋值为NULL// 不安全// 记得判断p是否为空int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}
对于例8,如果 malloc 开辟空间失败,此时 p 被赋值为NULL,而下面对于空指针进行操作, *(p + i) 始终为非法地址,我们的操作始终为非法操作,所以我们一定要在使用前记得判断p是否为空。
2、对动态开辟内存的越界访问
例9:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 2.对动态开辟内存的越界访问int* p = (int*)malloc(40);// 10个int -> 0-9if (p == NULL){return 0;}int i = 0;// 越界for (i = 0; i <= 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}
对于例9,我们使用 malloc 向系统申请了 10个int 类型,但是我们在后边访问了 11个int 类型,运行程序的时候就会出现假死的情况,虽然是动态内存,但是也是有边界的,一但越界访问,程序就会出现问题。
3、对非动态开辟内存使用free释放
例10:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 3.对非动态开辟内存使用free释放int a = 10;int* p = &a;free(p);p = NULL;return 0;
}
对于例10,a的空间是存放与栈区的,它并不是动态开辟的空间,free函数释放的一定是堆区上开辟的空间,如果对非动态开辟内存使用free释放,程序就会出现假死的情况。
4、使用free释放动态开辟内存的一部分
例11:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 4.使用free释放动态开辟内存的一部分int* p = (int*)malloc(40);if (p == NULL){return 0;}int i = 0;for (i = 0; i < 10; i++){*p++ = i;}// 回收空间free(p);p = NULL;return 0;
}
对于例11,我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。
5、对同一块动态内存的多次释放
例12:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}// 假设使用了空间// 释放free(p);// ...很多行代码过后 free(p);// 再次释放 return 0;
}
对于例12,我们在使用完空间后,释放了空间,在很多行代码过后,又释放了一次空间,这样程序同样会假死,那么我们如何改进呢?
例13:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}//可以这样free(p);p = NULL;free(p);return 0;
}
像例13这样,每次释放完空间,主动将p置为空指针,这样就可以有效避免了上述情况,因为我们之前提到过:
对于free函数:如果参数 ptr 是NULL指针,则free函数什么操作都不进行。
6、动态开辟内存忘记释放(内存泄漏)
例14:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 6.动态开辟内存忘记释放(内存泄漏)while (1){malloc(1);//警告 C6031 返回值被忽略 : “malloc”。}return 0;
}
对于例14,当我们开辟内存忘记释放的时候,就会造成内存泄漏。我们的电脑可能就会出现死机的情况,遇到这种情况我们一般都会重启,但是当我们写程序达到几万行的时候,出现了这种问题,那将是一个十分恐怖的事情。
四、几个经典的笔试题
1、题目1
void GetMemory(char* p)
{p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:程序崩溃
对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf("%s\n",str);” 。
解析代码:
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。
总结:
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:
str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏
2、题目2
“返回栈空间地址问题”
char* GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)
解析代码:
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(str);”
打印出来的值我们不清楚,所以结果为随机值。
3、题目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);
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏
解析代码:
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。
4、题目4
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)
解析代码:
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。
虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。
提示:free(p)和p=NULL一定要连贯使用!
总结
关于动态内存分配的讲解就到此结束了,动态内存分配其实并不困难,更多的还是一些概念的背诵,只要我们牢记这些易错点,拿捏起来,还是轻轻松松的!加油,冲冲冲!
ps:动态内存分配拖了蛮久的,关于文件的博客,博主会快马加鞭的肝的(doge)
C语言动态内存分配详解
文章目录
- 前言
- 一、为什么存在动态内存分配
- 1、已掌握的内存开辟方式
- 2、上述开辟空间方式的特点
- 3、为什么存在动态内存分配
- 二、动态内存函数的介绍
- 1、malloc
- 2、free
- 3、calloc
- 4、realloc
- 三、常见的动态内存错误
- 1、对NULL指针的解引用操作
- 2、对动态开辟内存的越界访问
- 3、对非动态开辟内存使用free释放
- 4、使用free释放动态开辟内存的一部分
- 5、对同一块动态内存的多次释放
- 6、动态开辟内存忘记释放(内存泄漏)
- 四、几个经典的笔试题
- 1、题目1
- 2、题目2
- 3、题目3
- 4、题目4
- 总结
前言
自从前两次博客写完以后,感觉对于我本人来说,收获很大,尤其是将学过的知识再度温习一遍,感觉基础扎实了很多,所以就养成了一个习惯,每学完一个模块,都会写一篇博客,不仅仅是写个我自己的,也是想通过这篇博客,与大家分享一些我的见解。本来周四就学完了动态内存分配,但是周末博主去玩了,于是忘记了写博客,现在加班奉上。
一、为什么存在动态内存分配
1、已掌握的内存开辟方式
在C语言中,我们将内存分为了4个区间:
代码区,全局变量与静态变量区,局部变量区即栈区,动态存储区,即堆(heap)区或自由存储区(free store)。
为了方便大家理解,图解如下:
通过之前的学习,我们了解了一些内存的使用方法:
(1)创建一个变量
当我们想要使用单一变量的时候,我们可以通过创建一个变量,来使用内存。
int a = 10;//局部变量 - 栈区
int g_a = 10;//全局变量 - 静态区
(2)创建一个数组
当我们需要使用多个相同类型变量的时候,我们可以通过创建一个数组,来使用内存。
int arr[10];//局部变量 - 栈区
int g_arr[10];//全局变量 - 静态区
2、上述开辟空间方式的特点
以上两种使用内存的方式是我们学过的,也是常用的,但是在某些情况下,仅仅有这两种方法是不足的。
例如:我们需要创建一个数组来存放一个班级的学生信息的时候。
我们在创建这个arr数组的时候,当我们直接给定数组的长度arr[50]的时候,这样是很简单,但是这样合理吗?
例1:
#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{struct s arr[50];// 50个struct s 类型的数据// 30 :不够// 60 :浪费return 0;
}
假设这个班级只有30个人,那么我们是不是就浪费了一部分的空间;假设这个班级有60个人,那么我们给定的50又不够。所以说这里给定多少都是不合理的。
这里有人又会说了,很简单啊:要多少给多少就好了嘛!就像这样
例2:
#include<stdio.h>
struct s
{char name[20];int age;
};
int main()
{int n = 0;scanf_s("%d", &n);struct s arr[n];//错误(活动) E0028 表达式必须含有常量值return 0;
}
运行结果为:报错
事实证明,我们的想法很美妙,但是现实却很残酷:
这里的错误名称叫:表达式必须含有常量值;
说明对于 struct s arr[n]; 这里的n是变量,那就不行了。
这里延伸一下:例2这种代码的写法叫做变长数组。
对于变长数组这种写法目前仅对于C99是可运行通过的。
总结:上述开辟空间方式的特点
(1)开辟空间的大小是固定的;
(2)数组在声明的时候,必须制定数组的长度,它所需的内存在编译时分配。
3、为什么存在动态内存分配
我们对于内存开辟空间的需求,不仅仅局限于这些方式,有时候我们需要的空间大小在程序运行的时候才能知道,这时上述方式就不能达成目的了,所以动态内存分配就应运而生了。
二、动态内存函数的介绍
1、malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
(1)如果开辟成功,则返回一个指向开辟好空间的指针;
(2)如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
(3)返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定;
(4)如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。
举一个例子
例3:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误 C2440 “初始化” : 无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
在例3中,我们如果采用 “ int* p = malloc(10 * sizeof(int)); ” 的方式来开辟空间,在大部分检测严格的编译器中,会报错,这是因为变量类型的不同,从这里我们也可以看出,malloc 开辟空间的返回值是 void* 类型;
上面我们也提到了:如果 malloc 开辟失败,则返回一个NULL指针,所以malloc 的返回值一定要做检查,所以我们用了一种特殊的方式来打印错误原因——“ printf("%s\n", strerror(errno)); ” ,这样如果开辟失败,编译器就不会报错了,而是在运行后将错误的原因打印出来。
易错提示:
因为我们计算机的内存也是有限的,所以我们不能为所欲为的开辟空间,当我们需要开辟的空间不够时,打印错误就会出现“Not enough space”。
2、free
紧接上文,我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还,我们在前边向系统借用了这么多内存,当我们用完以后,我们应该把这块内存还给系统,那么怎么还呢?这里就需要用到我们的 free 函数了。
C语言为我们提供了另外一个函数,专门用来做动态内存的释放和回收的:
void free(void *ptr)
free函数用来释放动态开辟的内存:
(1)如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;
(2)如果参数 ptr 是NULL指针,则函数什么操作都不进行。
先来看一个例子
例4:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整形的空间int* p = (int*)malloc(10 * sizeof(int));// malloc -> #include<stdlib.h>//int* p = malloc(10 * sizeof(int));// 错误 C2440 “初始化” : 无法从“void * ”转换为“int* ”if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
对比例3和例4,例4只是多了两行代码:
free( p );
p = NULL;
有人会疑惑了,例3和例4的运行结果明明是一样的啊,那为什么我们还要多此一举,加上这两行代码呢?
没错看上去运行结果是一样的,但这仅仅只是对于我们代码量很少的情况下,我们申请的内存够用了,所以目的达到了;但是假设我们要做一项任务量巨大的工程的时候,我们只借不还,系统的内存在不断减少,我们还能继续写程序吗?所以应该从现在养成一个习惯,申请的内存,用完以后一定要进行 free()操作。
这里有人又有疑问了,那我们用完了内存,释放了不就好了吗?为什么还要把这个指针p置为空指针呢?
其实当我们free(p)操作结束以后,这块空间是释放了,但是p的值并没有改变,如果有人找到了这个p,进行了破坏,我们的程序就有可能出问题,所以我们不妨主动将p置为空指针,让有非分之想的人断绝这些念想。
光说不练,是学习编程语言的大忌,我们趁热打铁,来做一道练习题:
正确答案为:
例5:
#include "string.h"
#include <stdio.h>
#include<stdlib.h>
int main()
{char* src="hello,world"; char* dest=NULL;int len=strlen(src);dest=(char*)malloc(len+1);// 要为\0分配空间char* d=dest;char* s=src+len-1;// 指向最后一个字符while(len--!=0){ *(d++)=*(s--);// 注意不要丢掉*号*d ='\0';// 字符串的结尾不要忘记'\0'} printf("%s",dest);free(dest);// 使用完要释放空间,避免内存泄露dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略return 0;
}
3、calloc
C语言还提供了一个函数叫 calloc ,calloc 函数也用来动态内存分配:
void* calloc(size_t num,size_t size)
(1)函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节初始化为0;
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0.
举个例子:
例6:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{int* p = (int*)calloc(10 , sizeof(int));// calloc -> #include<stdlib.h>if (p == NULL){// 打印错误原因的一个方式printf("%s\n", strerror(errno));// strerror -> #include<string.h>// errno -> #include<errno.h>}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候// 就应该还给操作系统free(p);p = NULL;return 0;
}
运行结果为:
0 0 0 0 0 0 0 0 0 0
由此可见:calloc 函数会将动态开辟空间的每个字节初始化为0
4、realloc
回归今天的核心问题,如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?
C语言同样为我们提供了一个函数叫 realloc ,realloc 函数可以让动态内存管理更加灵活:
void* realloc(void* ptr, size_t size);
(1)ptr 是要调整的内存地址;
(2)size 是调整后的新大小;
(3)返回值为调整之后的内存起始位置;
举个例子:
例7:
#include<stdio.h>
#include<stdlib.h>
int main()
{int* p = (int*)malloc(20);for (int i = 0; i < 5; i++){*(p + i) = i;}for (int i = 0; i < 5; i++){printf("%d ", *(p + i));}int* p2 = (int*)realloc(p, 40);for (int j = 5; j < 10; j++){*(p + j) = j;}for (int j = 5; j < 10; j++){printf("%d ", *(p + j));}free(p);p = NULL;return 0;
}
运行结果为:
0 1 2 3 4 5 6 7 8 9
(4)这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间;
(5)realloc 在调整内存空间的过程中存在两种情况
①原有空间之后有足够大的空间
此时,直接在原有内存之后追加空间,原来空间的数据不发生变化。
②原有空间之后没有足够大的空间
在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
图解如下:
三、常见的动态内存错误
1、对NULL指针的解引用操作
例8:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 1.对NULL指针解引用操作int* p = (int*)malloc(40);// 万一malloc失败了,p就被赋值为NULL// 不安全// 记得判断p是否为空int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}
对于例8,如果 malloc 开辟空间失败,此时 p 被赋值为NULL,而下面对于空指针进行操作, *(p + i) 始终为非法地址,我们的操作始终为非法操作,所以我们一定要在使用前记得判断p是否为空。
2、对动态开辟内存的越界访问
例9:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 2.对动态开辟内存的越界访问int* p = (int*)malloc(40);// 10个int -> 0-9if (p == NULL){return 0;}int i = 0;// 越界for (i = 0; i <= 10; i++){*(p + i) = i;}free(p);p = NULL;return 0;
}
对于例9,我们使用 malloc 向系统申请了 10个int 类型,但是我们在后边访问了 11个int 类型,运行程序的时候就会出现假死的情况,虽然是动态内存,但是也是有边界的,一但越界访问,程序就会出现问题。
3、对非动态开辟内存使用free释放
例10:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 3.对非动态开辟内存使用free释放int a = 10;int* p = &a;free(p);p = NULL;return 0;
}
对于例10,a的空间是存放与栈区的,它并不是动态开辟的空间,free函数释放的一定是堆区上开辟的空间,如果对非动态开辟内存使用free释放,程序就会出现假死的情况。
4、使用free释放动态开辟内存的一部分
例11:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 4.使用free释放动态开辟内存的一部分int* p = (int*)malloc(40);if (p == NULL){return 0;}int i = 0;for (i = 0; i < 10; i++){*p++ = i;}// 回收空间free(p);p = NULL;return 0;
}
对于例11,我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。
5、对同一块动态内存的多次释放
例12:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}// 假设使用了空间// 释放free(p);// ...很多行代码过后 free(p);// 再次释放 return 0;
}
对于例12,我们在使用完空间后,释放了空间,在很多行代码过后,又释放了一次空间,这样程序同样会假死,那么我们如何改进呢?
例13:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 5.对同一块动态内存的多次释放int* p = (int*)malloc(40);if (p == NULL){return 0;}//可以这样free(p);p = NULL;free(p);return 0;
}
像例13这样,每次释放完空间,主动将p置为空指针,这样就可以有效避免了上述情况,因为我们之前提到过:
对于free函数:如果参数 ptr 是NULL指针,则free函数什么操作都不进行。
6、动态开辟内存忘记释放(内存泄漏)
例14:
#include<stdio.h>
#include<stdlib.h>
int main()
{// 6.动态开辟内存忘记释放(内存泄漏)while (1){malloc(1);//警告 C6031 返回值被忽略 : “malloc”。}return 0;
}
对于例14,当我们开辟内存忘记释放的时候,就会造成内存泄漏。我们的电脑可能就会出现死机的情况,遇到这种情况我们一般都会重启,但是当我们写程序达到几万行的时候,出现了这种问题,那将是一个十分恐怖的事情。
四、几个经典的笔试题
1、题目1
void GetMemory(char* p)
{p = (char*)malloc(100);
}
void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:程序崩溃
对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf("%s\n",str);” 。
解析代码:
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。
总结:
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:
str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏
2、题目2
“返回栈空间地址问题”
char* GetMemory(void)
{char p[] = "hello world";return p;
}
void Test(void)
{char* str = NULL;str = GetMemory();printf(str);
}
请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)
解析代码:
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(str);”
打印出来的值我们不清楚,所以结果为随机值。
3、题目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);
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏
解析代码:
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。
4、题目4
void Test(void)
{char* str = (char*)malloc(100);strcpy(str, "hello");free(str);if (str != NULL){strcpy(str, "world");printf(str);}
}
请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)
解析代码:
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。
虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。
提示:free(p)和p=NULL一定要连贯使用!
总结
关于动态内存分配的讲解就到此结束了,动态内存分配其实并不困难,更多的还是一些概念的背诵,只要我们牢记这些易错点,拿捏起来,还是轻轻松松的!加油,冲冲冲!
ps:动态内存分配拖了蛮久的,关于文件的博客,博主会快马加鞭的肝的(doge)