本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内。内存错误是 C 和 C++ 编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决,它们可能严重影响应用程序,并且很少有开发团队对其制定明确的管理计划。但好消息是,它们并不怎么神秘。
- 内存泄漏
- 错误分配,包括大量增加
free()
释放的内存和未初始化的引用 - 悬空指针
- 数组边界违规
struct
或 C++ 的类,C 和 C++ 中内存管理和引用的模型在原理上都是相同的。以下内容绝大部分是“纯 C”语言,对于扩展到 C++ 主要留作练习使用。
清单 1. 简单的潜在堆内存丢失和缓冲区覆盖
void f1(char *explanation) { char p1; p1 = malloc(100); (void) sprintf(p1, "The f1 error occurred because of '%s'.", explanation); local_log(p1); } |
local_log()
对 free()
释放的内存具有不寻常的响应能力,否则每次对 f1
的调用都会泄漏 100 字节。在记忆棒增量分发数兆字节内存时,一次泄漏是微不足道的,但是连续操作数小时后,即使如此小的泄漏也会削弱应用程序。malloc()
或 new
的使用,本部分开头的句子提到了“资源”不是仅指“内存”,因为还有类似以下内容的示例(请参见清单 2)。FILE
句柄可能与内存块不同,但是必须对它们给予同等关注:清单 2. 来自资源错误管理的潜在堆内存丢失
int getkey(char *filename) { FILE *fp; int key; fp = fopen(filename, "r"); fscanf(fp, "%d", &key); return key; } |
fopen
的语义需要补充性的 fclose
。在没有 fclose()
的情况下,C 标准不能指定发生的情况时,很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等)同样值得考虑。清单 3. 未初始化的指针
void f2(int datum) { int *p2; /* Uh-oh! No one has initialized p2. */ *p2 = datum; ... } |
清单 4. 两个错误的内存释放
/* Allocate once, free twice. */ void f3() { char *p; p = malloc(10); ... free(p); ... free(p); } /* Allocate zero times, free once. */ void f4() { char *p; /* Note that p remains uninitialized here. */ free(p); } |
清单 5. 悬空指针
void f8() { struct x *xp; xp = (struct x *) malloc(sizeof (struct x)); xp.q = 13; ... free(xp); ... /* Problem! There's no guarantee that the memory block to which xp points hasn't been overwritten. */ return xp.q; } |
- 即使影响提前释放内存范围的代码已本地化,内存的使用仍然可能取决于应用程序甚至(在极端情况下)不同进程中的其他执行位置。
- 悬空指针可能发生在以微妙方式使用内存的代码中。结果是,即使内存在释放后立即被覆盖,并且新指向的值不同于预期值,也很难识别出新值是错误值。
explanation
的长度超过 80,则会发生什么情况?回答:难以预料,但是它可能与良好情形相差甚远。特别是,C 复制一个字符串,该字符串不适于为它分配的 100 个字符。在任何常规实现中,“超过的”字符会覆盖内存中的其他数据。内存中数据分配的布局非常复杂并且难以再现,所以任何症状都不可能追溯到源代码级别的具体错误。这些错误通常会导致数百万美元的损失。清单 6. 识别资源的源代码示例
/******** * ... * * Note that any function invoking protected_file_read() * assumes responsibility eventually to fclose() its * return value, UNLESS that value is NULL. * ********/ FILE *protected_file_read(char *filename) { FILE *fp; fp = fopen(filename, "r"); if (fp) { ... } else { ... } return fp; } /******* * ... * * Note that the return value of get_message points to a * fixed memory location. Do NOT free() it; remember to * make a copy if it must be retained ... * ********/ char *get_message() { static char this_buffer[400]; ... (void) sprintf(this_buffer, ...); return this_buffer; } /******** * ... * While this function uses heap memory, and so * temporarily might expand the over-all memory * footprint, it properly cleans up after itself. * ********/ int f6(char *item1) { my_class c1; int result; ... c1 = new my_class(item1); ... result = c1.x; delete c1; return result; } /******** * ... * Note that f8() is documented to return a value * which needs to be returned to heap; as f7 thinly * wraps f8, any code which invokes f7() must be * careful to free() the return value. * ********/ int *f7() { int *p; p = f8(...); ... return p; } |
- 专用库
- 语言
- 软件工具
- 硬件检查器
*alloc()
和 free()
或者 new
和 delete
的源主体。人工查看此类内容通常会出现像清单 7 中一样的问题。清单 7. 棘手的内存泄漏
static char *important_pointer = NULL; void f9() { if (!important_pointer) important_pointer = malloc(IMPORTANT_SIZE); ... if (condition) /* Ooops! We just lost the reference important_pointer already held. */ important_pointer = malloc(DIFFERENT_SIZE); ... } |
condition
为真,简单使用自动运行时工具不能检测发生的内存泄漏。仔细进行源分析可以从此类条件推理出证实正确的结论。我重复一下我写的关于风格的内容:尽管大量发布的内存问题描述都强调工具和语言,对于我来说,最大的收获来自“软的”以开发人员为中心的流程变更。您在风格和检测上所做的任何改进都可以帮助您理解由自动化工具产生的诊断。lint
、严格编译 和几种商业产品执行的内容:扫描编译器接受的源文本和目标项,但这可能是错误的症状。lint
已过时,并有一定的局限性,但是,没有使用它(或其较高级的后代)的许多程序员犯了很大的错误。通常情况下,您能够编写忽略 lint
的优秀的专业质量代码,但努力这样做的结果通常会发生重大错误。其中一些错误影响内存的正确性。与让客户首先发现内存错误的代价相比,即使对这种类别的产品支付最昂贵的许可费也失去了意义。清除源代码。现在,即使 lint
标记的编码可能向您提供所需的功能,但很可能存在更简单的方法,该方法可满足 lint
,并且比较强键又可移植。-g
标记生成的调试版本)、练习相关应用程序和研究由工具自动生成的报告。请考虑如清单 8 所示的程序。清单 8. 示例错误
int main() { char p[5]; strcpy(p, "Hello, world."); puts(p); } |
-
JS 三大难点
1,作用域链2,原型链3,闭包
原型链 作用域链 -
“C语言解惑课堂”疑难点答疑内容更新
“C语言解惑课堂”疑难点答疑内容更新
C语言C程序C/C++
举报文章
请选择举报类型
补充说明
0/200
上传截图
格式支持JPEG/PNG/JPG,图片不超过1.9M