« December 2009 | Main | February 2010 »

January 28, 2010

古怪的 C++ 问题

我好多年没写 C++ 程序了,读 C++ 代码也是偶尔为之。

今天晚上就碰到这么一个诡异的问题,我觉得是我太久没摸 C++ 了,对那些奇怪的语法细则已经不那么熟悉了。有知道的同学给我解惑一下吧。

事情的起因是,我想安装一个 perl 模块唤作 Syntax::Highlight::Universal 。

本来用 CPAN 安装很方便的,直接 install 即可。

可是在我的机器上,make 死活通不过。我就仔细研究了一下编译出错信息。又读了一下源代码,自己感觉没错。纠结了半天,仔细模仿出错的地方写了一小段程序测试。

template class A { protected: int a; }; template class B : public A { public: void foo() { a=0; } };

各位同学觉得有问题么?我初看觉得没有。但是 gcc 一编译就出错。一开始是觉得 gcc 版本太高(4.x),可能语法检查更严格了。后来换了 gcc3 ,问题依旧。

出错信息如下:

a.cpp: In member function `void B::foo()':

a.cpp:11: error: `a' was not declared in this scope

就是在 B 里找不到 A 定义的成员变量 a 。

如果 A 不是一个 template ,那么这个问题就没有。

我琢磨着这个问题跟编译器为 template 生成代码的行为有关,但是不确定。

注:这段代码在 VC6 里是可以正常编译的。

最后,我试了一下,把代码改成

    void foo() { 
        this->a=0;
    }

那么是可以编译通过的了。可是,这是新标准规定的么?

btw, 其实我写 C++ 的最后一年,都养成了显式用 this 指针的习惯。这样比较少犯错误。

January 27, 2010

最终幻想XIII

周六部门组织去滑雪,我第一次,还不错。回来办公室发现同事桌子上有本杂志,封面是最终幻想XIII 。顺手拿回家躺床上看。

翻了几页杂志,回想到当年最终幻想X 出的时候的惊艳。当时看到公司里有同事在玩,也是周末。第二天去去买了台 PS2 和游戏。之后,我就成了这个系列的死忠(之前我也玩过七和八,但是都没有十给我的震撼)。后来我补上了五和六,并且把七和八重玩了;再之后就是 XI 和 XII ,以及 X 和 VII 的外传。

很久没玩游戏了,想想真是忙呢。又或者是长大了?但是,我得承认,我心里那点感觉又回来了。星期天一起床,吃完饭就直接去了附近的游戏店。价钱很黑,但是我急着想要。不到五分钟就拿下了 FF13 连 PS3 的套装。

毕竟没让我失望啊。

我想这一作可以超过 X 在我心中的地位了。

不过一开始没有买 HDMI 线,文字相当模糊。昨天又在网上订了跟线,今天到了。换成高清模式舒服多了。看来这线是玩 FF13 必备的。

这次的战斗系统做的非常简洁明快。爽快感十足。还解答了我前年的一个疑问:

之前我做个一个战斗设定:回复 HP 技能可以来得比较轻易,并且几乎无消耗。强调爽快感,不要有太多繁杂操作(比如磕药,复杂的加血技能)干扰战斗。但是沙盘了半天都觉得很难做平衡,没有想出和高回复力对应的设定应该是什么。

FF13 告诉我,可以有一个 Break 系统做一个对应的补充。玩家重点在于控制操作的次序即可。(当然还有职业更换的策略)

之前我的构思里有个类似的设定,但是思维产生了个死角,没有把系统做成非线性的。而用了一个高斯分布。其实加一个 Break 临界点,就好玩了许多。

这些都是闲话了,等对此作的战斗系统有了更深刻理解后,再总结一下。

看网上介绍,这次的剧情通关应该在 40 小时左右,我现在游戏时间才 20 小时。争取这周先一周目吧。

January 21, 2010

招行虽然烂,但至少可以用

招行的网银只支持 Windows ,这点不只一个人像我这样痛恨过了 。我想大家都是恨铁不成钢吧。

别的银行我也用过几家,也骂过。比如农行早几年取款难。每个季度我都去转工资,每次要排 3 小时的队。后来花了 100 办了网上银行,居然跨行转帐最长等了我一周。后来工资转发到招行,总算解决了问题。愤然去消户,跑了两趟银行,交通费花了几十,手续费花了十块(补交年费,还有把卡弄坏的挂失费等等)。

当年信用卡还不普及的时候,招行的信用卡在一些酒店押不了房费。我琢磨着就想办张中国银行的信用卡。当时我已经在公司楼下的中国银行开过张借记卡,用来交电话费。里面常年有几万的余额。为了办那张信用卡,我填了无数资料。复印了很多材料。包括工资单,招行信用卡的还款单。买的房子的房契。等等。足够证明我是个信用良好,收入稳定的好市民了。

中国银行的人再给我打了两个电话,并陆续索取了一些我的个人材料后,通知我,审核没通过。不给办信用卡…… 看着满大街送上门的卡(我抽屉里还有张没开的广发信用卡),我彻底无语了。看来跟谈恋爱一样,只要是你主动找上门去的,立马掉价了。

此外,我还用过邮政储蓄,在读大学时办过张交通银行的卡(当时唯一在校园里有网点的银行)。读书时有次邮政储蓄内部系统错误,导致有些取款机异地取了不钱。我当时身无分文,(没钱买公交车票)在长沙市区里步行了 4 个多小时找可以取款的柜台。

最近因为需要,在建设银行以及交通银行都办了些业务。

早几年建设银行硬件水平之滥我早有耳闻。前几年,我作保,给朋友贷了一笔钱。资金打在了北京的建设银行里。这个朋友想从杭州把钱取出来,被告之,建设银行内部南北不通,是两个系统。这么转帐要收四位数的手续费。当时我就惊诧了。如果不考虑时间和精力,还不如自己北上去柜台办呢。

这些年应该好了些。可今天去建设银行转帐。柜台上手续繁杂不说。同城跨行居然要收 25 。还是打了 5 折的。我在招行免费转帐习惯了,还真有点不适应。(招行即使没有金葵花卡,同城跨行也只要 2 元)末了,银行的人推荐我开通手机银行,说是汇款 3 折。头三个月免费。

注意,是头三个月,以后每个月还是要收 6 元的。至于网上银行,必须买个 USB key 。

然后是交通银行,我免费开通的手机密码登陆网上银行。毫不怀疑的打开了 IE ,我想 Chrome Firefox 铁定是支持不了的了。

结果,弹出个 javascript 出错框,唉,习惯了,见怪不怪。安装了所谓安全输入控件后。我登陆了两次硬是没进去。不是我密码错误,而是那个图片校验码错…… 然后,我的帐户被锁定了。告之,去银行柜台解锁吧。

嗯,在其它银行软硬件不跟进的情况下,我就不指望招行能出跨平台的网银了。谁叫咱别无选择呢。

浅谈 C 语言中模块化设计的范式

今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。

我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。

层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。

其次,就是越层的模块联系。当模块 A 是模块 B 的上层,而模块 B 又是模块 C 的上层,这个时候,让模块 C 对模块 A 可见,在模块 A 中有对 C 导出接口的直接调用,对于清晰的设计是很忌讳的一件事。虽然,我们很难完全避免这个问题,去让 A 对 C 的调用完全通过 B 。但通常应尽力为之。(注:以后写书的话,我争取补充一些实际的例子来说明)不过,对语言不原生支持的数据类型,以及基础设施,但却有必要创造出来给系统用的。可以有些例外。比如内存管理,log 管理,字符串(C 语言用原始库函数管理比较麻烦)等等,我们可能以基础模块的形式提供。但却可能被不同层次的模块直接使用。但,上到一定层次后,还是需要去隐藏它们的。

下面来一点更实际的分析。

以 C 语言为例,由于 C 语言缺乏 namespace 的原生支持,我们通常给 api 加上统一前缀来区分。这倒也不麻烦。

那么模块 A 看起来就是一堆 'A_xxxxx' 为名字的方法。我个人主张单个模块不宜过大,在实现时适合放在同一个 .c 文件里即可。通常,一个模块会围绕一类对象处理。这些对象可以用整数 handle 来表示,也可以用一个特定类型的对象指针。两种方案各有千秋。先来谈对象指针的方案。

一个模块 A 的接口描述文件很可以是这样的(希望以后能补上更现实的代码):

#ifndef _A_h
#define _A_h

struct A;
struct B;

struct A* A_create(void);
void A_release(struct A *self);
void A_bind(struct A *self , struct B *b);
void A_commit(struct A *self);
void A_update(void);

int A_init(void);

#endif

这里,我们定义了 A 这种数据类型。我个人反对用 typedef 或宏来减少代码输入。除非有特别的理由,都写上 struct 前缀,而不是定义出新类型。尤其是在较底层的模块设计时更是如此。在接口描述时,struct A 的细节是绝对不应该暴露出来的,它的数据结构应该仅存在于实现的文件 a.c 中。

关于 A 的接口通常分两类,一类是对 struct A* 做一些处理的,那么就让第一个参数传入 self 指针。这相当于 C++ 的 this 指针。比如上例中的 A_commit ;另一类接近于 C++ 类的静态成员函数,通常用于对这一类对象全部做一个处理,如 A_update

注:我无意用 C 去模拟 C++ ,但基于一类数据类型做一些处理的方法,对于 C ,这样的写法也是一个常规的范式而已。至于面向对象等在构建复杂系统时常用到的方法,以后我会谈谈我自己常用的另一些范式。或许像 C++ ,也可以不像。怎么写更好,是个见任见智的问题。不用过于拘泥。

这里的例子中,我们还提到了另一个数据类型 B 。显然,它是放在 B 模块中的。

我们通常不会在 a.h 中去 include b.h ,而只是声明一下 struct B 。(对于 C 语言来说,这并不必要,但写上是个好习惯)。这是因为,如果 B 是位于 A 之下的模块,既在 A 模块的实现中,会用到 B 的方法,我们通常不会让用到 A 模块的人,可以看见 B 的接口。包含 a.h 的同时隐式包含 b.h 就是不必要的了。

从范例代码中,我们可以猜想,struct A 是对 struct B 的某种封装,可以通过对 A 的操作,间接操作到其中的 B 类型。在 A 的模块初始化 A_init 中一定就会初始化 B 了。如果是这样,B 的层次就位于 A 之下。

往往 struct B 中还会保留一个 struct A 类型的引用。首先,我们应该尽力避免这种情况。即:位于下层的 B 应该对上层的 A 一无所知是最好的。如果在 B 模块中必须出现 struct A,那么我们应该至少保证,仅仅是 struct A * ,一个引用,而绝对不能出现任何对 A 模块内接口的调用。不要认为使用巧妙的方法,绕过循环依赖初始化问题就够了。这应该是一个设计原则,不要去违反。

btw, 草率的接口设计往往是日后系统脆弱的根源。图一时之快,随意暴露一些接口,或是自以为聪明的用一些“巧妙”的方法,甚至是语法糖来绕过设计原则,都是很危险的。

一个常见的难处理的问题是:如果 struct A 和 struct B 相互有双向引用。怎样建立这个引用关系?这个建立的过程,到底是 A 的方法,还是 B 的方法?我的答案是,谁在上层,就是谁的方法。

但是 A 和 B 相互都看不见内部数据布局的细节,让 B 的内部对 A 类型做一个引用,比如也需要从 B 模块中暴露一个接口出来。这个接口,可能仅供 A 使用。在这个例子里,就是仅供 A_bind 这个方法去使用。

如果是 C++ ,我们或许会采用 friend 。也可能使用其它一些技巧。反正 C++ 里可以挖掘的语法太多了。但 C 怎么办?下面给个我自己的方案。

原本,我们在 B 中导出的 api 是这样的:

void B_set_A(struct B *self,struct A * a); 

现在写成:

struct i_A;

void B_set_A(struct B *self,struct i_A *a);

在 b.c 的实现中,加一个函数用于 struct i_A * 到 struct A * 的转换。

static inline struct A * A(struct i_A *a) { return (struct A *)a; }

然后在 a.c 的实现中,加一个类似函数用于转换 struct A * 到 struct i_A *

这样,在 a.c 之外,其它模块因为不能得到任何 struct i_A 类型,而不会错误的使用 B_set_A 这个接口了。

January 19, 2010

C 语言对模块化支持的欠缺

继续昨天的话题。随便列些以后成书可能会写的东西。既然书的主题是:怎样构建一个(稍具规模的)软件。且我选择用 C 为实现工具来做这件事情。就不得不谈语言还没有提供给我们的东西。

模块化是最高原则之一(在 《Unix 编程艺术》一书中, Unix 哲学第一条即:模块原则),我们就当考虑如何简洁明快的使用 C 语言实现模块化。

除开 C/C++ ,在其它现在流行的开发语言中,缺少标准化的模块管理机制是很难想象的。但这也是 C 语言本身的设计哲学决定的:把尽可能多的可能性留给程序员。根据实际的系统,实际的需要去定制自己需要的东西。

对于巨型的系统(比如 Windows 这样的操作系统),一般会考虑使用一种二进制级的模块化方案。由模块自己提供元信息,或是使用统一的管理方案(比如注册表)。稍小一点的系统(我们通常开发接触到的),则会考虑轻量一些的源码级方案。

首先要考虑的往往是模块的依赖关系和初始化过程。

依赖关系可以放由链接器或加载器来解决。尤其在使用 C 语言时,简单的静态库或动态库,都不太会引起大的麻烦。

C++ 则不然,C++ 的某些特性(比如模板类静态成员的构造)必须对早期只供 C 语言使用的链接器做一些增强。即使是精心编写的 C++ 库,也有可能出现一些意外的 bug 。这些 bug 往往需要对编译,链接,加载过程很深刻的理解,才能查出来。注:我并不想以此来反对使用 C++ 做开发。

我们需要着重管理的,是模块的初始化过程。

对于打包在一起的一个库(例如 glibc ,或是 msvcrt ),会在加载时有初始化入口,以及卸载时有结束代码。我想说的不是这个,而是我们自己内部拆分的更小的模块的相互依赖关系。

谁先初始化,谁后初始化,这是一个问题。

在 C++ 的语言级解决方案中,使用的是单件模块。要么由链接器决定以怎样的次序来生成初始化代码,这,通常会因为依赖关系和实际构造次序不同而导致 bug (注:我在某几本 C++ 书中都见过,待核实。自己好久不写 C++ 也没有实际的错误例子);要么使用惰性初始化方案。这个惰性初始化也不是万能的,并且有些额外的开销。(多线程环境中尤其需要注意)

我使用 C 语言做初期设计的时候,采用的是一种足够简单的方法。就是,以编码规范来规定,每个模块必须存在一个初始化函数,有规范的名字。比如 foo 模块的初始化入口叫

int foo_init()

规定:凡使用特定模块,必须调用模块初始化函数。

为了避免模块重复初始化,初始化函数并不直接调用,而是间接的。类似这样: mod_using(foo_init);

mod_using 负责调用初始化函数,并保证不重复调用,也可以检查循环依赖。

在这里,我们还约定了初始化成功于否的返回值。(在我们的系统中,返回 0 表示正确,1 表示失败)然后定义了一个宏来做这个使用。

#define USING(m) if (mod_using(m##_init,#m)) { return 1; }

注:我个人反对滥用宏。也尽可能的避免它。这里使用宏,经过了慎重的考虑。我希望可以有一个代码扫描器去判断我是否漏掉了模块初始化(可能我使用了一个模块,但忘记初始化它)。宏可以帮助代码扫描分析器更容易实现。而且,使用宏更像是对语言做的轻微且必要的扩展。

这样,我的系统中模块模块的实现代码最后,都有一个 init 函数,里面只是简单的调用了 USING 来引用别的模块。例如:

#include "module.h" /* 我个人偏爱把 module.h 的引入放在源文件最后,初始化入口之前。 它里面之定义了 USING 宏,以及相关管理函数。 这样做是为了避免在代码的其它地方去引入别的模块。 */ int foo_init() { USING(memory); // 引用内存管理模块 USING(log); // 引用 log 模块 return 0; }

至于模块的卸载,大部分需求下是不需要的。今天在这里就不论证这一点了。

January 18, 2010

好的设计

前几天说,想再写本书。许多朋友给了我支持。暂时我还写不了。因为:

  1. 工作真的很忙,很难脱开身。我觉得我在某种程度上陷进去了。需要花点时间整理规划一下。然后把工作的事情处理好。让它可以顺利做下去。不光是技术上要解决的问题,也不光是管理问题,也不光是团队合作的问题,也不光是项目开发运作的问题。反正很多很多,确实有很多麻烦。我要尽力做好。

  2. 觉得上一本没写好,是因为还是太仓促了。即使是已经写的那点东西,也积累不够。写书的经验也太少。我倒不怕有错误被人骂,是怕自己回头不满意。

  3. 如果再写,肯定只抓着很少的问题谈。但是具体写什么,还没全想好。积累是有了点,真能够拿出来写写的不多。毕竟,写书和写 blog 瞎扯还是不一样的。

孟岩建议我先在 blog 上列的大纲,然后随便写点。让同学们给意见,再逐步修改成书。我也有此想法,觉得不错。不过一开始,恐怕我连大纲都列不出来,就想到哪写到哪,随便写点东西吧。过段时间再把零碎想法串起来,作为正式列提纲的参考。

由于最近几年用的主要开发语言是 C 和 lua 。那么也打算以此为基础写。假定读者至少有不错的 C 语言基础了。我真正想谈的是,如何把一个软件很好的构建起来。到底需要做些什么。(从实现层面看)怎样才是好的软件。

那么有一个重点问题,也是老问题,怎样才是好的设计。

好的设计,必然是容易实现的。它可以很精巧,但不能难以理解。

太阳底下无新鲜事。软件行业已经发展了这么多年,你想到的东西,肯定有人都想到过了。

每个软件也都有它的生命期,我们只要在它的生命期内完成它的使命就行了。软件往往需要尽快的投入使用,然后在使用中演化。这个演化最大可能并不依靠你一个人的力量去推动。随着参与的人增加,人和人(指开发人员)的共性就会减少。每个人都看得懂可以充分接受,软件才不容易向坏的方面演化。

我们常常谈模块化,谈高内聚,低耦合。

本质上,就是如何管理复杂度。如何把一件很难的事情(开发一个软件),分解成小问题,分而治之。

这些小问题之间的千丝万缕的联系,是设计人员面临的最大难题。

有些原则听起来不错,但是坚持起来很难。

比如,让模块的输入输出没有副作用。你能让你的模块每个输入对对应着唯一输出吗?

又比如,让模块层次化。如果 A 模块依赖 B 模块,B 模块依赖 C 模块。一旦出现这个状态,你能保证 A 模块绝对和 C 模块隔绝吗?更有甚者,让三个模块循环依赖这种更糟糕的事情也并不鲜见。

抽象是个好东西。但借助不断的抽象,问题不断的包起来,演化成新的巨无霸,显然会让事情更糟。虽然最终可能真的能像搭积木一样去组装软件了。或是雇佣更多的程序员填表单一样的工作,相互不需要对方在做什么。但是,软件性能却下降到了不可以忍受的地步。bug 也隐藏的更久,更不可收拾。

好的设计,必须对问题有足够清晰的理解。有如庖丁解牛一般,把整个问题划开,在最薄弱的地方分离。其实,做到这点,也就够了。

解决这些问题,其实跟语言无关。语言之争是没有多大意义的。如开头所说,把设计做好,模块之间的关系,用足够简单的方式就能描述清楚了,大部分流行的开发语言都能做到。

用 C 来实作,而没有用它的近亲 C++ ,也是为了避免狭隘的争议:我们该用这个特性吗?该用那个特性吗?这个形式做是不是好点?那样会不会有更好的性能?

所谓开发效率,对于个人来说,语言之不同,是会有很大差异。但是那是实现层面的差异。对于完成设计,这个过程,效率和所用语言无关。

实现的阶段,程序员可不可以开心的放心的去完成那些接口,这就是衡量设计好不好的指标了。这个时候,一个高开发效率的语言有优势(更少的代码量),一个容易掌握的语言也有优势(可以让更多的人参于而少犯错误)。

对于我的团队,我会更乐于采用一种让实现人员更轻松的方式。不用理会太多的语言细节,不用在投入开发前学习更多的概念(尤其是这个项目独有的),不用特别严格的 code review 也可以允许大家提交新的代码,切不至于轻易的引入 bug 。

我相信,软件做到后面,设计人员不需要亲自写太多代码。虽然我现在每天还是大量的写,也并不觉得枯燥。

事必恭亲是不好,但并不是说,你给实现人员足够信任就可以放手的。真正让你放手的只能是,你做出了好的设计,无论是谁,他也写不坏它。这时,是你乐意自己写,还是多找几个同学帮忙写,已经不重要了。

January 16, 2010

武汉的黄牛还是实在

周五请了一天假回武汉看 iMax 版 Avatar。听说上海不排一晚上是买不到票了。特地周四晚飞回家,周五起了个早去新民众。

中午 12 点 50 到的,一看,好家伙,买票的人排了一层从楼梯都排到楼下了。周六下午场都卖完了。正打算去吃个饭再来排队的。结果一黄牛过来,说要不要 13 点的。我一看表正好 12 点 55 。100 快一张成交。

不错,就是没吃中饭,爆米花充饥。

电影不错,iMax 3D 感觉是不一样。屏幕大,才有足够的纵深。坐的位置估计也满重要。反正我坐在中间靠后一点,感觉正好。

这部片子啊,就是看效果了。情节就那样了,但是设定很严谨。适合我们这样做游戏的人看。

January 12, 2010

《The New C Standard》的新版下载

前几天被翻出来介绍《The New C Standard》的老帖,貌似被作者看到了。 The changing shape of code in the next decade

看来我的 blog 还是还是有点影响力 :) 好多人都下载了那本书。

Derek Jones 同学提醒我书已经有了新的版本。我已更新了原帖的下载链接。

btw, 机器翻译真是恐怖啊。当然,毕竟这位同学填对了 7 。 :D

Lua 5.2.0 (work1)

[ANN] Lua 5.2.0 (work1) now available ,这个消息有几天了。lua 社区这两天非常热闹,各大牛都现身了。

做 LuaJIT 的牛人 Mike Pall 对 bit 库没有采用他做好的现成方案那可是相当的不客气

不过,欢呼雀跃的人还是比较多的。每次 lua 升级个小版本,改动都非常大。对成熟项目,不给你伤点筋骨,那就不是 lua 三巨头的风格了。当然,对于时不时重写代码的我,欣赏这种风格 ;) 我喜欢更健康的 lua 语言。

嗯,无论如何,lua 的源代码是非常值得阅读的。

这次把 setfenv/getfenv 给取消了。我想这会牵扯许多项目的代码,包括我自己的。当然,想兼容的话,稍微改改也成。

不过新加的 in exp do block end 语法是我喜欢的东西。比用 setfenv 去做一些 DSL 的活方便多了。主要是不太容易出错,代码也更漂亮了。

这个玩意如果用 setfenv 去模拟,就难看多了。可以参考我前几年写的短篇 最后,模拟 pascal 中的 with 这段。

其它许多改动,对老代码都或多或少会有点影响。但是可以促使大家把代码重新写的更规范一些,好事儿。

我想,把代码写正确的话,性能也会提高一点的吧。

January 09, 2010

随便写写

这些年来,经常会收到前些年写的那本书的读者来信。最近有些同学抱怨说买不到了,有的想让我寄一本。我只能说,谢谢,我这里也没有。估计,出版社也不会再刷了。如果真不想看电子版,可以去图书馆复印一本。其实写过技术类书的同学都知道,写书并不是件好差使,也绝对不可能靠这个赚钱。基本上,你投入的精力,是金钱上的回报远远不及的。真正的意义或许在分享了自己的知识,多交了几个朋友吧。

话说那本书,我自己是不太满意的。04 年写的时候,基本只能总结 03 年之前的一些东西,还差许多火候。写完之后,我给自己打了 70 分,慢慢的,觉得有 60 分就不错了。勉强及格吧。

这几年一直有念头写点别的东西。源于这几年做项目的诸多经历。觉得自己悟到了不少东西,可又总感觉还总结不好。一开始想译一本关于 C 语言的书,没开始就被那厚度吓坏了。一些做书的同学又建议我还不如自己写。是啊,我翻了一下,最有名的,谭公的《C 语言程序设计》,无语。据说很多人想再写一本,我就不参合了。有什么能超过 K&R 的《C程序设计语言》呢?

其实,写语言是次要的东西。除了 C++ ,我看没几种语言需要有一本以上谈语言的书了。关键是借语言谈点事儿。我们并不是用不好语言,是不知道到底该怎么解决复杂性这个问题。

我们会有很多方法解决问题,可能也不会存在某种通用的伎俩。不过每个有 10 年以上编程经验的程序员,只要他不是天天重复昨天。那么总会有自己的一套东西,值得拿出来供人参考一下。可以是编程习惯,编码风格,常用的范式,刻意规避的问题。

近三年来,我用 C 语言堆砌了大量的代码。想了很多问题。加上之前一共 19 年(1991 年到 2010 年)的 C/C++ 经验,总算觉得稍微摸到点门道。唉,最近项目还是太忙了。忙过了,真想写写,总结一下。绕开各种语言表面的浮华,看清问题的本质,用 C 这样貌似简陋的语言工具,去构建一个由许多人共同维护的复杂项目,还是一件挺有趣的事情啊。

问题的实质往往不是你是否能找到一个精巧的结构,完美的表达它;而是找到一个最通俗易懂,不需要同伴付出额外学习负担的方式。所谓“大巧若拙”吧。