3. 常见的动态内存错误
动态内存虽然好用,但是使用不当就会让人十分苦恼,下面列出几个常见的错误。
3.1 对NULL指针进行解引用操作
int main() { int* p = (int*)malloc(INT_MAX); if (p == NULL)//判断 { perror("malloc"); return 1; } else { *p = 5; } free(p); p = NULL; return 0; }
分析:
如果开辟空间过大,malloc开辟空间失败,返回NULL空指针,这时对指针解引用操作,程序就会奔溃。
最好的方法是对p是否为空指针进行判断,如果为空指针则打印错误信息,并退出函数。
3.2 动态开辟空间的越界访问
int main() { int* p = (int*)malloc(20); if (p == NULL) { return 1; } //使用 int i = 0; for (i = 0; i < 20; i++)//把20当做元素个数了 { *(p + i) = i;//严重越界 } //释放 free(p); p = NULL; return 0; }
分析:
malloc开辟了20个字节的空间,但是我误以为20为元素个数,造成了严重的越界访问,导致程序奔溃。
一定要搞懂函数的意思,在对指针进行操作时候看清楚!!!
3.3 对非动态开辟的内存使用free释放
int main() { int num = 10; int* p = # //... free(p); p = NULL; return 0; }
分析:
平时创建的局部变量在栈空间上开辟,当作用域结束,变量会自动销毁。而free只作用于在堆区上开辟的空间,如果将平常开辟的内存进行释放,程序会奔溃。编译器会很凌乱,表示这届程序员真难带!!!
3.4 使用free释放一块动态开辟内存的一部分
int main() { int* p = (int*)malloc(40); if (p == NULL) { return 1; } int i = 0; for (i = 0; i < 5; i++) { *p = i; p++;//p改变了 } //释放 free(p); p = NULL; return 0; }
分析:
p在使用过程中,进行了调整,p不再指向原来动态内存开辟的空间的起始位置。这块空间可能是动态开辟内存的一部分,也可能完全不适于开辟的空间。这时运行程序,程序依然会奔溃。
3.5 对同一块动态内存多次释放
int main() { int* p = malloc(40); if (p == NULL) { return 1; } int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i; } free(p);//已经释放过了 //p = NULL//加上这个就不会奔溃 //...继续写代码 free(p);//忘记已经释放过了 return 0; }
分析:
当我们对一块动态内存进行释放后,接着写代码,然后忘记自己已经对这块空间进行释放。于是我们继续释放,当程序运行起来时,程序会奔溃。
要牢记一个malloc/calloc对应一个free。
如果我们在这里把p = NULL,就不会有问题了。因为free对空指针时不会操作的。
3.6 动态开辟内存忘记释放(内存泄漏)
//函数会返回动态开辟空间的地址,记得在使用之后释放 int* get_memory() { int* p = (int*)malloc(40); //... return p; } int main() { int* ptr = getmemory(); //使用 //没释放 return 0; }
分析:
函数返回了动态内存开辟的空间,我们可以对其进行使用。但是一定要释放,否则就会出现内存泄漏,也就是"吃内存"的情况。
在我们设计这个函数时就应该写好相应注释,提醒使用者。使用者也应该养成良好的习惯,对动态内存开辟的空间进行释放。
4. 几个经典的笔试题
4.1 题目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; }
运行结果:
分析:
程序奔溃了。这里有两个问题。
第一个问题:
GetMemory函数传参传str本身,p是str的一份临时拷贝。在函数中使用p开辟一块100个字节的动态内存的空间。也就相当于p改变了,但是str本身并没有改变。
回到Test函数中,str依然是NULL空指针。这时对str进行字符串拷贝。会对空指针进行解引用操作。程序奔溃。
值得一提的是这里的printf(str)并没有问题。可以通过一个简单的例子来证明:我们平时可以通过printf("hello")把hello打印出来,同样的我们也可以把字符串的首元素地址放入指针中,通过指针打印出字符串。因为printf("hello")是把h的地址传给了printf,这样打印没问题,那么我把其他部分省略,我的意思也是把地址传给printf,然后直接打印字符串。
第二个问题:
malloc开辟的空间没有释放。但是如果我们想释放也无法释放,因为在GerMemory函数中存放开辟空间地址的指针由于退出函数被释放了。返回Test函数后没人知道这块空间在哪里,也没法释放。
所以这个函数实际上是存在着很严重的问题的,所以我们接下来就将其改对。
正确写法:
传址调用,直接改变str
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; }
分析:
我们要改变str的值,那么就把str的地址传入。str是一级指针,那么&str就需要用二级指针接收。通过解引用,找到str,将动态开辟空间的首地址放入p中。在使用完之后对空间进行释放。
- 参数无意义,返回值改变str
char* GetMemory(char* p) { p = (char*)malloc(100); return p; } void Test(void) { char* str = NULL; str = GetMemory(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里存放的是空指针,然后调用GetMemory函数,在函数中在栈空间上开辟一个数组,返回p(指向数组首元素位置)。但是出了函数,在函数中开辟的数组就会被销毁。当str接收返回值时就会接收被销毁空间的地址,当我对str进行打印时,这块空间已经还给操作系统了,这块空间可能被更改,也可能没更改。当前我们对其进行打印是乱码。
这就是典型的返回栈空间地址的问题!!!
一个小细节:
刚刚说返回函数栈空间的地址不对,那么这个函数对不对?
int test() { int a = 10; return a; } int main() { int ret = test(); printf("%d\n", ret); return 0; }
分析:
这个函数是完全正确的。当局部变量a返回时,会把a放到寄存器中,假设我们这个寄存器为eax,然后a销毁。再通过eax把返回值带回。
那么这个呢?
int* test() { int a = 10; return &a; } int main() { int* p = test(); printf("%d\n", *p); return 0; }
分析:
这就是典型的返回函数栈空间的地址。
我主函数中的p指向的空间a已经被释放,属于野指针,如果通过指针去访问,就是非法访问。
但是大家可能会有疑惑,那我这个运行结果怎么解释:
这个其实是巧合。a所在的空间恰好没有被修改,如果我们坚信这个是对的,以后肯定是会翻车的!!!
如果我稍加改变,在打印*p
之前打印一句话,例如这样:
int main() { int* p = test(); printf("hello\n"); printf("%d\n", *p); return 0; }
这里仅仅是增加了一句话就改变了*p的值,这是为什么?
看过我之前函数栈帧博客的,可能好理解些,接下来简单说一下原理:
当我们调用test函数时,在main函数上方需要开辟test函数的函数栈帧。栈空间使用习惯是从高地址向低地址使用。首先在栈帧最下方开辟a变量所需空间,当返回时则将*p放入寄存器中,将值带回,test函数栈帧被销毁。这一时刻很巧,a的值也没有改变。但是如果我们在打印*p前再使用了printf函数来打印一句话。这个printf函数可能就会在原先被释放的test函数栈帧的基础上开辟栈帧空间,这时a空间中的数据可能就会被修改,这就是6的来源。
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 main() { Test(); return 0; }
运行结果:
分析:
str起始为NULL,将&str和开辟空间大小传给GetMemory函数,函数在内部通过*p找到str空间,将动态开辟空间的起始地址放入str中,在通过strcpy进行拷贝,拷贝也成功了,最后打印也没问题。
这个过程看似一气呵成,但是缺了释放动态开辟的空间!!!及时释放非常重要!!!
正确写法:
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.4 题目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; }
运行结果:
分析:
malloc开辟了一块100个字节的空间,放入str中,strcpy也将"hello"放入了str中,然后我就对str空间进行释放了。但是我没有置为空指针。所以下面的if语句是会执行的,这时我使用了被释放的str,str为野指针,为非法访问。在对str进行strcpy将world放入str中,再进行打印。
虽然跑出了结果,但是它本质上是错的,只能说明编译器大意了,没有闪(doge)。我们还是要发挥主观能动性,自己发现错误,毕竟我们是程序员。
正确写法:
这个代码其实槽点挺多的,首先它释放空间后没有置空。其次它也没有开辟完空间就对str是否为空指针进行判断,所以我们不妨对它进行一个大整改。
我们在释放完空间之后直接将str置为空指针。让下面的if语句起到作用,就达到了我们原本的目的。
void Test(void) { char* str = (char*)malloc(100); if (str == NULL) { return; } strcpy(str, "hello"); free(str); str = NULL; if (str != NULL) { strcpy(str, "world"); printf(str); } } int main() { Test(); return 0; }
运行结果:
5. 结语
到这里,本篇博客就到此结束了。相信大家对动态内存管理也有了一定的了解。动态内存管理在C语言中是一块非常重要的知识,还是希望大家可以熟练掌握。