甩掉强迫症
  选择用 C 语言来写程序,这已经让我们牺牲了很多东西——项目进度、漂亮的桌面程序、炙手可热的网站前端……如果你再为 C 语言的一些『脆弱』之处患上强迫症,这样的人生太过于悲催。用 C 语言,要对自己好一点。
  负责分配内存的 malloc 函数可能会遇到内存分配失败的情况,这时它会返回 NULL。于是,问题来了,是否需要在程序中检测 malloc 的返回值是否为 NULL?我觉得没必要检测,只需记住 malloc 可能会返回 NULL 这一事实即可。如果一个程序连内存空间都无法分配了,那么它还有什么再继续运行的必要?有时,可能会因为系统中进程存在内存泄漏,导致你的程序无法分配内存,这时你使用 malloc 返回的 NULL 指针来访问内存,会出现地址越界错误,这种错误很容易定位,并且由于你知道 malloc 可能会返回 NULL 这一事实,也很容易确定错误的原因,实在不济,还有 valgrind。
  如果确实有对 malloc 返回值进行检查的必要,例如本文评论中 @依云 所说的那些情况,可以考虑这样做:
#include
#include
#define SAFE_MALLOC(n) safe_malloc(n)
void * safe_malloc(size_t n) {
void *p = malloc(n);
if (p) {
return p;
} else {
printf("你的内存不够用,我先撤了!n");
exit(-1);
}
}
int main(void) {
int *p = SAFE_MALLOC(sizeof(int));
... ... ...
return 0;
}
  如果你被我说服了,决定不去检查 malloc 的返回值是否为 NULL,那么又一个问题随之而来。我们是否需要在程序中检测一个指针是否为 NULL?NULL 指针实在是太恐怖了,直接决定了程序的生死。为了安全起见,在用一个指针时检测一下它的值是否为 NULL 似乎非常有必要。特别是向一个函数传递指针,很多编程专家都建议在函数内部首先要检测指针参数是否为 NULL,并将这种行为取名为『契约式编程』。之所以是契约式的,是因为这种检测已经假设了函数的调用者可能会传入 NULL……事实上这种契约非常容易被破坏,例如:
  void foo(int *p)
  {
  if(!p) {
  printf("You passed a NULL pointer!n");
  exit(-1);
  }
  ... ... ...
  }
  int main(void)
  {
  int *p;
  foo(p);
  return 0;
  }
  当我将一个未初始化的指针传给 foo 函数时,foo 函数对参数的检测不会起到任何作用。
  可能你会辩解,说调用 foo 函数的人,应该先将 p 指针初始化为 NULL,但这有些自欺欺人。契约应当是双方彼此达成一致意见之后才能签署,而不是你单方面起草一个契约,然后要求他人必须遵守这个契约。foo 不应该为用户传入无效的指针而买单,何况它也根本无法买这个单——你能检测的了 NULL,但是无法检测未初始化的指针或者被释放了的指针。也许你认为只要坚持将指针初始化为 NULL,并坚持消除野指针,那么 foo 中的 NULL 检测是有效的。但是很可惜,野指针不是那么容易消除,下面会讨论此事。凡是不能彻底消除的问题,不应该再浪费心机,否则只是将一个问题演变了另一个问题而已。那些被重重遮掩的问题,一旦被触发,你会更看难看清真相。
  指针不应该受到不公正待遇。如果你处处纠结程序中用到的整型数或浮点数是否会溢出,或者你走在人家楼下也不是时时仰望上方有没有高空坠物,那么也不应该对指针是否为 NULL 那么重视,甚至不惜代价为其修建万里长城。在 C 语言中,不需要指针的 NULL 契约,只需要遵守指针法律:你要传给我指针,必须保证你的指针是有效的,否则我用程序崩溃来惩罚你。
  第三个问题依然与 NULL 有关,那是一个指针所引用的内存空间被释放后,是否要将这个指针赋值为 NULL?对于这个问题,大家一致认为应该为之赋以 NULL,否则这个指针成为『野指针』——野指针是有害的。一开始我也这么认为,但是久而久之觉得消除野指针,是一种很无聊的行为。程序中之所以会出现野指针引发的内存错误,往往意味着你的代码出现了拙劣的设计!如果消除野指针,再配合指针是否为 NULL 的检测,这样做固然可以很快的定位出错点,但是换来的经常是一个很脏的补丁式修正,而坏的设计可能会继续得到纵容。
  如果你真的害怕野指针,可以像下面这样做:
#include
#include
#define SAFE_FREE(p) safe_free((void **)(&(p)))
void safe_free(void **p) {
if(*p) {
free(*p);
*p = NULL;
} else {
printf("哎呀,我怕死野指针了!n");
}
}
int main(void) {
int *p = malloc(sizeof(int));
for(int i = 0; i
  对于所引用的内存被释放了的指针,即使赋之以 NULL,也只能解决那些你原本一眼能看出来的问题。更糟糕的是,当你对消除野指针非常上心时,每当消除一个野指针,可能会让你觉得你的程序更加健壮了,这种错觉反而消除了你的警惕之心。如果一块内存空间被多个指针引用,当你通过其中一个指针释放这块内存空间之后,并赋该指针以 NULL,那么其他几个指针该怎么处理?也许你会说,那应该用引用计数技术来解决这样的问题。引用计数的确可以解决一些问题,但是它又带来一个新的问题,对于指针所引用的空间,在引用计数为 0 时,它被释放了,这时另外一个地方依然有代码在试图 unref,这时该怎么处理?
  的不去检测指针是否为 NULL 肯定也不科学。因为有时 NULL 是作为状态来用的。例如在树结构中,可以根据任一结点中的子结点指针是否为 NULL 来判断这个结点是否为叶结点。有些函数通过返回 NULL 告诉调用者:『我可耻的失败了』。我觉得这才是 NULL 真正的用武之地。
  王垠在『编程的智慧』一文中告诫大家,尽量不要让函数返回 NULL,他认为如果你的函数要返回『没有』或『出错了』之类的结果,尽量使用 Java 的异常机制。这种观点也许是对的,但是鉴于 C 没有异常机制(在 C 中可以用 setjmp/longjmp 勉强模拟异常),只有 NULL 可用。有些人形而上学强加附会的将这种观点解读为让函数返回 NULL 是有害的,甚至将这种行为视为『低级错误』,甚至认为 C 指针的存在本身是错误,认为这样做是整个软件行业 40 多年的耻辱,这是小题大作,或者说他只有能力将罪责推给 NULL,而没有能力限制 NULL 的副作用。如果我们只将 NULL 用于表示『没有』或『出错了』的状态,这非但无害,而且会让代码更加简洁清晰。
  如果你期望一个函数能够返回一个有效的指针,那么你有义务检查它是不是真的返回了有效的指针,否则没必要检查。这种检查其实与这个函数是否有可能返回 NULL 无关。类似的 NULL 检查,在生活中很常见。即使银行的 ATM 机已经在安全性上做了重重防御,但是你取钱时,也经常会检查一下 ATM 吐出来钱在数目上对不对。你过马路时,虽然有红绿灯,而且司机师傅也都是通过驾照考试的,但你依然会有意识的环顾左右,看有没有正在过往的车辆。
  如果你通过一个查询函数在 C 式的泛型容器中查询一个元素(其实是指针),而容器中没有这个元素,那么查询函数返回一个 NULL,这是一个无效的指针,这时,你可以认为查询函数内部出错了,也可以认为查询函数告诉你容器中没有这个元素。这种情况下,查询函数的返回结果出现了二义性,但是这难道不可以视为是重新检验查询函数正确性的好机会么?你可以自行遍历你所用的容器中是否存在要查询的元素,如果确定有,那么可以肯定是刚才你用的查询函数有 bug。如果容器中的确没有这个元素,那么查询函数的正确性得到了检验。如果你坚持 NULL 的意义不明确而导致歧义,然后得出推论『返回 NULL 的函数是有害的』,那么马克思都会比你善于写程序。
  当你打算检测一个指针的值是否为 NULL 时,问题又来了……我们是应该
  if(p == NULL) {
  ... ... ...
  }
  还是应该
  if(!p) {
  ... ... ...
  }
  ?
  很多人害怕出错,他们往往会选择第一种判断方式,他们的理由是:在某些 C 的实现(编译器与标准库)中,NULL 的值可能不是 0。这个理由,也许对于 C99 之前的 C 是成立的,但是至少从 C99 不再是这样了。C99 标准的 6.3.2.3 节,明确将空指针定义为常量 0。在现代一些的 C 编译器上,完全可以放心使用更为简洁且直观的第二种判断方式。
  用 C 语言,不要想太多。想的太多,你可能不会或者不敢编程了。用 C 语言,你又必须想太多,因为不安全的因素到处都有,但是也只有不安全的东西才真正是有威力的工具,刀枪剑戟,车铣刨磨,布鲁弗莱学院传授的挖掘机技术,哪样不能要人命!不要想太多,指的是不要在一些细枝末节之处去考虑安全性,甚至对于野指针这种东西都诚惶诚恐。必须想太多,指的是多从程序的逻辑层面来考虑安全性。出错不可怕,可怕的是你努力用一些小技俩来规避错误,这种行为只会导致错误向后延迟,延迟到基于引用计数的内存回收,延迟到 Java 式的 GC,延迟到你认为可以高枕无忧然而错误却像癌症般的出现的时候。
  不好的设计品味
  上文谈到,内存错误往往是由不良的设计导致的。我不确定怎样的设计算是好品味的,但是我可以确定一些品味不怎么样的设计。
  第一种品味不怎么样的设计是割裂算法与数据结构的关系。这种设计源于对教科书的盲目信仰。几乎任何一本讲数据结构与算法的书都会煞有介事的告诉你 程序 = 数据结构 + 算法。这个『公式』是 Pascal 之父 Nicklaus Wirth 提出的,但实际上形同废话,类似于 英语 = 单词 + 语法。可是这种废话却让许多人形而上学了。有些人坚定不移的确信,只要把数据结构设计正确了,正确的算法不言自明,于是他们从面向对象开始设计程序。还有些人坚定不移的确信,只要将算法设计正确了,正确的数据结构不言自明,于是他们从算法设计开始。这都是不学习马克思哲学的下场。
  马克思哲学是一门注重迭代的哲学,马克思经常说,算法与数据结构是矛盾的,二者统一于程序之中;算法决定了数据结构,数据结构又反过来影响算法。马克思说的肯定不是废话,任何一个有素养的程序员都不会否定迭代设计。任何一个程序在诞生之初都不是完美的,但是负责任的设计者会努力使之进化,趋向完美。如果马克思学编程,他一定会将泛型编程与面向对象编程这两大范式统一起来,左手画圆,右手画方,傲视群雄。达尔文也说过,程序不是设计出来的,而是进化出来的。
  第二种品味不怎么样的设计是面向某种编程范式。我在『面向指针编程』一文中捏造了一个很简单的例子,然后将面向对象、泛型编程以及函数式编程中基本的手段穿插在一起,结果可以得到一个思路清晰、代码简洁的小程序。其实这个例子源自很多年前我自己写的一个双向链表模块,在用 C 构建稍微有点规模的程序或库时,这些手段都是基本的。事实上,当时我在写这些代码时,并不怎么懂面向对象、泛型编程以及函数式编程这些烧脑的概念,完全是为了解决我面对的一些小问题,自然而然的用上了这些手段。很多年后我才隐约发现这些手段竟然对应着一种又一种编程范式的雏形。
  我应该庆幸,除了指针与宏之外没有任何特性的 C 语言没有给我带来太多难以理解的概念,以至于我可以直接面向问题编程。C 语言的发明者 Dannis Ritche 说,A language that doesn’t have everything is actually easier to program in than some that do. 翻译过来,是『不试图拥有一切的语言实际上要比那些试图拥有一切的语言更易于编程』。
  大部分情况下,我们写的代码在逻辑上并不复杂,所以各种编程范式看上去都同样有效——如果你说你能用面向对象解决问题,那么肯定会有人说他用函数式编程也能解决同样的问题。大部分程序在数据结构与算法方面只用到了线性表与排序算法,程序中大部分代码是与问题领域息息相关的。像树与图这种数据结构及相关的算法,往往已经以库的形式被实现了。连遗传算法、神经网络算法以及支持向量机这些复杂的算法也有现成的库可调用。当问题足够复杂时,你会发现任何编程语言、范式以及框架都没法帮助你解决问题,它们甚至对你如何理解问题都没有任何帮助。在我看来,任何编程范式在本质上都是代码层面的『图形界面(GUI)』,它们并不能真正代表设计上的好品味。如果你的程序是持续进化的,那么它总是有可能进化为适合它的那些编程范式。如果莱布尼茨、欧拉、费马等大神改行写程序,他们一定能写出具有小作用量的程序,而不是背负一大堆编程范式所带来的各种包袱的程序。
  第三种品味不怎么样的设计是不为自己的设计提供有效的文档,主要表现为 (1) 文档写的比代码还烂;(2) 文档不能反映新的代码;(3) 干脆不写文档,让他人去 read the fucking source code. 如果你能够长期维护你所写的代码,不提供有效的文档也不是什么大事,否则你让别人去阅读你写的 fucking source code,那是对他人的侮辱。因为你是面向机器写代码,他人要想读懂你的代码,必须通过代码去逆向还原你的思路。一些水平很烂的的程序员,企图像 Linus 那样高调,趾高气扬的让我们去阅读他们写的代码,这种行为无异于让我们从其排泄物中猜测他们中午在哪个馆子吃了什么饭……结果往往是他们的代码很快死掉了。这个世界上能实现代码即文档,文档即代码的人几乎没有。即使软件世界的泰山北斗 Donald Knuth 老先生也得借助文式编程的方式来注解自己的代码,也是说他只能达到又能写书又能写代码的境界,即便如此,这个星球上能与之比肩的人寥寥无几。
  我又成功的跑题了。其实我想说的是,与这些品味不好的设计相比,用 C 管理一下内存所带来的繁琐与困难根本不值一提。请记住 Peter Norvig 说的,学会编程通常需要十年,为何人人都这么着急?