无论一门语言有多么流行或多么优秀,它总是存在一些问题,C语言也不例外。本章讨论的重点是C语言本身存在的问题,作者煞费苦心的用一个太空任务和软件的故事开头,也用另一个太空任务和软件的故事结尾,引人入胜。
关于这两个故事,在这里不说,有兴趣的朋友还是建议买这本书去看看,这本书用相当轻松的文字而又不失深沉地向我们道来C语言的各种特性与特别的用法。
书中提到一种分析编程语言缺陷的方法,让我们能够详细的去分析各种编程语言的缺陷,即把所有的缺陷归于3类:不该做的做了 (多做之过)、该做的没做 (少做之过)、该做的做了但不合适 (误做之过),本章也是按照这样一种分析方法来分析C语言本身存在的一些问题,由于C是一门神奇的语言,被许多平台所选用,也被大家所学习,所以了解C语言是一件相当有必要的事情,本章就是从缺陷来了解C语言。
多做之过,就是语言中存在某些不应该存在的特性,包括容易出错的 switch 语句、相邻字符串常量自动连接和缺省全局作用域。
首先说说 switch 语句吧,这个语句在多条件的时候使用率还是相当高的,相比大量 if 语句,我还是比较倾向于它的。switch 语句的一般形式如下:
switch(表达式)
{
case 常量表达式 1: 语句 1; break;
....
case 常量表达式 n: 语句 n; break;
default: 语句; break;
}
其次,相邻字符串常量的自动合并这个约定也会带来一些问题。在 printf 的使用中,这是一个优点,因为你不用担心要输出的字符串有多长,你可以放心的用双引号包括每一行的内容,反正它会自动合并,比方说
printf("A second favorite children's book"
"is Thoms the tank engine and the Naughty Enginedriver who"
"tied down thomas's boiler safety value");
这个 printf 语句会自动连接三个行,这可以使每一行的代码看起来简洁而又完整,不过,你该担心的是下列情况:
char *available_resouces[] = {
"color monitor",
"big disk"
"Cray"/* 少了一个逗号 */
"on-line drawing routhines",
...
在这种情况下我们都知道,由于数组大小的缺省,而少了逗号会使两个字符串常量自动连接,所以在编译器看来,这并不是一个错误,它也就不会提示你,而程序可能会莫名其妙的运行,打印 "Crayon-line drawing routhines" 或是修改其他变量,因为字符串数目比预期少了一个。
缺省可见性这个问题主要体现在全局函数的定义上,我们知道在声明函数的时候,如果没有任何关键字限制,那么会被自动定义为全局函数,除非你加上 static 关键字,才能限制对这个函数的访问。事实上,几乎所有人都没有在函数名前添加存储类型说明符的习惯,所以绝大多数函数都是全局可见的,然而,根据实际经验,这个缺省的全局可见性多次被证明是个错误。软件对象在大多数情况下应该缺省的采用有限可见性,当程序员需要让它全局可见时,应该采用显式的手段,原因在于这种大范围的全局可见性会与 C 语言的另一个特性产生影响,也就是用户编写和库函数同名的函数并取而代之的行为。这也说明了在 C 语言中,对信息可见性的选择很有限,要么是 extern,意味着整个库的所有对象都可见,要么是 static,对其他文件都不可见。
所谓 "误作之过",就是语言中有误导性质或是不适当的特性,这些特性跟 C 语言的简洁性有关,有些则与操作符的优先级有关。
C 语言存在的其中一个问题就是它太简洁了,仅增加、修改或删除一个字符就会使原先的程序变成另外一个仍然有效但全然不同的程序,这就意味着,如果你在一个小问题上出了一点问题,那么编译器是不会检查出来提示你的,因为你的程序仍然有效。当然,还造成一个问题,那就是很多符号同时具有好几种意思,你要直到它到底是什么意思,还要根据上下文来,这一点尤其体现在作用域上。比方说 static 关键字就曾经令我疑惑,它有时候表示静态变量,有时候又表示内部链接属性,那么它到底代表什么呢?正确的答案是这样的,在函数内部,表示静态变量,当表示函数时,代表内部链接属性。同样的 extern 关键字也是这样,在缺省可见性已经提到,extern 的外部链接属性不应该作为缺省属性。还有 & 操作符,既表示取地址操作符,又表示按位与操作,同样 * 操作符也有多种含义,最明显的、用法最多的操作符可能还是要数 () 操作符了,它们无处不在。一个符号所表达的意思越多,编译器就越难检测到这个符号在你的使用中所存在的异常情况。
另外在操作符的优先级上,我完全能够感同身受,初学 C 语言,甚至在学完 C 语言很久一段时间之内,我都没有真正的完全搞清楚过操作符的优先级,凭感觉用吧,一般来说结果都是错的,不过用多了,可能也就会了。还记得 ->这个操作符在结构指针中的使用吗,我们知道 ->这个操作符是对一个结构成员进行解引用,它所代表的意思 p->f 也就相当于 (*p).f,不过千万别忘了添加括号哦,因为 "." 操作符的优先级大于 "*",这个问题也是导致 -> 操作符出现的原因之一,类似的还有很多,比如 [] 的优先级高于 *,int *p[]这个表达式呢代表 p 是一个元素为 int 指针的数组,而不是说 p 是个指向 int 数组的指针哦。不过在多年前,Dennis Ritchie 解释了这些不正常的情况是如何由于历史的偶然原因而产生的,最大的原因还是,如果现在把它们更改过来的话,现有的大量代码都可能出现问题。
最后,少做之过的特性就是语言应该提供但未提供的特性,如标准参数处理以及把 lint 程序错误的从编译器中分离出来。
标准参数处理这个问题不管是在 UNIX 还是在C语言中都没有得到好好的处理,因为参数与文件名,程序是分不清楚的。其中一个例子就是在在 UNIX 中创建一个文件,文件名以'-'连字符开头,然后却发现无法用 rm 命令把连字符去掉,这就是它分不清文件名与参数的影响,书中还给出一个有趣的实例——关于在 1990 年以前给 "用户名的第二个字母是 f 的用户" 发邮件,那么他将收不到,进一步让我们理解分不清参数与文件名的影响。
而 lint 程序,甚至现在好多使用 C 语言的人都没有听过,在早期的 C 语言中,语言设计者作出了明确的规定——把编译器中所有的语义检查措施全部分离出来,错误检查由一个单独的程序完成,这个程序被称为 "lint",在省掉 lint 之后,编译器可以做得更小,更快而且更简单,所以理所当然的,它被去掉了,不过,所付出的代价是,代码中悄悄混入了大量的 Bug 和不可靠的编码风格,许多程序员缺省情况下在每次编译中并不使用 lint。在书中给出了一些实例,是一些程序员在写代码的过程中容易犯得错误而编译器又检查不出,如果使用 lint 程序,则可以全部检查出来,所以作者大力推荐使用 lint 程序作为检查。
下面,来介绍一下这个 lint 程序吧。
lint 程序不但可以检查出可移植性问题,而且可以检查出那些虽然可移植并且完全合乎语法但却很可能是错误的特性,lint 程序会产生一系列程序员有必要从头到尾仔细阅读的诊断信息。
这是 lint 程序的系统版本:
UNIX 系统 在 UNIX 系统中,可自动获得 lint,它是一个标准的 UNIX 工具。Linux 系统 在 Linux 各种发行版中, 使用 lint 的版本是 GNU 下的 Splint(前身是 LClint)Windows 在 Windows 系统中,从第三方获得的 lint 工具的名称是 PC lint 以及 Splint 在这里,由于我使用的是 Linux,所以介绍一下 Linux 中 lint 的使用。首先安装 splint 工具:
- sudo apt install splint
然后假定你要检查的文件是 main.c
- splint main.c
其中 main.c 中代码如下所示,使用了 switch 语句来测试:
- #include
- int main(void)
- {
- int x;
- scanf("%d",&x);
- switch(x){
- case 3:
- printf("4\n");
- case 4:
- printf("4\n");
- }
- return 0;
- }
如果是直接 gcc main.c,那么不会有任何提示,使用 splint 程序之后,它显示了这些文本:
- Splint 3.1.2 --- 03 May 2009
- main.c: (in function main)
- main.c:6:5: Return value (type int) ignored: scanf("%d", &x)
- Result returned by function call is not used. If this is intended, can cast
- result to (void) to eliminate message. (Use -retvalint to inhibit warning)
- main.c:10:10: Fall through case (no preceding break)
- Execution falls through from the previous case (use /*@fallthrough@*/ to mark
- fallthrough cases). (Use -casebreak to inhibit warning)
- Finished checking --- 2 code warnings
显而易见的是,它给出了两条提示,一条是说你的 scanf 语句的返回值并没有用,另一条就是 switch 语句没有 break 语句,并且提示你,如果确实不需要 break 语句,请用 /*fallthrough*/ 把它注释出来。
所以,多用 lint 程序来检查你的程序吧,说不定会给你一个惊喜。
来源: