Bootstrap

C语言精华:指针有害健康?

黑暗的内存

很多人对 C 语言深恶痛绝,仅仅是因为 C 语言迫使他们在编程中必须手动分配与释放内存,然后通过指针去访问,稍有不慎可能就会导致程序运行运行时出现内存泄漏或内存越界访问。

C 程序的内存泄漏只会发生在程序所用的堆空间内,因为程序只能在堆空间内动态分配内存。NULL 指针、未初始化的指针以及引用的内存空间被释放了的指针,如果这些指针访问内存,很容易就让程序挂掉。

除了堆空间,程序还有个一般而言比较小的栈空间。这个空间是所有的函数共享的,每个函数在运行时会独占这个空间。栈空间的大小是固定的,它是留给函数的参数与局部变量用的。栈空间有点像宾馆,你下榻后,即使将房间搞的一团糟,也不需要你去收拾它,除非你把房间很严重的损坏了——用 C 的黑话来说,即缓冲区溢出。

虽然导致这些问题出现的原因很简单,但是却成为缺乏编程素养的人难以克服的障碍,被 C 语言吓哭很多次之后,他们叛逃到了 Java、C# 以及各种动态类型语言的阵营,因为这些语言将指针隐藏了起来,并提供内存垃圾回收(GC)功能。他们赢了,他们懒洋洋的躺在沙发上,拿着遥控器指挥内存,信号偶尔中断,内存偶尔紊乱。

C 内存的动态分配与回收

C 语言标准库(stdlib)中为堆空间中的内存分配与回收提供了 malloc 与 free 函数。例如,在下面的代码中,我们从堆空间中分配了 7 个字节大小的空间,然后又释放了:

#include

void *p = malloc(7);

free(p);

一点都不难!跟你去学校图书馆借了 7 本书,然后又还回去没什么两样。有借有还,再借不难,过期不还,就要罚款。有谁因为去图书馆借几本书就被吓哭了的?在这里我退荐一个关于编程的QQ交流群,大家有兴趣的可以来群里面一起交流哦!里面还有整理出来的一些关于学习的资料呢。

我们也可以向堆空间借点地方存储某种类型的数据:

int *n = malloc(4);

*n = 7;

free(n);

如果你不知道 int 类型的数据需要多大的空间才能装下,那就用 sizeof,让 C 编译器去帮助你计算,即:

int *n = malloc(sizeof(int));

*n = 7;

free(n);

策略与机制分离

在 C 语言中有关内存管理的机制已经简单到了几乎无法再简单的程度了,那么为何那么多人都在嘲笑讥讽挖苦痛骂诅咒 C 的内存管理呢?

如果你略微懂得一些来自 Unix 的哲学,可能听说过这么一句话:策略与机制分离。如果没听说过这句话,建议阅读 Eric Raymond 写的《Unix 编程艺术》第一章中的 Unix 哲学部分。

malloc 与 free 是 C 提供的内存管理机制,至于你怎么去使用这个机制,那与 C 没有直接关系。例如,你可以手动使用 malloc 与 free 来管理内存——最简单的策略,你也可以实现一种略微复杂一点的基于引用计数的内存管理策略,还可以基于 Lisp 之父 John McCarthy 独创的 Mark&Sweep 算法实现一种保守的内存自动回收策略,还可以将引用计数与 Mark&Sweep 这两种策略结合起来实现内存自动回收。总之,这些策略都可以在 C 的内存管理机制上实现。

借助 Boehm GC 库,就可以在 C 程序中实现垃圾内存的自动回收:

#include

#include

#include

 

int main(void)

{

    GC_INIT();

    for (int i = 0; i

在 C 程序中使用 Boehm GC 库,只需用 GC_MALLOC 或 C_MALLOC_ATOMIC 替换 malloc,然后去掉所有的 free 语句。C_MALLOC_ATOMIC 用于分配不会用于存储指针数据的堆空间。

如果你的系统(Linux)中安装了 boehm-gc 库(很微型,刚 100 多 Kb),可以用 gcc 编译这个程序然后运行一次体验一下,编译命令如下:

$ gcc -lgc test-gc.c

GNU 的 Scheme 解释器 Guile 2.0 就是用的 boehm-gc 来实现内存回收的。有很多项目在用 boehm-gc,只不过很少有人听说过它们。

如果 C 语言直接提供了某种内存管理策略,无论是提供引用计数还是 Mark&Sweep 抑或这二者的结合体,那么都是在剥夺其他策略生存的机会。例如,在 Java、C# 以及动态类型语言中,你很难再实现一种新的内存管理策略了——例如手动分配与释放这种策略。

Eric Raymond 说,将策略与机制揉在一起会导致有两个问题,(1) 策略会变得死板,难以适应用户需求的改变;(2) 任何策略的改变都极有可能动摇机制。相反,如果将二者剥离,就可以在探索新策略的时候不会破坏机制,并且还检验了机制的稳定性与有效性。

Unix 的哲学与 C 有何相干?不仅是有何相干,而且是息息相关!因为 C 与 Unix 是鸡生蛋 & 蛋生鸡的关系——Unix 是用 C 语言开发的,而 C 语言在 Unix 的开发过程中逐渐成熟。C 语言只提供机制,不提供策略,也正因为如此才招致了那些贪心的人的鄙薄。

这么多年来,像 C 语言提供的这种 malloc + free 的内存管理机制一直都没有什么变化,而计算机科学家们提出的内存管理策略在数量上可能会非常惊人。像 C++ 11 的智能指针与 Java 的 GC 技术,如果从研究的角度来看,可能它们已经属于陈旧的内存回收策略了。因为它们的缺点早就暴露了出来,相应的改进方案肯定不止一种被提了出来,而且其中肯定会有一些策略是基于概率算法的……那些孜孜不倦到处寻找问题的计算机科学家们,怎能错过这种可以打怪升级赚经费的好机会?

总之,C 已经提供了健全的内存管理机制,它并没有限制你使用它实现一种新的内存管理策略。

手动管理内存的常见陷阱

在编写 C 程序时,手动管理内存只有一个基本原则是:谁需要,谁分配;谁最后使用,谁负责释放。这里的『谁』,指的是函数。也就是说,我们有义务全程跟踪某块被分配的堆空间的生命周期,稍有疏忽可能就会导致内存泄漏或内存被重复释放等问题。

那些在函数内部作为局部变量使用的堆空

;