动态内存管理常见的错误
对空指针的解引用操作:
#include<stdlib.h>
int main()
{
int*p=(int*)malloc(40);
*p = 20;
free(p);
return 0;
}
我们首先通过malloc函数申请40个字节的内存,把这部分内存强制类型转化为整型指针,然后赋给整型指针p,然后解引用p=20.但是我们这里还有产生空指针的情况:假如申请空间很大,计算机内存不够,所以申请失败,返回空指针,我们这时候就形成了对空指针的解引用,对空指针解引用造成运行时错误。
下面是对应的解决方案:
#include<stdlib.h>
int main()
{
int*p=(int*)malloc(40);
if (p == NULL)
{
return 1;
}
*p = 20;
free(p);
p = NULL;
return 0;
}
我们在对p进行解引用的前面放上判断语句,如果为空指针,退出循环。
第二种问题:对动态开辟空间的越界访问
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<stdlib.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) = 0;
}
free(p);
p = NULL;
return 0;
}
以上代码其余都没什么问题,除了for循环中,我们malloc申请了40个字节的内存,但是我们的for却访问并初始化了11个字节,这就形成了越界访问。
第三个问题:对非动态开辟内存使用free释放:
#include<stdlib.h>
int main()
{
int a = 0;
int*p = &a;
free(p);
p = NULL;
return 0;
}
这串代码,我们创建的是非动态内存,我们对非动态内存进行free释放产生问题。
进行运行,程序崩溃
错误的原因是:free释放的空间应该是堆区的calloc malloc realloc函数申请的空间,free不能释放战区的空间。
第四种错误:使用free释放动态内存开辟的一部分。
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;
p++;
}
free(p);
p = NULL;
return 0;
}
这串代码看上去没有什么问题,问题出现在哪里呢?
答:问题出现在了for循环中,我们在for循环中,移动了p指针,所以p指针指向的内容不再是这片空间的起始位置,而是这片空间的末尾,我们画图进行解释:
p指向的十个整型空间如图所示,我们通过for循环,让p++,造成的最终结果是:
p指向了这个位置。
所以我们的p并没有指向这个空间的其实位置,我们的free必须释放掉完整的一个空间,不能只释放掉一部分。
假如我们对for循环进行改值会发生什么?
由10改成5,对应的p的位置应该是:
那是不是代表我们能释放p后面的五个整型空间呢?
答案是不:我们的free永远只能释放一整块空间,不能释放一部分。
我们进行编译
会报错。
第五个问题:对同一块内存空间的多次释放。
int main()
{
int*p=(int*)malloc(40);
//---
free(p);
//---
free(p);
return 0;
}
free只能对指定空间释放一次,再次释放就会报错,防止这种错误产生,我们可以:
int main()
{
int*p=(int*)malloc(40);
//---
free(p);
p = NULL;
//---
free(p);
return 0;
}
我们要记得每次释放过后,把对应的指针置为空指针。这样,我们进行编译就不会报错了。
第六种错误:动态开辟内存忘记释放(内存泄漏)
void test()
{
int*p=(int*)malloc(100);
int flag = 0;
scanf("%d", &flag);
if (flag == 5)
{
return 1;
}
free(p);
p = NULL;
return 0;
}
int main()
{
test();
return 0;
}
就像这串代码,假如我们输入的flag的值为5,那么我们就会直接退出test函数,我们就来不及执行free函数,对应的内存空间就没有被释放掉,所以造成内存泄漏。
下一个场景:
#include<stdio.h>
#include<stdlib.h>
void* test()
{
int*p=(int*)malloc(100);
if (p == NULL)
{
return p;
}
//---
return p;
}
int main()
{
int *ret = test();
return 0;
}
我们在test函数内部完成了申请内存,我们的对应的内存的地址返回给参数ret,所以我们要在main函数中对对应内存进行释放,否则就会导致内存泄漏。
所以,调用别人的函数中有动态内存开辟的话,我们要注意要及时对动态内存进行释放。
接下来,我们做几道经典的笔试题:
第一道题目:
#include<stdio.h>
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;
}
这道题错误的地方有哪些,会打印出来hello world吗?
答:并不能,会出现以下问题:我们进行分析:
首先执行test函数,在test函数中,我们首先创建指针str,str指向的是空指针,对应的图像为
然后我们调用GetMemory函数,参数为str。
我们在GetMemory函数中,函数的形式参数是p,形式参数p是实际参数str的临时拷贝,对应的图像为
我们在GetMemory函数中调用malloc函数开辟内存为100个字节,讲其强制转化为char*的类型,然后把首地址传递给p,因为我们p原来指向的是空指针,所以对应的图像为
p现在就指向动态内存的首地址,我们退出函数,p是形式参数,退出函数p自动释放。
P释放过后,相当于动态内存的首地址找不到了,这就造成了这部分内存我们无法寻址,造成内存泄漏的问题。
我们的str一直为空指针,我们接下来使用strcpy字符串拷贝函数,我们知道字符串拷贝函数内部的实现需要对str进行解引用操作,因为str指向的是空指针,对空指针解引用会导致运行式错误。
因为我们的程序提前崩溃,所以就无法执行打印函数。所以并不能打印出我们想要的结果。
如何对上述代码进行修改呢?
#include<stdio.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;
}
我们的函数GetMemory函数在传参时,把对应的str的地址传递过去,str是char类型的指针,对str进行取地址之后的类型是char**,我们用char**来接收,*p表示的是str指针,我们把对应的空间的首地址传递给str,所以str就指向了对应的空间。
因为我们的str不再是空指针,所以我们可以调用strcpy函数,并且可以打印出对应的结果
在函数的最后,我们记得要把动态内存进行释放。
上述函数也增加了一种打印字符串的方法
例如
int main()
{
char*p = "hello world";
printf("hello world\n");
printf(p);
return 0;
}
我们进行编译
我们可以这样解释:
char*p = "hello world";表示把首字符h的地址传递给p指针。
printf("hello world\n");表示把首字符h的地址传递给printf函数
p相当于首字符的地址,我们这里也同样是把首字符的地址传递给函数,所以是可行的。
下一道题目:
这道题的问题出现在哪里?
答:野指针问题:我们在f1函数的内部创建变量x,然后我们把x的地址当作返回值,我们的函数结束,因为x已经被释放,x的值既然被释放了,我们取地址x取出的地址就是未申请的空间,所以就造成了野指针的问题。
下一道题目
出现的问题也是野指针问题:我们创建一个整型指针ptr,不进行初始化,指针不初始化时,里面放的是随机值,对随机地址(也就是未申请的空间)进行解引用,造成了野指针的问题。
下一道题目:
能否打印出hello world的字符串?
答:我们对代码进行分析:创建一个指针str,str为空指针,调用GetMemory函数,把对应的返回值返回给str,打印str指向的字符串。
在函数部分,我们创建一个数组p,把字符串的首元素的地址传给p,返回p,这时候,我们的函数调用完毕,函数完成调用,我们的创建的数组是一个临时数组,函数调用完毕,对应的空间就要返还给操作系统了,假如空间里对应的内容没有被覆盖的情况下,打印的结果还是hello world,假如被覆盖,打印的结果就不清楚了,我们画出对应的图像:
开始调用函数
p指向的是字符串首元素的地址,假如首元素的地址对应是:ox12ff40时
我们把p返回给str
所以
str就指向我们创建的字符串首元素的地址,但是因为我们已经调用完毕函数,我们在函数内部创建的空间是临时数组,函数结束时,数组对应的空间就返还给操作系统了,那么对应的空间就可能被覆盖了,假如被覆盖的话,我们就打印不出来对应的字符串,假如没有被覆盖,我们还能打印出来。
并且,str指向指向的空间不是我们申请的,而是操作系统的,所以我们的str就变成了野指针。
我们写一串代码加强我们的理解。
int*test()
{
int a = 10;
return &a;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
对代码进行分析:我们子啊test函数中创建一个变量,返回变量的地址,用p来接受对应的地址,这里函数已经结束,所以变量指向的空间就会返还给操作系统,这时候我们的p指向的其实是不属于我们申请的空间,所以p就是一个野指针,我们进行打印
打印出10,是不是说明对应的a空间并没有被释放
答案是错,我们对应的a的空间已经被释放了,返回给操作系统了,之所以能打印出10是没有其他信息覆盖到这篇区域。
int*test()
{
int a = 10;
return &a;
}
int main()
{
int*p = test();
printf("hehe\n");
printf("%d\n", *p);
return 0;
}
我们在打印*p前面打印一个hehe
*p对应的值发生了很大的变化,这就是被其他信息覆盖的结果。
我们用函数栈帧的思想进行解释:
这是我们之前写的关于函数栈帧的博客,有兴趣的朋友们可以去看看第9讲 - C语言关键字(9)_赵思凯的博客-CSDN博客
黑色大方块表示main函数中的栈帧,绿色大方块表示test函数中的栈帧,
p和a的小方块分别对应的是参数p和参数a。
当我们调用完毕test函数时,我们的test函数的栈帧就释放了,也就是对应的绿色方块,返还给操作系统了,然后我们调用printf函数,我们就会在原有的黑色方块上面重新开辟一个栈帧
对应的是红色方块。我们可以发现,我们红色的方块把原来的绿色的方块进行了覆盖,所以我们从前遗留在绿色方块内的数据也已经被覆盖,所以我们打印的结果就发生了改变。
上述的问题都是返回栈空间的地址的问题。
下一个问题:上述代码能打印出来hello吗?如果能打印出来,还存在其他的问题吗?
#include<stdio.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);
}
int main()
{
test();
return 0;
}
我们对代码进行分析并画图像:
执行test函数,创建指针str等于空指针
然后,我们调用函数GetMemory
我们把开辟的动态内存的首字符的地址传递给str
我们通过strcpy进行字符串拷贝。
我们进行打印,能够打印出结果hello。
这里出现的问题是我们没有对动态内存开辟的空间进行释放。
#include<stdio.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;
}
这就是释放后的结果。
下一道题目:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;
}
这串代码犯了什么错误,进行解释
我们进行画图分析:
执行Test函数
申请一个100个字节的动态内存,讲对应内存的首元素的地址强制类型转化为char*类型的赋给str。
strcpy函数:
使用free释放掉str指向的动态内存的空间
进行if语句的判断,满足if语句,调用strcpy函数
strcpy函数会对str进行解引用,str现在存放的地址指向的空间是未申请的,所以str就成了野指针,程序不再运行。
在我们c语言中,不同的代码都是放在哪里呢?
首先,内存分为五个区:内核空间,栈区,内存映射段,堆区,数据段,代码段:
内核空间:用户代码不能读写。
栈区:保存的一般是局部变量。
内存映射段:
堆区:保存的是动态内存开辟的代码
数据段:保存的一般是static修饰的静态变量和全局变量。
代码段:保存的是可执行代码和只读常量。(一般是常量字符串)。
我们讲一道编程题目:
描述
小乐乐最近在课上学习了如何求两个正整数的最大公约数与最小公倍数,但是他竟然不会求两个正整数的最大公约数与最小公倍数之和,请你帮助他解决这个问题。
输入描述:
每组输入包含两个正整数n和m。(1 ≤ n ≤ 109,1 ≤ m ≤ 109)
输出描述:
对于每组输入,输出一个正整数,为n和m的最大公约数与最小公倍数之和。
示例1
输入:
10 20
复制输出:
30
复制
示例2
输入:
15 20
复制输出:
65
这道题目该如何写呢?我们先写出最简单的方法:我们进行思考,两个数的最大公约数是能够同时被这两个数整除的最大数字,两个数的最小公倍数是能够整除这两个数字的最小数字。
并且最大公约数至少是两个数中较小的那个数 最小公倍数至少是两个数中较大的那个数。
#include<stdio.h>
int main()
{
int m = 0;
int n = 0;
while (scanf("%d%d", &m, &n) == 2)
{
int min = m < n ? m : n;
int max = m>n ? m : n;
int i = min;
int j = max;
while (1)
{
if (m%i == 0 && n%i == 0)
{
break;
}
i--;
}
while (1)
{
if (j%m == 0 && j%n == 0)
{
break;
}
j++;
}
printf("%d", i + j);
}
}
但这种方法运行时间过长,有些题目要求是不能够满足的。
那么我们讲第二种方法:辗转相除法
首先,我们先求两个数字的最大公约数,假设m是m,n中较大的那个数。
例如:
就如我们图像中所说的,我们先让m%n=r,再把n的值赋给m,r的值赋给n,再计算m%n的结果,直到m%n的结果为0时,对应的n的结果就是最大公约数。
还有一个补充的点就是:最大公约数×最小公倍数的结果=m*n
int main()
{
int n = 0;
int m = 0;
int r = 0;
while (scanf("%d%d", &n, &m) == 2)
{
int i = m > n ? m : n;
int j = m < n ? m : n;
while (r = i%j)
{
i = j;
j = r;
}
printf("%d", j + m*n / j);
}
return 0;
}
我们使用这种方法进行进行自测运行是能够通过的
但我们进行编译的时候,发现并不能通过
我们可以发现,原因出现在我们的int类型是不能够存储如此大的数据的,所以我们使用long类型
当我们使用long类型的时候,我们再进行提交
成功通过。