虽然封边是顶针,但是我们还是要好好学习♥
1.常见动态内存错误整理
1.1.对NULL空指针的解引用操作
1.2.对动态开辟空间的越界访问
1.3.对非动态开辟内存使用free进行释放
1.4.使用free释放一块动态开辟内存的一部分
1.5.对同一块动态内存的多次释放
1.6.动态开辟内存忘记释放或者错误释放(内存泄漏)
2.经典笔试题详细讲解
3.完结撒❀
创作不易,望三连支持,拜托啦🙏qaq
-----❀------❀------❀------❀------❀-------❀------❀------❀------❀------❀------❀------❀------❀------❀------❀-----
1.常见动态内存错误整理
我的上篇博客讲了动态内存管理,为了能够帮大家熟练掌握这块知识,这篇我把动态内存管理常见错误整理出来为大家进行讲解,帮助大家更好的去掌握并且使用。
1.1.对NULL空指针的解引用操作
上前博客我们讲到使用内存函数进行动态内存开辟之后函数都会返回所开辟空间的起始地址,我们在接收起始地址之后必须对其地址进行检验是否为空指针,因为如果空间开辟失败,那么函数返回得就是空指针。所以我们常见的错误之一就是没有对内存函数所返回的值进行检验而直接对其进行解引用操作,代码如下:
int main()
{
//创建动态内存空间
int* p = (int*)malloc(40);
//使用
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//销毁
free(p);
p = NULL;
return 0;
}
我们创建了40个字节,但是直接进行了使用,在VS中虽然代码跑起来了,并且也打印成功,但是这种是非常错误的代码,创建空间后必须要对返回的指针进行检验,在VS中编译器其实已经报错了:
再次强调一下,对创建动态内存函数所返回的创建空间起始地址的指针必须对其进行检验是否为空指针NULL
1.2.对动态开辟空间的越界访问
这个错误也很常见,看下面代码:
int main()
{
//开辟空间
int* p = (int*)calloc(10, sizeof(int));//一共开辟了10个整形大小
//检验空指针
if (p = NULL)
{
perror("calloc");
return 1;
}
//使用
for (int i = 0; i <= 10; i++)//这里一共访问了11个整形
{
*(p + i) = i;
}
//打印
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//销毁
free(p);
p = NULL;
return 0;
}
我们一共是开辟了10个整形,即40个字节,但在使用的时候for循环里我们一共访问使用了11个整形,这就是动态空间开辟的越界访问,直接运行程序的话就会报错:
再次强调一下,注意所开辟的动态内存空间大小,避免越界访问
1.3.对非动态开辟内存使用free进行释放
我们在进行动态内存的开辟使用过程中,对于内存函数所返回的起始地址存放的指针变量的值最好不要进行修改,就算必须要修改,那么对于所开辟的空间其实地址也要进行保留储存,要保证最后在释放空间时,所传给free函数的地址的确是所开辟的动态内存的起始地址,见下面代码:
int main()
{
int a = 0;
//内存开辟
int* p = (int*)realloc(NULL, 40);
//检验空指针
if (p == NULL)
{
perror("realloc");
return 1;
}
//使用。。。
p = &a;//p指向的空间就不在是堆区上的空间
//释放
free(p);
p = NULL;
return 0;
}
在使用所创建的动态内存的途中我们把之前所创建的整形变量a的地址赋给了记录动态内存空间其实地址的p,这就修改了p内所存的地址,下面传给free函数的也不是动态内存开辟的起始地址,所以就不能进行释放,如果运行程序的话就会报错:
再次强调一下,对于存储所开辟的动态内存空间起始地址的指针变量不要轻易进行修改,修改的话必须创建其他变量记录起始位置的地址。
1.4.使用free释放一块动态开辟内存的一部分
我们开辟内存使用之后,使用free函数进行释放,我们给free函数所传的地址必须是所开辟动态内存空间的起始地址,不然程序运行就会报错,见下代码:
int main()
{
//内存开辟
int* p = (int*)realloc(NULL, 40);
//检验空指针
if (p == NULL)
{
perror("realloc");
return 1;
}
//使用。。。
p++;
//释放
free(p);
p = NULL;
return 0;
}
我们给free函数所传的地址为p加一处的地址,这样free函数是不会对其进行释放的,运行编译器报错:
再次强调一下,给free函数所传的地址必须的所开辟动态内存空间的起始地址。
1.5.对同一块动态内存的多次释放
在写代码的过程中,如果开辟动态内存过多的话,我们可能会对同一块动态内存进行多次的释放,这是错误的,见下代码:
int main()
{
//内存开辟
int* p = (int*)realloc(NULL, 40);
//检验空指针
if (p == NULL)
{
perror("realloc");
return 1;
}
//使用。。。
//释放
free(p);
//...
free(p);//二次释放
p = NULL;
return 0;
}
这里我们就对p所对应的动态内存空间进行了二次释放,如果直接运行代码的话程序就会报错:
但是如果我们每次在释放动态内存空间后都把p指针赋值为空指针,那么程序也不会出现任何问题,如下代码:
int main()
{
//内存开辟
int* p = (int*)realloc(NULL, 40);
//检验空指针
if (p == NULL)
{
perror("realloc");
return 1;
}
//使用。。。
//释放
free(p);
p = NULL;
//...
free(p);//二次释放
p = NULL;
return 0;
}
因为free函数如果接收了NULL的话,什么都不会干,所以程序就不会报错。
再次强调一下,对于free函数释放空间后必须要对指针进行空指针的赋值,并且注意一个动态内存空间不要释放二次
1.6.动态开辟内存忘记释放或者错误释放(内存泄漏)
举例:
void test()
{
int* p = (int*)malloc(40);
if (p != NULL)
{
*p = 20;
}
}
int main()
{
test();
while (1);
return 0;
}
上面代码就是开辟动态内存空间之后没有对其进行释放,如果没有释放的话,程序自己是不会进行释放的,这就会造成内存泄漏,我们在创建动态内存空间的时候开辟内存函数malloc等和free函数最好要配套使用,避免忘记释放空间的情况,改良如下:
void test()
{
int* p = (int*)malloc(40);
if (p != NULL)
{
*p = 20;
}
free(p);
p = NULL;
}
int main()
{
test();
while (1);
return 0;
}
有时候我们记得释放空间,但还是会出现错误,比如:
int* test()
{
int* p = (int*)malloc(40);
if (p != NULL)
{
return p;
}
else
{
perror("malloc");
return 1;
}
free(p);
p = NULL;
}
int main()
{
int* p1 = test();
//使用。。。
while (1);
return 0;
}
我们可以看到在test函数中如果p不为空指针,那么直接就返回p,而test函数在返回p之后就会结束,代码根本就执行不到下面的free函数哪里,所以也就不会进行释放。
或者我们在test函数里面不进行释放,那么返回指针p后在主函数中最好表示注释:所开辟的40个字节为动态内存空间,使用完毕后请进行释放。代码如下:
int* test()
{
int* p = (int*)malloc(40);
if (p != NULL)
{
return p;
}
else
{
perror("malloc");
return 1;
}
}
int main()
{
int* p1 = test();//所开辟的40个字节为动态内存空间,使用结束后请进行释放
//使用。。。
while (1);
free(p1);
p1 = NULL;
return 0;
}
再强调一边,我们创建动态内存并使用的话内存开辟函数malloc等要和free函数搭配使用,并且注意代码逻辑保证正确进行了销毁
上面就是常见动态内存错误的整理,总之,我们在写代码的时候一定要脑回路清晰,避免一些错误发生。
2.经典笔试题详细讲解
题目1:
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”呢?打印不出来的话问题又在哪里?
答案:程序运行会崩溃,最后无法打印“hello world”
讲解:下面我们对上述代码进行分析,首先从主函数进入Test函数内部,创建了一个指针变量str并赋值了空指针进行初始化,之后进入GetMemory函数内部,把str指针传了过去并用字符指针p来进行接收,在GetMemory函数中开辟动态内存空间,并将malloc返回的起始地址赋给了字符指针p,下面跳出GetMemory函数,进行strcpy的字符串赋值,最后打印字符串str。
总体过程就是如此,那么对于指针学的好的同学估计已经知道错误在哪里了,就在GetMemory函数传参哪里,因为传过去的是str一级指针,接收时用一级指针p进行接收,之后把开辟好的动态内存空间起始地址返回给p,p里面这是确实存放的是所开辟空间的起始地址,但是我们下面进行字符串的拷贝赋值用的是str指针,也就是说按照逻辑如果我们想要正常进行拷贝,那么str中应该存放的是GetMemory函数里面malloc函数所返回的指针地址,而将起始地址传给一级指针p并不能改变指针str的值,所以肯定是不能被打印的,我们也可以将代码进行修改,得到正确的代码,如下:
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的值,使指针str指向了所开辟内存空间的起始地址,那么就可以将“hello world”进行拷贝了,执行上面代码结果如下:
题目2:
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
问题:上述代码最后是否可以打印出“hello world”呢?打印不出来的话问题又在哪里?
答案:无法打印出“hello world”,最后打印出来的是随机值。
讲解:我们先对上面的代码逻辑梳理一遍,从主函数进入Test函数内部,在Test函数中创建字符指针p并赋值空指针,再进入GetMemory函数内部,在GetMemory函数内部创建一个字符串p保存“hello world”,之后再把字符串p返回(这里p里面存的是字符串“hello world”的首地址),再将p赋到str里面最后打印字符串。
学过函数栈帧创建与销毁的同学我想已经想知道问题所在了,之前我也写过讲解函数栈帧的创建与销毁的博客,没有学过的同学可以去认真学习一下哦:函数栈帧的创建与销毁
每次函数调用都会在栈区开辟一块空间,函数调用结束后空间便会销毁,上面代码的问题就在于GetMemory函数调用结束后在栈区上开辟的内存空间便会销毁,而我们却把记录在GetMemory函数里面创建的字符串的首地址进行了返回,回到Test函数后,GetMemory函数在栈区上开辟的空间已经被销毁,销毁后内存上面存放的是随机值,而指针str里面存放了原先所开辟内存空间里面字符串的地址,这时候str便成了野指针,打印再次找到这处地址后便打印出来的是随机值。
我们假设字符串p的首地址为0x0012ff40,那么画图就可以那么理解:
如果我们想要修改此代码,使其正常运行并且打印出“hello world”,我们就可以这么修改:
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;
}
没错,就是加个static上去,让GetMemory函数调用结束后字符串p的内存也不会销毁,那么就可以顺利打印了。
题目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);
}
int main()
{
Test();
return 0;
}
问题:上述代码最后是否可以打印出“hello”呢?打印不出来的话问题又在哪里?
答案:可以打印出"hello’',但是代码其实有问题。
讲解:对于上面代码,打印出“hello”是没有问题的,但是我们仔细观察就会发现它居然没有释放空间!!!,这是一个很严重的错误,谁没有发现问题再去重新学!【狗头保命】,我们开辟了动态内存空间使用完毕之后一定要记得释放空间,可就把malloc函数可free函数当作是亲双胞胎兄弟,两个1要用必须一起用,加深印象。向上面代码一样,如果是创建一个函数专门进行动态内存的开辟的话,在使用这些内存的函数里面建议都要明确标注内存的动态开辟申请的,使用完毕需要进行释放。代码修改正确如下:
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:
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;
}
问题:上面代码执行完毕会打印出什么结果?
答案:打印出“world”,但是代码严重错误。
讲解:对于上面代码也是按照一贯逻辑,从主函数进入Test函数内部,再进行空间的开辟和使用,但是学习扎实的同学可能已经注意到了,free函数释放空间之后并没有把指针str置为空指针,而free函数是不会改变str指针的,那么因为之前所创建的100个字节的空间已经被释放,而str指针里面存放的还是先前所创建的动态内存空间的起始地址,所以这时候指针str已经成了一个野指针,而顺着代码逻辑下去运行的话是可以打印出“world”的,但是已经形成了非法越界访问,所以代码是错误的。之后我们使用了free函数释放空间以后也必须对存放原起始地址的指针进行赋值空指针,这样才能必变野指针的发生,避免出现非法越界访问。修改代码如下:
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
其实这里是需要在free函数后面将str赋值空指针的,但是按照代码逻辑也不太合适,大家学习知道开辟内存函数,free,赋值空指针,这三个必须一起正确使用就可以了。
3.完结撒❀
如果以上内容对你有帮助不妨点赞支持一下,以后还会分享更多编程知识,我们一起进步。
最后我想讲的是,据说点赞的都能找到漂亮女朋友