C语言也许挺简单,但是C标准有700页,所以,如果你不想花费毕生精力去研究它,那么你应该知道哪些部分可以被忽略。
让我们从二合字母或者三合字母开始,如果你的键盘缺少{}键,你可以用<%和%>来替代,就像是int main()<%...%>。这在20世纪90年代还是有用的,因为那个时候世界上的键盘有不同的样式,但是今天你很难找到没有{ }的键盘了。三合字母类似于??<和??>,这个东西更没用,以至于gcc和clang的作者都没有花任何时间去编码解析它们。
像三合字母这种,语言中过时的东西很容易就被忽略掉了,因为根本没有人提到它们。但是语言的其他部分在教科书里面却被反复地提及,只是用来满足旧的C89规范的要求,或者为了适应20世纪90年代计算机硬件的限制。限制越少,我们写代码的效率就越高,如果你能从删除代码并去掉那些没用的冗余中获得快乐,那么本文适合你。

1.1 不需要明确地从main函数返回

我们首先去除一行几乎在每个程序中都会遇到的代码,并以此热身。
所有程序必须有一个main函数,它的返回类型是int,因此在程序中必须要有下面这条代码:

读者会觉得其中必然有一条return语句,表示main函数所返回的整数值。但是,C标准知道这个返回值极少使用,所以不想麻烦我们。C标准要求:“……到达结束main函数的}之前返回一个0值。”[C99和C11,§5.1.2.2(3)]。也就是说,如果我们在程序最后一行没有编写return 0;,C会默认加上它。
想起来了吗?当你运行完你的程序后,你可以使用echo$?来了解这个程序的返回值;你可以用它来查看你的main函数是否运行完毕,并返回了零值。
本文的最开始已经向读者展示了这个版本的hello.c程序,可以看到其中只包括了一条#include语句和一行代码[1]:

自己动手:检查自己的程序,从main函数中删除return 0这一行,观察这样做会不会有区别。

1.2 让声明的位置更灵活

让我们回想一下以前阅读剧本时的情况。在剧本的一开始是人物表,里面列出了剧中出场的所有人物。但在开始阅读剧本之前,一串人名列表对于我们而言并没有太大的意义。因此,我们大多会跳过这一部分,直接阅读正文的内容。当我们沉浸在情节中并忘了Benvolio是谁时,可以很方便地返回到剧本的最前面,看看人物表中对他的简单介绍(哦,他是Romeo的朋友,Montague的侄子)。但是,前提是我们所阅读的是纸版的剧本。如果我们是在屏幕上阅读剧本,就必须搜索Benvolio最早出现的地方。
简而言之,人物表对于读者而言并不是非常有用的。在人物第一次出场时再介绍他们显然更为合适。
我经常看到类似下面这样的代码:

首先是3~4行的介绍性材料(要看是不是算上空行),然后才是实质性的程序。
这是ANSI C89的复古风格,它要求所有的声明都出现在代码块的头部,这样做的原因是早期编译器的技术限制。现在,我们仍然必须声明所有的变量,但可以尽量减轻作者和读者的负担,在第一次使用变量时才声明它们:

在上述代码中,声明恰好出现在需要的位置,因此声明的责任降低到最低限度,相当于在第一次使用变量前加上它的类型名。如果使用了彩色语法高亮显示,这种声明仍然很容易被发现(如果你的编辑器不支持彩色,最好还是换一种支持它的,这类编辑器数不胜数)。
在阅读不熟悉的代码时,我看到一个变量的第一直觉就是回过头去看看它是在什么地方声明的。如果它是在第一次使用时或者在第一次使用之前的那一行被声明的,我就可以省掉这几秒浏览的时间。另外,根据应该使变量的作用域尽可能小的规则,我们要想方设法降低活动变量对前面代码的依赖,这对于较长的函数而言是非常重要的。
在上面这个例子中,声明出现在它们各自代码块的起始处,然后是非声明性的代码行。这正是这个例子所显示的结果,我们可以自由地混合声明行和非声明行。
我把denom的声明放在函数的头部,但我们也可以把它移动到循环的内部(因为它只是在循环内部使用)。我们可以信任编译器能够充分地理解,而不会浪费时间和精力在每次循环迭代时对这个变量进行销毁和重新分配[尽管在理论上会这样,参见C99和C11,6.8(3)]。站在索引的角度,它应该和循环同时结束。因此,把它的作用域限制在循环之内是非常自然的做法。
这种新语法会拖慢程序吗?
不会。
编译器所执行的第1个步骤是把代码解析为独立于语言的内部表示形式。这样gcc(GNU编译器集合)在解析步骤的最后才能够产生C、C++、ADA和FORTRAN的可兼容目标文件,它们看上去都是相同的。因此,C99为了阅读方便而在语法上所提供的便利一般在可执行文件被生成之前就已经被抽掉了。
同时,运行程序的目标设备所看到的只是经过编译的机器指令,因此不管源代码所遵循的是C89、C99还是C11标准,对它来说是没有区别的。
在运行时设置数组的长度
除了把声明放在任意位置外,你也可以在运行时分配数组,并根据声明之前的计算结果来确定它的长度。
同样,这个方法在以前是不允许的。25年前,我们要么必须在编译时知道数组的长度,要么使用malloc。
例如,假设想创建一组线程,但线程的数量是用户通过命令行设置的。旧式的方法就是通过atoi(argv[1])(也就是把第1个命令行参数转换为一个整数)从用户那里获取数组的长度。接着,在运行时确定了长度之后,就分配一个具有正确长度的数组。

我们可以写得更紧凑些:

后面的写法由于行数少,所以更不容易出错,它看上去像是声明一个数组,而不是对内存寄存器进行初始化。我们必须释放手工分配的数组,但可以简单地把自动分配的数组放下不管,它在程序离开特定的作用域后会被自动清理[2]。

1.3 减少类型转换

在20世纪七八十年代,malloc返回的是一个char*指针,我们必须对它的结果进行类型转换(除非是为字符串分配内存),其形式类似:

现在不需要这样做了,因为malloc向我们返回的是一个void指针,编译器可以很方便地将它自动转换为任何类型。最简单的转换方式是声明一个具有正确类型的新变量。例如,必须接受一个void指针为参数的函数,一般可以用下面这样的形式开始:
在更普遍的情况下,如果把一种类型的数据项赋值给另一种类型的变量是合法的,即使我们没有指定显式的类型转换,C也会为我们完成这个任务。如果它对于给定的类型是不合法的,就必须编写一个函数设法完成转换。这对于C++而言并不正确,后者对类型更为依赖,因此需要显式地指定每个类型转换。
另外还有两个原因支持使用C的类型转换语法把一个变量从一种类型转换为另一种类型。
首先,当两个数相除时,一个整数除以另一个整数总是返回一个整数,因此下面这两条语句都是正确的:
第2个除法是许多错误的来源。这个错误很容易修正:如果i是个整数,则i + 0.0就是与这个整数匹配的浮点数。不要忘了括号,这样就简单地解决了问题。如果常量2是个整数,那么2.0或简单的2.就是浮点数。因此,下面这些变型都是可行的:
我们也可以使用类型转换的形式:

站在美学的角度,我倾向于加零的形式。当然,读者也可以选用转换为double的形式。但是每次在使用“/”操作符时都要养成其中一种习惯,因为这是许多错误的来源(并不仅仅限于C,许多其他语言也坚持int/int的结果应该是int类型,而不会自动予以修正)。
其次,数组的索引必须是整数。这是规定[C99和C11,§6.5.2.1(1)],如果发送了一个浮点数的索引,gcc就会报错。因此,我们可能必须去转换,即使我们知道在当前情况下实际提供的总是一个整数值的表达式。
从上面可以看到,尽管存在少数合理的理由需要类型转换,但我们也可以选择避免使用类型转换的语法:加上0.0或为数组索引声明一个整数变量。
这不仅仅是为了减少书写混乱的问题。你的编译器为你进行类型检查并相应地发出一些警告,但是一个显式的转换就是对编译器说:别管我,我知道我正在做什么!例如,考虑下面的程序,试图设置list[7]=12,但是犯了两次典型的错误,应该用一个指针指向的值,但是这里却直接使用了指针的值。

1.4 枚举和字符串

枚举的出发点是好的,但是它走上了歧路。
它的优点是足够清楚:整数值并不容易记忆,因此当我们在代码中使用一个简短的整数列表时,最好对它们进行命名。下面是一种甚至更糟的方法,在不使用enum关键字的情况下用#define进行定义:

使用enum,我们可以把上面这4行代码缩减为1行,并且调试器更容易知道EAST的含义。下面是对#define序列的改进:

但是,现在全局空间中有了5个新符号:directions、NORTH、SOUTH、EAST和WEST。
为了让枚举发挥作用,它一般必须是全局的(在一个将被整个项目中多次包含的一个头文件中声明)。例如,我们常常在函数库的公共头文件中看到枚举类型的typedef声明。创建全局变量会产生相应的责任。为了尽可能地减少名字空间的冲突,函数库的作者会使用像GCONVERTERRORNOTABSOLUTE_PATH这样的名字,或者是相对简洁的CblasConjTrans。
此刻,单纯而感性的想法就此破灭。我不想手工输入这些紊乱的名字,对它们的使用频率之少使我在每次使用之前都必须进行查阅(尤其是其中有许多是不常用的错误值或输入标志,因此要相隔相当长的一段时间才会再次使用)。另外,全大写的写法看上去像有人在向你吼叫。
我个人的习惯是使用单个字符,用't'来标记转置,用'p'来表示路径错误。我觉得这已经具备足够的提醒作用。事实上,我觉得'p'的方案在拼写上也比那些采用全大写的方案容易记忆得多,并且它不需要在名字空间中添加新项。
在提到老生常弹的效率问题之前,记住枚举一般是个整数,而char事实上只是C对于单字节的说法。因此,对枚举进行比较时,很可能需要比较16个状态位甚至更多。但是如果使用了char,只需要比较8位。因此,即使在速度问题相当重要的情况下,使用单个char的方案也胜于枚举。
我们有时候需要对标志进行组合。当我们使用系统调用open打开一个文件时,我们可能需要发送ORDWR|OCREAT,这是两个枚举的逐位组合。我们很可能不会非常频繁地直接使用open,而是使用POSIX的fopen,后者更为友好。它并没有使用枚举,而是使用了一个单字母或双字母的字符串(例如"r"或"r+")来表示某个文件是可读的、可写的、同时可读写的等。
在这个上下文环境中,我们知道"r"表示读取。即使没有记住这些约定,在使用过几次fopen之后,我们也可以对这些标记了如指掌。反之,对于CblasTrans或CblasTranspose,我每次在使用之前都必须进行查阅。
枚举当然也有优点,它表示一个小型的固定符号集,因此如果我们误输入了其中一个,编译器就会停止,并迫使我们修正打字错误。对于字符串,在运行时之前是无法知道输入错误的。但反过来,字符串并不是小型的固定符号集,因此我们可以更轻松地对枚举集进行扩展。例如,我曾经看到过一个错误处理程序,将它自身提供给其他系统所使用,前提是新系统所产生的错误要与原系统的枚举所包含的错误匹配。如果这些错误是短字符串,其他人对此进行扩展也是很简单的。
有一些理由支持使用枚举:有时候我们有一个数组,没理由把它作为结构,但是它又需要使用命名元素;或者在完成内核层次的工作时,为位模式提供名字具有重要意义。但是,当枚举用于表示一个简短的选项列表或者一个简短的错误代码列表时,单字符或者短字符串同样可以完成任务,同时又不至于扰乱名字空间和用户的记忆。
本文摘自《C程序设计新思维(第2版)》

深入解析C语言特性
塑造编程新思维

本书展现了传统C语言教科书所不具有的最新的相关技术。全书分为开发环境和语言两个部分,从编译、调试、测试、打包、版本控制等角度,以及指针、语法、文本、结构、面向对象编程、函数库等方面,对C程序设计的核心知识进行查缺补漏和反思。本书鼓励读者放弃那些对大型机才有意义的旧习惯,拿起新的工具来使用这门与时俱进的简洁语言。
本书适合有一定基础的C程序员和C语言学习者阅读,也适合想要深入理解C语言特性的读者参考。
作者简介:

Ben Klemens 自从于加州理工学院获得社会科学博士后,就一直从事统计分析和人口的计算机辅助建模工作。他的观点是,写代码一定应该是趣味横生的,并先后非常愉快地为布鲁金斯学会、世界银行、美国国家精神健康中心等机构写过分析和建模代码(主要是C代码)。他作为布鲁金斯学会的非常驻研究员,与自由软件基金会一道,做了很多工作来确保有创意的程序员拥有保留其作品使用权的权利。他目前为美国联邦政府工作。

延伸推荐

点击关键词阅读更多新书:
Python|机器学习|Kotlin|Java|移动开发|机器人|有奖活动|Web前端|书单

在“异步图书”后台回复“关注”,即可免费获得2000门在线视频课程;推荐朋友关注根据提示获取赠书链接,免费得异步图书一本。赶紧来参加哦!
点击阅读原文,查看本书更多信息
扫一扫上方二维码,回复“关注”参与活动!

点击阅读原文,查看《C程序设计新思维(第2版)》