文章目录
1. 前言2. 正文2.1. “分配” 与 “释放”2.2. 运行测试2.2.1. VSCode 下使用 gcc 编译2.2.2. VS 下使用 MSVC 编译2.3. 程序漏洞测试2.4. 程序漏洞修复2.5. 附加测试3. 总结欢迎大家移步 我的博客 查看原文。
1. 前言
上机时遇到如下 C++ 代码 ( C 代码):
//删除带头结点的多项式单链表中系数为 0 项 void DelZero(PolyNode *&L){PolyNode *pre = L, *p = pre->next;while (p != NULL){if (p->coef == 0.0){pre->next = p->next;free(p);}pre = p;p = p->next;}}来源:《数据结构教程 (第 5 版) 上机实验指导》(李春葆主编) 第二章实验题 10
当if
语句执行后,指针p
指向的内存已经被释放,接着又执行p = p->next
,使用了指向已经被释放了的内存的指针。那么,这样的用法是否正确呢?
2. 正文
free(p)
后p = p->next
能否正确执行,要看编译器的具体实现,不同的编译器可能会产生不同的结果。上述程序我在VSCode中使用gcc编译能正常运行,但使用VS的MSVC编译后不能正常运行。而且即便能够正常运行,这种用法也会导致程序出现漏洞。
2.1. “分配” 与 “释放”
在调用内存分配函数 (malloc、callocmalloc、callocmalloc、calloc 等) 在堆区分配一块内存后,这块内存会被标记为已使用,确保在之后的内存分配中不会将这块已经分配过的内存进行二次分配。
而调用free
函数将内存释放后,这块内存便会被标记为未使用,在往后的内存分配中这块内存便能再次被分配了。而free
后内存中的值是否改变这要看编译器的具体实现,不同编译器可能具有不同的实现方式。
2.2. 运行测试
2.2.1. VSCode 下使用 gcc 编译
在调用free
函数释放动态分配的内存后,指针p
仍然指向这块已经被释放的内存 (指针变量p
中仍然保存着这块内存的地址),而使用gcc进行编译,被释放的内存中原有的内容并未被覆盖 (前言中给出的代码对应的程序是这样,一会儿会举个反例),执行p = p->next
后p
指向单链表的下一个结点,因此程序能正常运行而不会错误中止。虽然能正常运行,但程序存在漏洞。
参见 2.3 程序漏洞测试
2.2.2. VS 下使用 MSVC 编译
在VS下使用MSVC进行编译,第一次执行free(p)
前p
指向的内存中的内容如下:
第一次执行free(p)
后,p
指向的内存中的内容被覆盖 (红色部分),这时再执行p = p->next
便会产生错误,使程序卡死在这一步。
2.3. 程序漏洞测试
我们举一个例子来说明在能正常运行的前提下 (gcc编译),前言中的程序存在的漏洞。
根据多项式 x9+0x5+0x3+3x2+0xx^9 + 0x^5 + 0x^3 + 3x^2 + 0xx9+0x5+0x3+3x2+0x 创建一个多项式单链表 (如下, 这里为了方便只写出系数)。pre
和p
初始指向如图。 此时p != NULL
,进行第 111 次循环,p->coef == 1
,两指针向后移。 此时p != NULL
,进行第 222 次循环,p->coef == 0
, 删除p
指向的结点后两指针向后移。整理一下这一步得到的单链表。 此时p != NULL
,进行第 333 次循环,p->coef == 0
, 这时本应删除p
指向的结点后两指针向后移,但此时pre
指针指向的是已经被释放的结点,pre->next = p->next
改变的是已释放结点的next
域指向,从而导致漏删了一个本应删除的结点。 此时p != NULL
,进行第 444 次循环,p->coef != 0
, 两指针向后移。 此时p != NULL
,进行第 555 次循环,p->coef == 0
, 删除p
指向的结点后两指针向后移。 此时p == NULL
,循环结束,最终得到的单链表如图。
测试代码如下,这里打印出free(p)
执行前后指针变量p
中内容以及p
指向的内存中的内容。
void DelZero(PolyNode *&L) //删除系数为零的项{PolyNode *pre = L, *p = pre->next;while (p != NULL){if (p->coef == 0.0){pre->next = p->next;#ifdef DEBUGprintf("free(p) 前, p = %p, p->coef = %f, ""p->exp = %d, p->next = %p\n", p, p->coef, p->exp, p->next);#endiffree(p);#ifdef DEBUGprintf("free(p) 后, p = %p, p->coef = %f, ""p->exp = %d, p->next = %p\n\n", p, p->coef, p->exp, p->next);#endifp = pre->next;continue;}pre = p;p = p->next;}}int main(){PolyArray a[] = {{1, 9}, {0, 5}, {0, 3}, {3, 2}, {0, 1}};PolyNode *L;int n = 5;CreatePolyR(L, a, 5);printf("原多项式为: ");DispPoly(L);printf("删除系数为 0 项后为: \n\n");DelZero(L);DispPoly(L);DestroyPoly(L);return 0;}
执行结果如图所示:
可以看到,free(p)
操作执行前后,指针变量p
中内容以及p
指向的内存中的内容均未发生改变,所以在free(p)
后执行p = p->next
编译器并不会报错,但却给程序带来了逻辑上的漏洞。
2.4. 程序漏洞修复
//删除带头结点的多项式单链表中系数为 0 项 void DelZero(PolyNode *&L){PolyNode *pre = L, *p = pre->next;while (p != NULL){if (p->coef == 0.0){pre->next = p->next;free(p);+ p = pre->next;+ continue;}pre = p;p = p->next;}}
漏洞修复后运行结果如图:
2.5. 附加测试
在VSCode下使用gcc编译以下 C 代码:
测试 111
#include <stdio.h>#include <malloc.h>int main(){int *p = (int *) malloc (sizeof(int));*p = 9;printf("p = %p\n", p);printf("free(p) 前 *p = %d\n", *p);printf("-----------------\n");free(p);printf("p = %p\n", p);printf("free(p) 后 *p = %d\n", *p); printf("------------------------\n");*p = 9;printf("p = %p\n", p);printf("free(p) 后再执行 *p = 9, 此时 *p = %d\n\n", *p);return 0;}
测试 111 运行结果如下:
测试 222
#include <stdio.h>#include <malloc.h>int main(){char *q = (char *) malloc (20 * sizeof(char));q = "Hello World";printf("q = %p\n", q);printf("free(q) 前 q 指向内存中的内容为: %s\n", q);printf("--------------------------------------------\n");free(q);printf("q = %p\n", q);printf("free(q) 后 q 指向内存中的内容为: %s\n", q); printf("--------------------------------------------\n");q = "Are you OK?";printf("q = %p\n", q);printf("free(q) 后执行 q = \"Are you OK?\", 此时 q 指向内存中的内容为: %s\n\n", q);return 0;}
测试 222 运行结果如下:
可以看到,测试 111 中free(p)
后p
指向内存中的内容发生了改变,而 测试 222 中free(q)
后q
指向内存中的内容并没有发生变化,这又是为什么呢?难道gcc编译器对于是否覆盖free
后的内存中的内容还有什么规则吗?这个疑问暂时得不到解答,留待日后弄清。
3. 总结
调用free
函数释放内存后,原先指向这块内存的指针p
仍然指向这块内存,不过这时候的指向已经是不合法的了。如果这块内存被分配用作其他用途,这时再次引用指针p
(当做free(p)
之前的用途来使用) 就有可能引发未知的错误 (前言中的程序尽管在gcc编译下能够正常运行,并且内存释放后也没有再分配,但引用指向已释放内存的指针p
还是使程序产生了漏洞)。所以,在free(p)
后,最好不要出现使用指向已释放内存的指针p
的情况,必要时应将p
置为NULL
。
参考资料
动态内存分配,C语言动态内存分配详解
malloc和free函数使用注意事项,C语言malloc和free使用详解
(C语言内存十八)malloc函数背后的实现原理——内存池
ZH奶酪:C语言中malloc()和free()函数解析
C语言free释放内存后为什么指针里的值不变?竟然还可以输出?
C语言中free掉动态内存后原本指向这块内存的指针和内存里的值会有什么变化