3. 常见的动态内存错误
在进行动态内存管理时,有很多需要注意的,一旦我们使用不当,就有可能会导致错误的发生。
接下来我们就来总结一下,哪些操作可能会引发动态内存错误。
3.1 对NULL指针的解引用操作
通过上面的学习我们已经知道了,malloc,realloc,calloc在开辟空间时,一旦开辟失败,就会返回空指针,如果我们不小心对这些空指针进行了解引用,就会导致错误的发生。
举个例子:
int main() { int* p = (int*)malloc(4); *p = 20;//如果p的值是NULL,就会有问题 free(p); p = NULL; return 0; }
大家看这段代码有问题吗?
因为malloc有可能返回空指针,所以像上面这样不做判断,直接对malloc返回的指针,解引用,就可能会导致问题出现。
我们写出这样的代码,有的编译器可能就直接会报警告:
不过上面的代码中我们申请的空间比较小,只有4个字节,可能不会申请失败。
如果我们要申请一块特别大的空间,很有可能就会开辟失败:
我们来试一下:
int main() { int* p = (int*)malloc(INT_MAX); *p = 20;//如果p的值是NULL,就会有问题 free(p); p = NULL; return 0; }
开辟INT_MAX字节的空间,INT_MAX是整型的最大值:
这次,很有可能就会失败,所以我们最后加一个判断:
int main() { int* p = (int*)malloc(INT_MAX); if (p == NULL) { perror("malloc"); return 1; } else { *p = 20; } free(p); p = NULL; return 0; }
这里的perror也是一个打印错误信息的函数(和strerror差不多),不过它可以在前面加上我们自定义的信息。
我们看结果是什么:
说明,这里malloc就开辟失败了,返回的是空指针。
所以,对于
malloc,realloc,calloc
的返回值,我们一定要进行一个检查,防止对空指针解引用。
3.2 对动态开辟空间的越界访问
对动态开辟空间的越界访问,也会发生错误。
举个例子:
int main() { int i = 0; int* p = (int*)malloc(5 * sizeof(int)); assert(p);//断言,防止p为空指针 for (i = 0; i <= 10; i++) { *(p + i) = i;//越界访问 printf("%d ", *p); } free(p); p = NULL; return 0; }
上面的代码中,我们使用malloc开辟了5个整型大小的空间,即20个字节,那p 作为1个整型指针,加1跳过4个字节,那我们循环10次,是不是就越界访问了啊。
这时我们运行就出错了:
因为我们越界访问了。
所以,也注意不能越界访问。
3.3 对非动态开辟内存使用free释放
这个我们在上面其实也已经提到过了。
我们要知道,free是用来释放动态开辟的内存空间的,
如果我们用free去释放非动态开辟的内存,此时free的行为是标准未定义的。
比如:
int main() { int num = 10; int* p = # free(p); p = NULL; return 0; }
num 是我们定义的局部变量,是保存在栈区的,而堆区才是用来动态内存分配的。
这样的代码运行,可能是会出错的。
所以我们不要用free去释放非动态开辟的内存。
3.4 使用free释放一块动态开辟内存的一部分
什么意思呢?
我们在使用free释放一块动态开辟的内存空间的时候,传给free那个指针必须是指向这块空间的起始位置。
如果在使用过程中,原本指向内存块起始位置的指针发生了改变,不再指向该空间的起始位置,那我们最后用free去释放这块空间的时候,就不能再传这个指针了。
举个例子:
int main() { int* p = (int*)malloc(10); assert(p); p++; free(p);//p不再指向动态内存的起始位置 p = NULL; return 0; }
这样的操作就是错误的,我们free(p)
的时候,p 已经不再指向这块动态内存的起始位置了。
运行这样的代码,程序就出错了。
如果确实需要改变:
我们可以再创建一个指针变量,保存一下最初指向起始地址的指针,这样最后释放的时候,我们依然能找到起始位置的地址。
像这样:
int main() { int* p = (int*)malloc(10); assert(p); int* ptr = p;//用ptr保存malloc开辟空间的起始位置 p++; free(ptr);//释放的时候传ptr ptr = NULL; p = NULL; return 0; }
这样,程序就不会出错了。
3.5 对同一块动态内存多次释放
什么意思呢?
我们动态开辟一块内存空间,使用完直接释放了,并且没有将指向该内存块起始位置的指针(假设是指针p)置空,过了一会儿可能忘记已经释放过了,然后再后面又把p传给free,又对这块空间进行一次释放。
这样的话我们运行代码就也会出错的。
像这样:
int main() { int* p = (int*)malloc(100); free(p); ///.....; free(p);//重复释放 return 0; }
这样程序是会崩掉的。
为了避免这种情况发生:
我们在释放掉p指向的空间之后,要及时将p置空。
int main() { int* p = (int*)malloc(100); free(p); p = NULL; ///.....; free(p);//重复释放 return 0; }
这样,虽然我们释放了两次,但因为我们第二次传的是空指针,所以不会有问题。
因为如果free的参数 ptr 接收的是NULL指针,不执行任何操作。
所以:
在使用free释放一块动态内存空间后,及时将指向起始位置的指针置空是一个好习惯。
3.6 动态开辟内存忘记释放(内存泄漏)
就是我们动态开辟的空间在使用完之后,一定要记得释放,不释放的话有可能会造成内存泄漏。
切记:
动态开辟的空间一定要释放,并且正确释放 。
4.经典笔试题讲解
下面,我们一起来看几个动态内存管理相关的经典笔试题。
4.1 题目1
我们来看这段代码:
#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; }
请问运行 Test 函数会有什么样的结果?
经过上面的学习,我们相信大家应该很容易能够看出来,上面这段代码存在一些比较严重的问题。
我们一起来分析一下:
首先,test函数进去,定义了一个字符指针char* str,给它赋了一个空指针NULL,然后,调用GetMemory函数,实参传的是str,GetMemory函数用了一个字符指针p接收,在GetMemory内部,用malloc动态开辟了100个字节的空间,返回值强制类型转换为char*赋给了p。
走到这一步我们其实就能发现一个小问题:
这里没有对malloc进行一个判断或者断言,因为malloc有可能开辟空间失败返回空指针。
当然这还不算这段代码中最严重的问题。
那我们接着往下看:
GetMemory(str);调用结束,下一句代码是:
strcpy(str, "hello world");
看到这里我们应该能猜出来这段代码的目的:
应该是想把字符串"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,然后打印出来。
那到这里第二个问题就出来了。
strcpy(str, "hello world")
既然是想把"hello world"拷贝到函数GetMemory(str)中动态开辟的那100个字节空间里,那第一个参数str是不是应该指向malloc开辟的那100个字节才对啊。
但是,上面代码里面传参传的是指针变量str,形参p实际只是str的一个临时拷贝。我们把malloc的返回值赋给了p,让p指向了这100个字节空间的首地址,但是str是不是并没有改变啊,Test 函数中的 str 一直都是 NULL。
而strcpy在拷贝是应该是会对str解引用的,这样会导致程序崩溃的!!!
还有一个问题:
malloc开辟的空间使用完是需要使用free释放的,但是上述代码并没有释放,这样就可能导致内存泄漏,因此,这也是一个比较严重的错误。
那接下来我们就来修改一下这段代码,将它变成正确的:
#include <stdlib.h> #include <string.h> void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str);//不传地址,将p作为返回值赋给str也可以 strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main() { Test(); return 0; }
这样代码就正确了:
4.2 题目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; }
大家再来看看这段代码有没有什么问题?
我们一起来分析一下:
首先str还是一个char*的指针,给它赋值为NULL,然后调用GetMemory(),
GetMemory()内部创建了一个字符数组p,放了一个字符串"hello world",p是数组名,是首字符’h’的地址,将p作为返回值赋给str,那我们是不是就可以通过str访问数组p了,printf(str)就把"hello world"打印出来了。
是这样吗?
如果这样想,那就错了。
为什么呢?
数组p是我们在函数内部创建的一个局部的数组,当函数调用结束就被销毁了,数组所在的这块空间就还给操作系统了,那这时候我们再去打印这块空间里的内容,是不是就非法访问内存了。
这样也是不行的。
4.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); } int mani() { Test(); return 0; }
再来看这段代码,有什么问题:
这段代码前面都没有什么大问题,当然这里还是没有对malloc进行是否为空指针的判断。
传的是str的地址,GetMemory调用结束后,str指向的就是malloc开辟的那100字节的空间,那strcpy(str, "hello");就能够成功把字符串"hello"拷贝到str指向的空间,然后打印出来,这都没什么问题。
但是:
是不是没有对malloc开辟的空间进行释放,还是存在一个内存泄漏问题。
我们可以来修改一下:
void GetMemory(char** p, int num) { *p = (char*)malloc(num); assert(*p); } void Test(void) { char* str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); free(str); str = NULL; } int mani() { Test(); return 0; }
这样就没什么问题了。
4.4 题目4
void Test(void) { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); if (str != NULL) { strcpy(str, "world"); printf(str); } }
来看这段代码,有没有问题:
首先,第一个问题还是没有对malloc的返回值进行一个判断,有可能是空指针。
其次,我们发现,在strcpy(str, "hello");之后,就直接free(str)了,这时str指向的空间已经被释放了,但是str还保留这块空间的地址,因为释放后我们并没有将它置空,那此时的str是不是已经成为野指针了,因为它指向了一块已经被释放的空间。
那下面的if (str != NULL)判断结果为真,就会进入if语句,而在if语句里面又有strcpy(str, "world"),把"world"拷贝到已经不属于我们的动态内存区,篡改动态内存区的内容,后果难以预料,非常危险。
所以这段代码也是有问题的。
以上就是对C语言动态内存管理的讲解及一些笔试题练习,欢迎大家指正!!!