手动管理内存的常见陷阱
  在编写 C 程序时,手动管理内存只有一个基本原则是:谁需要,谁分配;谁后使用,谁负责释放。这里的『谁』,指的是函数。也是说,我们有义务全程跟踪某块被分配的堆空间的生命周期,稍有疏忽可能会导致内存泄漏或内存被重复释放等问题。
  那些在函数内部作为局部变量使用的堆空间比较容易管理,只要在函数结尾部分稍微留心将其释放即可。一个函数写完后,首先检查一下所分配的堆空间是否被正确释放,这个习惯很好养成。这种简单的事其实根本不用劳烦那些复杂的内存回收策略。
  C 程序内存管理的复杂之处在于在某个函数中分配的堆空间可能会一路辗转穿过七八个函数,后又忘记将其释放,或者本来是希望在第 7 个函数中访问这块堆空间的,结果却在第 3 个函数中将其释放了。尽管这样的场景一般不会出现(根据快递公司丢包的概率,这种堆空间传递失误的概率大概有 0.001),但是一旦出现,够你抓狂一回的了。没什么好方法,惟有提高自身修养,例如对于在函数中走的太远的堆空间,一定要警惕,并且思考是不是设计思路有问题,寻找缩短堆空间传播路径的有效方法。
  堆空间数据在多个函数中传递,这种情况往往出现于面向对象编程范式。例如在 C++ 程序中,对象会作为一种穿着隐行衣的数据——this 指针的方式穿过对象的所有方法(类的成员函数),像穿糖葫芦一样。不过,由于 C++ 类专门为对象生命终结专门设立了析构函数,只要这个析构函数没被触发,那么这个对象在穿过它的方法时,一般不会出问题。因为 this 指针是隐藏的,也没人会神经错乱在对象的某个方法中去 delete this。真正的陷阱往往出现在类的继承上。任何一个训练有素的 C++ 编程者都懂得什么时候动用虚析构函数,否则会陷入用 delete 去释放引用了派生类对象的基类指针所导致的内存泄漏陷阱之中。
  在面向对象编程范式中,还会出现对象之间彼此引用的现象。例如,如果对象 A 引用了对象 B,而对象 B 又引用了对象 A。如果这两个对象的析构函数都试图将各自所引用对象销毁,那么程序会直接崩溃了。如果只是两个相邻的对象的相互引用,这也不难解决,但是如果 A 引用了 B,B 引用了 C, C 引用了 D, D 引用了 B 和 E,E 引用了 A……然后你可能凌乱了。如果是基于引用计数来实现内存自动回收,遇到这种对象之间相互引用的情况,虽然那程序不会崩溃,但是会出现内存泄漏,除非借助弱引用来打破这种这种引用循环,本质上这只是变相的谁后使用,谁负责释放。
  函数式编程范式中,内存泄漏问题依然很容易出现,特别是在递归函数中,通常需要借助一种很别扭的思维将递归函数弄成尾递归形式才能解决这种问题。另外,惰性计算也可能会导致内存泄漏。
  似乎并没有任何一种编程语言能够真正完美的解决内存泄漏问题——有人说 Rust 能解决,我不是很相信,但是显而易见,程序在设计上越低劣,越容易导致内存错误。似乎只有通过大量实践,亡羊补牢,塞翁失马,卧薪尝胆,破釜沉舟,久而久之,等你三观正常了,不焦不躁了,明心见性了,内存错误这种癌症会自动从你的 C 代码中消失了——好的设计品味,自然是内存友好的。当我们达到这种境界时,可能不会再介意在 C 中手动管理内存。
  让 Valgrind 帮你养成 C 内存管理的好习惯
  Linux 环境中有一个专门用于 C 程序内存错误检测工具——valgrind,其他操作系统上应该也有类似的工具。valgrind 能够发现程序中大部分内存错误——程序中使用了未初始化的内存,使用了已释放的内存,内存越界访问、内存覆盖以及内存泄漏等错误。
  看下面这个来自『The Valgrind Quick Start Guide』的小例子:
  #include
  void f(void)
  {
  int* x = malloc(10 * sizeof(int));
  x[10] = 0;
  }
  int main(void)
  {
  f();
  return 0;
  }
  不难发现,在 f 函数中即存在这内存泄漏,又存在着内存越界访问。假设这份代码保存在 valgrind-demo.c 文件中,然后使用 gcc 编译它:
  $ gcc -g -O0 valgrind-demo.c -o valgrind-demo
  为了让 valgrind 能够更准确的给出程序内存错误信息,建议打开编译器的调试选项 -g,并且禁止代码优化,即 -O0。
  然后用 valgrind 检查 valgrind-demo 程序:
  $ valgrind --leak-check=yes ./valgrind-demo
  结果 valgrind 输出以下信息:
==10000== Memcheck, a memory error detector
==10000== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==10000== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==10000== Command: ./valgrind-demo
==10000==
==10000== Invalid write of size 4
==10000==    at 0x400574: f (valgrind-demo.c:6)
==10000==    by 0x400585: main (valgrind-demo.c:11)
==10000==  Address 0x51d3068 is 0 bytes after a block of size 40 alloc'd
==10000==    at 0x4C29FE0: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==10000==    by 0x400567: f (valgrind-demo.c:5)
==10000==    by 0x400585: main (valgrind-demo.c:11)
==10000==
==10000==
==10000== HEAP SUMMARY:
==10000==     in use at exit: 40 bytes in 1 blocks
==10000==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==10000==
==10000== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==10000==    at 0x4C29FE0: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==10000==    by 0x400567: f (valgrind-demo.c:5)
==10000==    by 0x400585: main (valgrind-demo.c:11)
==10000==
==10000== LEAK SUMMARY:
==10000==    definitely lost: 40 bytes in 1 blocks
==10000==    indirectly lost: 0 bytes in 0 blocks
==10000==      possibly lost: 0 bytes in 0 blocks
==10000==    still reachable: 0 bytes in 0 blocks
==10000==         suppressed: 0 bytes in 0 blocks
==10000==
==10000== For counts of detected and suppressed errors, rerun with: -v
==10000== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
  valgrind 首先检测出在 valgrind-demo 程序中存在一处内存越界访问错误,即:
  ==10000== Invalid write of size 4
  ==10000==    at 0x400574: f (valgrind-demo.c:6)
  ==10000==    by 0x400585: main (valgrind-demo.c:11)
  ==10000==  Address 0x51d3068 is 0 bytes after a block of size 40 alloc'd
  ==10000==    at 0x4C29FE0: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
  ==10000==    by 0x400567: f (valgrind-demo.c:5)
  ==10000==    by 0x400585: main (valgrind-demo.c:11)
  然后 valgrind 又发现在 valgrind-demo 程序中存在 40 字节的内存泄漏,即:
  ==10000== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
  ==10000==    at 0x4C29FE0: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
  ==10000==    by 0x400567: f (valgrind-demo.c:5)
  ==10000==    by 0x400585: main (valgrind-demo.c:11)
  由于我们在编译时开启了调试选项,所以 valgrind 也能告诉我们内存错误发生在具体哪一行源代码中。
  除了可用于程序内存错误的检测之外,valgrind 也具有函数调用关系跟踪、程序缓冲区检查、多线程竞争检测等功能,但是无论 valgrind 有多么强大,你要做的是逐渐的摆脱它,永远也不要将自己的代码建立在『反正 valgrind 能帮我检查错误』这样的基础上。