我的 C 语言学习生涯记——纪念 Dennis M. Ritchie

本文讲述了作者从高中时期接触计算机,到大学深入学习C语言的过程,提到了DOS、BASIC、QBASIC、C++等编程语言,以及对Dennis M. Ritchie的敬仰。作者在学习中逐渐理解了C语言的魅力,包括指针、内存管理和RAII等概念,还探讨了Windows API编程。文章表达了对Dennis M. Ritchie的怀念,并强调了C语言在系统编程和操作系统中的重要地位。
摘要由CSDN通过智能技术生成
我的 C 语言学习生涯记——纪念 Dennis M. Ritchie

(本文应 gaobo 要求而写,以纪念我们永远的 Dennis M. Ritchie 老师)

让我把时间的车轮倒转,回到 1998 年下半年。那个时候,我是交大附中高二的一名学生。尽管因为在理科班,学习物理并参加竞赛是我的重心所在,但是,心中对于计算机那颗神秘的心,还是满怀着十分的期待。

记得六年前,在从一个默默无闻的茂名北路小学,刚进入市重点育才中学的时候,见到了天文台,见到了物理实验室、生物实验室,也见到了从未见过的一台一台大大的、有着超越一般计算器能力的东东——电子计算机,老师们都称它们为 80286 微机。第一堂计算机课就学习了 CCDOS 中文输入,领教了 DOS 的 Bad command or file name,被同学说“你的名字不好,输进去成了 bad 什么什么 name”。而老师则进一步让我们用 BASIC 做计算器用,只要键入“?123*456”马上就会出现 56088 这样的数字等等。

记得三年前,也就是初二的时候,刚上了第一门计算机课。学习了 WPS 与 LOGO,也在兴趣小组上第一次把书上的 BASIC 程序输入电脑,做出了我史上第一个 BASIC 程序,领略了 FOR 语句的奥妙之后,却除了上课之外,很少有机会接触电脑,因此更增加了我对计算机编程的渴望。WPS 是我们主要学习的课程之一,它能编辑各种文件,打印各种稿子,模拟显示打印结果,并且支持加密——虽然早期版本只要用“Ctrl+求伯君”就能解密;可惜没法用来编程。LOGO 可以用来画画,但是老师并未教我们画线、画方、画圆之外更复杂的编程,所以也还是不会,只记得 LOGO 1.0 里面输入 .DOS 回车就能与它 bye-bye。

而如今,高中计算机课上老师教我们使用 FOXBASE 编程,虽然能用到了数据库这样比较高级的东西,它的容量能比内存还大,但是总觉得不是我最喜欢的程序。

在我自学了有限量的一点点 BASIC/QBASIC 之后,看了一本引人入胜的 DOS 书——一本清华大学出版社的《DOS 6.22 使用手册》。书的封面是墨绿色的,上面用楷体印着作者郑全战、成秀珍的名字。可曾想,数年后郑全战大哥在微软工作了,又数年后,去了腾讯做了首席架构师,而我,则数年后仍未做到我最想做的工作,乃至我最想做的工作已经不再是行业的热点了,但却在业余时默默地构建着心中的理想。当时,心里最想做的则是如何能写出像 HIMEM、EMM386、SMARTDRV、RAMDRIVE 这样能在 DOS 的系统级别上工作的程序,以及像 UCDOS 里的那个时钟那样的 TSR。

其实,高中时有时候实在想摸一摸计算机了,会去全奔同学家玩玩他家的东海 396A 电脑。这台电脑叫是叫做 396A,实际则是一台 386。虽然牌子是东海,但却感觉和 AST、HP 等国外品牌不相上下。特别东海的键盘手感很清脆,打上去不紧不松,也不粘手,国产机器能做成这样真的是不错的。

一到他的 386 上,用我的软盘启动,或者用他的硬盘启动。我的软盘上的东东,都是我东拼西凑出来的,里面的 DOS 命令有的是 MS-DOS 5.0 的,有的是 6.2 的,有时候一不小心某个工具就报 Incorrect DOS version,搞得我十分难堪。有几个工具是从全奔同学的电脑上拷来的:PCTools 5.0——整个就一个命令 PC.EXE,ARJ 压缩程序,HD-COPY 软盘格式化与复制工具。还拿了三张 3 寸盘装了个他已安装好的 Windows 3.1。那时也不懂,觉得拷的都是些小软件,感觉像是本来就是公共领域似的,后来才知道这也叫盗版。

带着这些小东东,我在学校的电脑上一启动,用 SMARTDRV 一开,第一次运行 FOXBASE 要 10 秒,第二次运行,1 秒之后就出来了。有一次跟我同学说,你看我这样 A:\> 提示符上输入 FOXBASE 回车,要多久?结果一回车连软盘灯都没亮,FOXBASE 就出来了,后来才知道我是用了 PROMPT A:\$G,把当前目录设在 RAMDRIVE 里来蒙人的。

这样东拷西拷的软件果然也带了问题。一次我拿着我软盘去我表姐夫家用了一下。然后他过来查些资料,结果发现他的硬盘被感染病毒了,费了他好大工夫去杀毒。然后那次之后我去全奔同学家,他也帮我的软盘用 KV200 杀了一次毒,查出来个 DieHard,不过据说这个 KV200 也不是最新的,也不知道是不是把病毒全都清除了。

虽然只会 QBASIC,但曾经却有些惊人的梦想——想做一个像钱龙一样的股票分析软件、想做并做成了一个汉诺塔演示程序、想做但没最终实现的一个地址簿软件。那个股票分析软件是因为看了一些技术分析的书,很想把那些技术指标的算法写出来的缘故;那个汉诺塔程序大小约 8KB,一开始全部写在纸上,然后拿着纸到全奔同学家输入并调试,最终成功运行;那个地址簿软件是想做给当时心目中喜欢的一位女生的,写了 5 页纸,只是最后由于使用语言是非结构化的传统 BASIC,以至于结构上遇到难以解决的瓶颈了,最后只能中止。

正是在写那个地址簿程序的时候,我意识到了我一向习惯的 OPTION BASE 1,也就是让 BASIC 中数组默认下标从 1 开始而不是从 0 开始,给我带来了很多麻烦。人们数数也一般从 1 开始,但是一旦要把多维数组的位置映射到一维数组时,1, 1, 1 这样的下标将会给计算带来麻烦;也正如此,之后学 C 语言的时候,对那下标从 0 开始的数组感觉很亲切。

在学校里,每天下午如果只上二节课的话,我就会在课后走进图书馆阅览室,交上学生证拿着插书牌,走到最后一个书架那里。那里有我最感兴趣的杂志合订本——《电脑爱好者》。虽是 1998 年了,合订本却是 1997 的。但是,这早已满足我的需要了。里面有许多有趣的文章。有次看到一篇讲主引导扇区和分区表的,我用自己的笔记本抄了一部分下来,至今也影响了我,我的个人主页上的主引导扇区和分区表的文章到现在还在维护呢。而且,有一次我的老电脑的分区表被 PQMagic 不小心搞糟了,分区表乱掉了,系统进不去了,文件拷不出了,那次我真的差点觉得我彻底完了——我只有这一台电脑,只有这一个硬盘,里面的东西大多数都没备份……后来倒靠着 Norton Utilities 2002 的 DOS 版中的两个工具 NDD.EXE 和 DISKEDIT.EXE 加上我对分区表结构的回忆,再加上一个 10 位精度的科学计算器用来算分区地址,把四个分区找了回来……

而 1998 年的电脑爱好者合订本,里面有一篇有关如何在 Turbo C 中让程序使用鼠标的文章,让我对 C 语言产生了兴趣。只可惜学校没有教过,所以一直不会用。那篇文章中的代码,我用笔记簿抄了下来,但是那本本子不知道什么时候被我爸扔掉了,所以一直没能真正用上那段代码。

于是除了图书馆阅览室,我去外借室看了看,趁寒假,借了本 C 语言的书回家。那本书里称 C 语言为“中级语言”,因为像 BASIC 之类的高级语言抽象层次较高,而汇编这样的低级语言又太接近机器,于是 C 语言就成为了在汇编之上一层的“中级”语言。但是那本书(不是老谭的,放心吧,本人的学习历程中没看过他老人家的书)太深了一点,看不大懂。而且自己就没有电脑。于是只能作罢。话说外借室边上还有个视频资料阅览室,里面有录像机能放录像带看,没想到我们学校还有这么好的地方。

年复一年,突然间已经高三了,由于我们这届正好遇到有“直通车”的大学招生计划,因此我们班里有 20 名同学直接就被大学录取。只是经过了一个不太难的综合考。当然,去哪所大学,以及去哪个专业,并不完全是自己说了算的,要看大学的名额的。清华、北大的名额相当少,而且专业都是他们说了算,我们班学习成绩最好、会说六国语言的伯君同学,进入了清华的材料专业。而韩同学则进入了北大。其他同学则分别进入了复旦和交大。而我从小就受我那在交大的伯伯的影响,很想进交大,于是就很自然地进了交大。

进了交大以后,学习计算机语言,是程国英老师教我们的。她那时已是软件学院的老师,当然有些老师是计算机系和软件学院共享的。程老师用的教材是钱能版 C++。虽然这本书在后来也发现有许多不好的地方,比如模板介绍得太少太不完整、标准库用法是老的 DOS 里的用法,不是新的标准用法等等,但是作为入门来说已经很不错了。

其时,我在家里有了我自己的电脑。电脑上装了 Windows 98,但我也有时不启动 Windows,通过编辑 MSDOS.SYS(Win98 里它只是个纯文本文件)只启动它自带的 DOS 7.1 环境。我学 C 语言那会儿,就是习惯在纯 DOS 里使用 QUICK C 2.5。纯 DOS 环境的好处,在于单任务,强迫我不能分心。可以休息时去喝咖啡,但让我不至于在电脑上玩游戏。

堂姐和她老公已经去了美国。许多书籍,一部分扔了,一部分我要的留下给我。其中有两本 QUICK C 1.0 使用手册,北京希望出版社的。虽然有点旧,但味道正宗,里面深入浅出地讲了如何使用 QUICK C,如何学习 C 语言等等。说实话这本书在我入门的路上帮助很大,让我少走了许多弯路,让我很快地就理解了指针、结构体、联合等的概念。再加上钱能的书,双管齐下,进步显著。所以到了最后考试的时候,什么难题都难不倒我。不过奇怪的是,程老师让一位同学,我高中时的班长,去调试程序;他是学文科的,进我们理科班是因为学习成绩好的缘故,我估计他计算机不太擅长;结果他说我的程序跑不出来,电话里问我,我说我的程序至少逻辑都对的,语法有点小问题,你帮帮忙改改吧,可他还是搞不定。算了,也只能这样了。由于这是大一开始前的课程,各门课的成绩下来,语文成绩刚及格,高等代数挂科,于是就被分进了生存压力较小的计算机试点班,未能有机会进入联读班的门坎。

直到那时为止,虽然已知道 C、UNIX、XENIX 的大名,但对其之父还是不太了解,只是隐隐约约地知道 K&R 的名字,后来了解到 K 是 Brian Kernighan。倒是 C++ 之父 Bjarne Stroustrup 的大名如雷贯耳。但是在日后的学习中,逐渐逐渐接触到更多的大牛和之父的名字,也让我有越来越多的偶像,虽然不像追星一样,但是看见他们的名字就像见到真人一样。虽然他们搞计算机的年代各不相同,但是有一点是共同的,他们谱写了计算机科学与技术的历史。在此向 Dijkstra、Donald E. Knuth、Ken Thompson、Dennis Ritchie、Brian Kernighan、Rob Pike、David Cutler、Andrew S. Tanenbaum、Ron Rivest、William Henry Gates III、Steve Jobs、David Solomon、Jeffrey D. Ullman、Marshall Kirk McKusick、Anders Hejlsberg、Don Box、Jeffrey Richter、John Robbins、Linux Torvalds、Bram Moolenaar、Matt Pietrek、Mark Russinovich、Dan Bornstein、Owlman Ling、Daniel Gao 等等等等过去、现在和未来的大牛致敬!

记得 C 语言的第一块语法糖就是 ANSI C 的函数头部分。老的 C 的函数头如此写:

int main(argc, argv)
int argc;
char **argv;
{
    /* ... */
}

新的这样写:

int main(int argc, char **argv)
{
    /* ... */
}

这使我对 ANSI C 产生了由衷的喜爱。两年后觉得这对于写 C 的编译器也增加了些难度:

int (*func1(int a, char **b))();
int (*func2(int a, char **b))(void);
int (*(*func3(int a, char **b))(void))[3];
int (*(*func4(int (*a)(int b), char **b))(void))[3];

但是的确很折腾人,无聊的时候花点时间研究这个不错。只可惜在大学里的岁月,翘了太多课,玩了太多 CS 和 SC 还有 NFS,没有好好学习,挂课两门,好几门成绩不理想,考研没考上,唉……这样,大学时候的过分不用功导致了后来的过分用功——工作之后读了计算机技术工程硕士,在此期间买了近 500 元的书,一本本看过去,包括数据库的、网络的、UNIX 的、密码学的、Windows API 的,等等等等。还自己装了个 Fedora Core 2,虽然里面有许多小 bug,但仍然用得乐此不疲,乃至后来还用了 Slackware。后来 08 年开始则好久没玩 Linux 了。

回到我的大学生活。大学里 C 和 C++ 是语言学习的重头戏。我们这届虽然用过一点 Java,但几乎是自学的,而我当时的自学方法也有限,不懂得用官网和 Google 双管齐下,而且看的书是偏理论的 Thinking in Java,因此根本没有学好。而 C/C++ 却成了我最喜欢的两门语言。

刚开始的 C++ 课程,老师让我们用的都是基于 DOS 的编译器,像 Turbo C++ 之类。后来才开始用 VC5 之类 Windows 上的更新一点的编译器。老的编译器支持的语言标准也比较落后,像 Turbo C++ 都不支持异常处理的。新的编译器支持这些新特性之后,许多功能用起来也更顺理成章一点。

发现但凡学 C/C++,初学者首先要学好的一点就是:指针只是一个整型或长整型数字,它不包含数组的长度,并且仅此而已,甚至在运行时没有办法完美地验证它的值是否有效,因此事先应确保其有效。对于静态分配的数组,在编译时知道它的长度,但是在运行时也是没有办法获取的,因此必要时应传递它的静态长度。对于动态分配的数组么,其实对 C 来说根本没有动态分配的数组这样的概念。malloc 分配出来的只是一个内存块,你爱把它当什么用就当什么用。理论上 malloc 有可能知道一个指针指向的内存块是多大的,有可能封装在某个未公开的函数中,但是实际上除了调试或验证程序的正确性之外,程序不应该依赖于这样的判断。

第二点关键的就是 C 语言里面函数参数或返回值的传递都是 by value(按值传递)的,不是 by reference(按引用传递)的。这个我不大能翻译准确,解释一下:如下的 swap 函数是不工作的:

void swap(int a, int b)
{
    int tmp;

    tmp = a;
    a = b;
    b = tmp;
}

如果运行 swap(int1, int2); 那么 int1 的值会被复制给 swap 栈上的 int a(如果看了汇编就会明白,int a 是栈上的另一个变量,或者是一个保存在寄存器中的变量);然后进一步对 int a 和 int b 操作;最后 int1 和 int2 的值都没变过,变的只是 swap 的栈上的变量。所谓按值传递,也就是说,所有参数都是复制过去的。但要注意,复制的只是参数本身一层,不包括这个参数所指向的东西。比如参数如果是一个整型指针,那么复制的只是这个指针的值,但不包括它指向的那个整型。因此下面的 swap 函数是能工作的。

void swap(int *a, int *b)
{
    int tmp;

    tmp = *a;
    *a = *b;
    *b = tmp;
}

使用时,可以传递整型指针变量,也可以传递临时产生的整型指针值。前者是指,可以使用 int *p = &b; 然后传 swap(p, ...) 这样的形式;后者是指,可以直接用 int a; int b; swap(&a, &b) 这样的形式。

第三点关键的就是 undefined(未定义的)就是 undefined,尽量避免在实际应用的代码中使用未定义的东西。如果搞研究则未尝不可,但实际应用应该避免。比如 C/C++ 里面新定义的非静态局部变量,如果未给它赋值的话,它的值就是未定义的,因此应该避免去读它的值。之所以 C/C++ 允许这样的局部变量是出于效率的考虑,这也是这两种语言与其他高级语言不同的地方:Java/C#/VB.NET 之类语言都会初始化所有变量,就算没有给它赋初始值,语言本身也会让它成为空值。

顺便一说,使用指针而非引用也是 C 语言的特色之一。Java/C# 使用引用,并且使用受管理的堆(暂不用托管堆这个名词,容易被想成是微软专有的东西)。使用这种受管理的堆之后,引用虽然本身也可能就像指针一样记录一个地址,但是这个地址就会在垃圾回收的过程中被修改了,从而将正在用的内存块堆在一起,不再让它们散布在各个角落。而使用指针的 C 语言的堆里的内存块都是固定的,不会被移来移去,但是也会让内存中有许多小块的释放出来的内存,它也占用了虚拟内存,乃至在一定程度上也影响了物理内存的分配(x86 上虚拟内存是以“页”为单位的,因此如果一页中既有被用的内存块,又有空闲的内存块,那么在使用到被占用的内存块时,这部分空闲的内存块也会被一起读入物理内存,造成一些空间的浪费)。

C++ 相比 C 来说,复杂多了。加入了面向对象的支持:重载、封装、继承、多态、多重继承、虚继承、异常。加入了模板,以及特化、偏特化之类功能。有了新的标准模板库和 Boost 库。要讲 C++,三天三夜都讲不完。说实话,不知是出于兼容性考虑还是怎样,我见到过的一些企业的 C++ 项目里并没有广泛应用到 STL,更不用说 Boost 了。

对于 C 来说,如果要实现继承以及多态,也可以做到,虽然比较麻烦一点。如果只实现多态和接口,不实现继承的话,做起来更为方便,只要使用包含函数指针的结构体即可。必要时,为了调试方便,可以加上一个字符指针,指向一个包含“类名”的字符串,以模仿 RTTI。

C++ 一个很有意义的语义功能是 RAII,即 resource acquisition is initialization。分配内存空间等资源之后立即初始化。但是 C++ 的某种典型构造函数写法中,会进一步动态分配内存,或者打开文件等操作;众所皆知,此类操作是有可能失败的。因此,我本人不太推荐这种做法。因为这种做法将会增加构造函数的复杂性。如下:

class CTest
{
public:
    int *mem1;
    int *mem2;

    CTest()
    {
        /* below is incorrect { */
        //mem1 = new int;
        //mem2 = new int;
        /* } */
        /* below is correct { */
        auto_ptr<int> amem1(new int);
        auto_ptr<int> amem2(new int);
        /* must do ownership transfer after successful allocation */
        mem1 = amem1.get();
        amem1.release(); /* ownership transferred */
        mem2 = amem2.get();
        amem2.release(); /* ownership transferred */
        /* } */
    }
    ~CTest()
    {
        delete mem1;
        delete mem2;
    }
};

其中之所以不能直接为 mem1 与 mem2 使用 new 来分配内存,是因为当 mem1 分配成功而 mem2 分配失败时,bad_alloc 异常将被抛出。此时 mem1 将无法被释放。所以用两个 auto_ptr 过渡一下,就保险了,一旦 amem2 分配失败,amem1 也将自动被释放。而之后的所有权传递过程则因为没有用到资源分配,从而理论上不会失败。当然,C++ 中这种事务处理起来一般不会太困难,因为 C++ 有栈开解机制,所有变量在结束生命期之前都会被析构。

我比较喜欢的 RAII 是这样的,虽然有点繁琐,但它的好处是初始化过程将没有任何异常出现,从而有利于把它转换为 C 语言代码,因为不需要栈开解:

class CTest2
{
public:
    int *mem1;
    int *mem2;

    CTest2()
    {
        mem1 = NULL;
        mem2 = NULL;
    }
    void Create()
    {
        mem1 = new int;
        mem2 = new int;
    }
    ~CTest2()
    {
        if (mem1 != NULL) {
            delete mem1;
        }
        if (mem2 != NULL) {
            delete mem2;
        }
    }
};

以上 Create 函数中,一旦分配失败,要做的唯一一件事情就是析构掉。非常直截了当。对于它的 C 的对应实现,也很简单:

typedef struct test2
{
    int *mem1;
    int *mem2;
} test2_t;

void init_test2(test2_t *obj)
{
    obj->mem1 = NULL;
    obj->mem2 = NULL;
}

bool create_test2(test2_t *obj)
{
    bool retres = true;

    obj->mem1 = (int *)malloc(sizeof(int));
    RAISEERR(obj->mem1 != NULL, "Failed to malloc.");
    obj->mem2 = (int *)malloc(sizeof(int));
    RAISEERR(obj->mem2 != NULL, "Failed to malloc.");
lbl_finally:
    return retres;
}

void destroy_test2(test2_t *obj)
{
    if (obj == NULL) {
        return;
    }
    if (obj->mem1 != NULL) {
        free(obj->mem1);
    }
    if (obj->mem2 != NULL) {
        free(obj->mem2);
    }
}

bool do_something(void)
{
    bool retres = true;
    test2_t obj;

    init_test2(&obj);
    ERRHDL(create_test2(&obj));
lbl_finally:
    destroy_test2(&obj);
    return retres;
}

以上例子中的异常处理部分需要 librmosaic 支持。librmosaic 的当前版本可以从我的 winrosh.sourceforge.net 项目里最新的 cmdtools 包的 2011 beta 版中找到。

在我们熟悉的 Windows API 编程中,大多数基础的 API 都是以 C 语言的形式提供的。我曾经非常喜欢在我的老电脑上用 NT 4。NT 4 是一个小巧的操作系统(相对于我的奔 II 电脑来说)。它在磁盘上安装后的占用空间也只有 130MB 左右。它在 64MB 内存的机器上都运行得很流畅,可以明显感觉到它的磁盘缓存带来的速度感。那时,我让全奔同学在他的 32MB 内存的奔腾 133 上也装了 NT 4。在他的 NT 4 上,我编写了一些小的 Windows API 程序,算是看了书以后的一种练习。

练习程序中有一个叫 ButtonButtonX。它的作用是使用 API 来实现一个我们自己绘制的按钮控件。ButtonButtonX 小有所成,实现了我的想法,但是,如何让它变成一个 ActiveX 控件,让 VB 来用呢?于是又有了后来的 VBBBX 工程。可惜当时我做不大来 ActiveX 控件,所以到最后也未成功。不过有趣的是,后来我又看了点 Windows API UI 编程的书,发现还可以使用重定义标准控件窗口的窗口过程(WindowProc)来进行子类化(subclassing),不过具体怎样做还是没做过。到如今流行 Android 编程了,也不知道 Windows API 编程还有多少生意。但是如果真有这样一份工作让我去做,我会很愿意学习一下。

另一个有趣的 Windows API 的纯 C 程序则是我当时用 QCWIN 编写的 16 位 Windows 程序。记得当时有书上说 C 语言里 int 的大小是由操作系统确定的,我觉得我不能完全同意这种说法。因为我看到 32 位 Windows 上可以运行 16 位的 Windows/DOS 程序,而这些程序则都是用 16 位整型的。那个 QCWIN 编写的 16 位程序就是 AClock,一个简易的小闹钟。可以从我的个人主页下载到它。2010 年我发现 64 位 Windows 无法运行 16 位 Windows 程序了,于是我又把它移植到了 32 位 Windows 平台上来,也算是对它的价值的一种肯定吧。它的主要价值所在就是提醒水快烧开了、泡面泡好了之类的事情。全奔同学有一次从他联读班的 4 楼下来找我,然后泡面,并提议说,最好每份飞碟炒面上都附带一张光碟,其中包含 AClock 程序,让它成为泡面专用提醒程序。

时光如梭,大学的校园生活很快就过去了,转眼间在学校里学了好多课程,有些都有点淡忘了,有些还记得。计算机导论、C 语言、数学分析、线性代数、工程数学、概率论、机械制图、金工实习、模拟电路、模电实验课、数字电路、计算机组成与系统结构、数据结构与算法、数字逻辑、数据库、数理逻辑、自动化控制、面向对象编程与 C++ 语言、微机原理、操作系统、微机实验课(8086 单片机)、计算机网络、计算机图形学、编译原理、ERP 系统、人工智能、模糊逻辑……

在这些课程里面,隐隐约约地都能感觉到 C 语言的存在。作为一门系统编程语言,C 语言在日常商务系统中的直接应用并不多,虽然大学时也见到过许多用 VC++ 写的商务应用程序,但是时至今日,这样的程序已经越来越少了。毕竟,商务应用程序经常会规模越来越大,而规模大的 C/C++ 程序由于其天生的高性能目标导致的内存访问没有运行时检查的问题,以及天生没有完善的异常处理机制(是的,包括 C++)导致的野指针、空指针等问题,会随着它的规模而越来越多,以至于很难修复。真正如果要修复,其实也是有办法的,一是使用如 Windows 平台上的 Application Verifier 或 Linux 平台上的 Valgrind 等工具来检测。二是遵循一套完善的异常处理机制:对于 C 来说,可以模仿 Java 语言的机制,在每个线程中使用一定的缓冲区记录异常发生的栈跟踪信息;而对于 C++ 来说,使用异常类的时候必须使用指定的基类,并且在每个库中尽可能使用自己的异常类,在设计文档中说明它们的名称和用途,在函数的注释中写上预见会遇到的异常,并让它们能用 RTTI 检测到具体类型,而且还要在经常出现异常的地方加上一些栈跟踪信息,如哪个文件的哪个函数里哪一行。

在我们熟悉的 Windows 和 Linux 这两个操作系统中,内核代码绝大多数就是 C 语言写的。这对于习惯使用 C++ 的同学来说似乎有些不可理解。但事实上如果用得好的话,用 C 编程也未必是什么头疼的事。相反,比 C++ 更透明,更容易理解。

在 2004 年,我开始工作以后,C/C++ 也越用越少。单位里绝大多数的程序是 C# 的。偶尔有一次,我尝到了 C/C++ 的甜头:应单位要求,我用 C++ 写了一个基于 Windows API 的抽奖程序。后来还有一位同事用 C# 写了一个界面类似但功能不同的辅助程序。他问我,为什么你的程序一跑起来就很流畅,而我的程序刚打开总会卡一下呢?后来我才知道 C# 的 JIT 是每加载一个 exe 或 dll 就编译一下的,而 dll 加载是程序第一次用到这个 dll 里的东西的时候加载的,因此这样的结果就是刚打开的时候有点卡。

在 2006 年,在周六工程硕士要上课的上午和要上课的晚上之间没事干的下午坐在徐汇校车候车室,阅读了 Brian Kernighan & Rob Pike 的《程序设计实践》,里面既讲到 Java 程序又讲到 C 程序。结果我就跳过 Java 的,只看 C 的了。在学了 Python 之后,感觉到 C Python,乃至 Iron Python、Jython,底层都是 C/C++ 的,一种混凝土般的朴实而又强大的感觉。C 语言中的大部分元素都很简单明了,但是,malloc 让我还是费了不少脑筋。

malloc 之所以让我费脑筋的原因,是因为它是 C 的语言能力中唯一一块最复杂的东西。C 的复杂语法,再复杂也让人能理解,但是 malloc 的实现,却有着许多优化点。Windows 上的 malloc,通常是委托给 kernel32.dll 中的 HeapAlloc 函数的;当然某些库的实现除外,如 SmartHeap 和 glibc。malloc 如何实现得比较高效?因为很多用 C 写的算法,都假定 malloc 的时间复杂度是 O(1)。如果只是用简单的线性链表来做,时间复杂度会随着已分配的内存块的个数而上升。因此各种实现就纷纷有了自己的独特算法。比如 Windows XP 中 HeapAlloc 的实现实际上是用了十字链表,即对一个已分配或未分配的内存块,它同时属于两个链表。其中一个链表是指向内存地址上前一个或后一个内存块(分配或未分配的),另一个链表则是指向上一个或下一个同样大小的内存块。具体的实现我不详述,CSDN 曾经有人作过分析,我收藏过这样一篇文章。它对于 1KB 以下的内存块能做到 O(1) 时间复杂度的分配和释放。另外还有一个前端分配器,它用于缓存那些刚被释放的内存块,一旦同样大小的块又被分配,那就可以直接返回出去,不需要从更为复杂的使用十字链表的后端分配器中取得。

在 2007 年,Windows Vista 出现于市场的时候,欣喜地发现它里面自带的应用程序大多并非用 C# 基于 WPF 编写的,而还是基于 C++ 多数。可惜内存占用还是有点大。这不,我的笔记本电脑的 1G 内存实在不爽了,只能再买了一根 2G 的内存条才能流畅。

在 2008 年,在网上读到了一篇对 Dennis Ritchie 的采访。此前也读到过对 Linus Torvalds 的采访以及对 Bram Moolenaar 的采访。发现 Linus 和 Bram 都是用惯 Linux 的,而 Dennis Ritchie 虽然一开始使用 Unix,但到后来他的日常操作系统是 Windows,因为它易用。就这么简单。

而我则是自 Windows Vista 开始从技术上接受 Windows 的。Windows 95 没有 32 位完全保护和用户权限控制以及硬链接,Windows NT 4 有了。Windows NT 4 没有磁盘卷挂载点和目录(服务端)链接(Junction),Windows 2000 有了。Windows XP 没有符号链接,Windows Vista 有了。而且 Windows Vista 在进程调度的公平性上有了明显改进,还有了新的基于使用频率的主动缓存填充(Superfetch)。这些技术,基本上都是用 C 语言实现的,想到这一点就能感觉到 C 语言的强大。

而且,C 语言虽然在多任务的编写上没有汇编好写,但是仅是通过 setjmp/longjmp 已能够实现协作式多任务了。

如今,Ritchie 老师已经安祥地离开了我们,但是他的发明,将伴随着一代又一代新的计算机,进入未来的广阔天地,而我们,也应好好发挥我们的才干,让 C 语言的高效、灵活和简单性影响到上层的世界,让技术世界更添光彩~~



  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值