我从调试书籍中抽出时间来谈论一场灾难性的教科书写作秀:罗伯特·J·特雷斯特(Robert J.Traister)的“掌握C指针:编程能力的工具”(Masting C Pointers:Tools for Programming Power)。
我是通过Brian Kernighan的一次演讲了解到这本书的,他在演讲中称这本书可能是“有史以来最糟糕的C编程教科书”。[1]他没有指名道姓,但在一些帮助下,我能够找到他间接准确的参考文献。
这本书已经成了我的白鲸。自从我开始阅读调试书籍,尤其是现在我正在挖掘较旧的书籍,我发现一些建议今天根本不管用。虽然其中一些可能被认为是无用的或愚蠢的,但我总是发现作者们来自认真的立场,试图根据体面的原则和他们写这本书时所知道的东西得出最好的结论。在某些情况下,他们可能知道的不多,但他们&。
当Kernighan举了下面的例子时,我看到了似乎与之相反的东西。
Char*Combine(s,t)char*s,*t;{int x,y;char[100];strcpy(r,s);y=strlen(R);for(x=y;*t!=';\0';;++x)r[x]=*t++;r[x]=';\0';return(R);}。
这个程序(保留了格式)摘自该书的第一版(第146页)。它很可怕(科尼根称它为“渎职”)。它没有表现出我在20世纪70年代末有关如何调试Basic程序的书中看到的真实性。我不知道它是什么。欺骗?懒惰?无休止的无知?写这篇文章的人的心态是什么,大概认为这是一个好主意?
这本书有两个版本:第一个版本是1990年出版的,第二个版本是1993年出版的。两个版本的事实更激起了我的好奇心。它的销量足以制作另一个版本?那个可怕的例子得到纠正了吗?我拿到了两个版本的副本,并阅读了它们。
评论这本书毫无意义。Kernighan是对的:它是垃圾。第二版只会让事情变得更糟。任一版本的教学都是违反道德的。如果你遵循特雷斯特的编码实践,即使根据今天的标准进行调整,你也肯定会制造缺陷和恶毒的潜在错误。这本书的一个微妙有害的方面是写作的随意基调。这本书足够非正式,如果你不太了解C语言,他说的有点像是…
任何训练有素的程序员都会认识到这些教训毫无价值。他的术语到处都是,而且通常是不准确的,如果不是明显错误的话。表达式“返回一个值”。值被“交给”位置。常量被“直接写入程序”。联合是一个“专用指针”。术语甚至不一致。微优化总是被强调的,程序效率比理解更重要。他甚至不能正确地定义指针:“指针是返回内存地址的特殊变量”
虽然我不打算复习一下内容(那会花很长时间,因为那里没有什么可取之处),但我确实做了大量的笔记。如果你有兴趣的话,你可以读一读。
然而,我必须,花点时间挑出代码。它普遍是糟糕的,其中大部分是完全错误的。(想象一下,试图从一本包含大量甚至不能编译的程序的书中学习编程原理。)我已经转录了一些程序,并用注释对它们进行了注释,这样您就可以体验到Traister作为一个C程序员是多么的笨拙。需要记住的一件事是(对于程序和笔记)C89/C90在当时是新的,并且代码是在(和为)MS上编写的。
材料讲得够多了,我想探究这样一个问题,这么错误的东西是怎么写出来的。这并不是说书中的每一件事都是错的,但感觉就是当它是对的时候,它就是偶然的正确的。
特雷斯特还写过其他一些书,有些是关于电子学的,有些是关于编程的,其中一本名为从Basic到C。在掌握C指针中,他谈到了他创建的一种名为CBreeze的产品,该产品将Basic代码转换为C代码。在这本书中,他通过对Basic的迂回引用,使用的术语表明他写了很多基本代码。例如,有整整一章介绍了使用指针访问内存,其中读和写内存被称为基于窥视的“窥视”和“戳”我确信他精通BASIC知识,并致力于为小型电子设备编写软件。
为什么这很重要?当我读这本书的时候(如果你读了我的笔记,你会知道这是怎么回事),我开始注意到措辞和语气中的一些东西。我越往前走,我就越相信这一点,我想这解释了他为什么如此严重地破坏了C指针的解释。
对于Basic,当时关于大多数实现需要了解的关键一点是,除了全局作用域之外,没有任何函数和作用域。[2]与BASIC中的函数最接近的是GOSUB命令。GOSUB命令跳转到一行并执行代码,直到它到达RETURN语句,在该语句中,控制权将转移回跟随GOSUB命令的行。在GOSUB中,您可以使用另一个GOSUB跳转到其他地方。该控件遵循堆栈原则,但不传递参数。GOSUB例程是提取公共代码的一种方法,但该公共代码必须作用于全局变量。(和。
现在,考虑一下简单的电子设备的情况。即使在今天,一些通常用C语言编程的嵌入式设备也没有为自动变量动态分配空间的调用堆栈。这就是没有足够的内存。取而代之的是,编译器布局内存,使得每个函数的局部变量都有固定的内存地址(“编译堆栈”模型)。您拥有的唯一堆栈是用于返回地址的,它可能是在硬件中处理的。
假设您习惯于为小内存电子设备编写BASIC,并且了解了C语言。您阅读了有关指针的知识,并意识到:可以编写一个可以在不知道变量名称的情况下更改变量的子例程。这简直是上天赐予的恩赐!您不必再将全局变量指定为子例程的“参数”。生活是美好的。
这就是我认为Traister曾经有过但从未改变过的心态。在这本书中,有一次短暂提到堆栈,提到过多的(自动)内存分配。(在MS-DOS上,如果程序中的局部变量空间太大,它可能无法编译。)他始终如一地将变量描述为在程序中具有“独占”地址。他写的关于指针的文章表明,他认为,对于每个函数,都留出空间来在程序持续时间内保存局部变量。但是您只能在声明它们的函数内部访问它们,所以指针非常强大,因为您可以将此地址提供给另一个函数,并且它只需使用一个参数就可以更改该值。
他缺乏理解力的另一个证据是,他经常引用函数中荒谬的空间微优化,比如如果可能的话,避免将整数用于索引变量。另一个经常被提到的是具有固定大小的局部字符数组。不使用它们有很好的理由,但他的不在其中。他的警告是,它们浪费空间。从技术上讲,这是正确的,但它们直到上架才存在。而且他从来不说话。
一旦你开始询问malloc的函数是如何工作的,这种解释就会遇到一些问题,但值得指出的是,几乎没有关于内存管理的讨论。在一本专门讨论C指针的书中,这是严重疏忽和无能的有毒组合。几乎只有一个短小的段落专门讨论自由函数-它被描述为“旁注”。
这种解释的另一个症结是Traister编写接受可变数量参数的函数的令人费解的方法。他通过将任意数量的参数传递给函数(第一个是参数的数量)并使用第一个参数地址的偏移量来访问它们来实现这一点。这表明他对以动态方式传递参数有一定的了解,但这是非常错误的[3],让您怀疑他是否在发布程序之前尝试过他的程序。
老实说,这是我能想到的对文本最慷慨的诠释,但它仍然描绘了一幅可怕的画面。奥卡姆的“剃刀”认为特雷斯特只是一无所知。但就像分析一部不知何故制作出来的可怕电影一样,从“幕后”部分进行推理会更有趣。
考虑到这本书的拙劣,你会认为它是自己出版的。你错了。它是通过学术出版社出版的,学术出版社当时是哈考特律师事务所(HarCourt,Brace&;World)的一个部门,但现在是“爱思唯尔的印记”。
在第二版的序言中,它说第一版是“由出版商聘请的专业C程序员审阅的”,该程序员说不应该出版,这位程序员是对的,但出版商还是继续出版了它。
[5]这并不是说当时的科技出版业有什么特别之处。我认为,当时有效的机会主义在今天仍然存在,而且可能一直都是如此。可以说,今天的情况更糟。
[6]我之所以知道这一点,是因为这本书的一段奇怪的、杂乱无章的宣传视频,其中特雷斯特的名字被新作者遗忘了。