315,关于《C程序设计伴侣》一书致人民邮电出版社的公开信
邮电社,不出版文盲写的书行吗?
目录
邮电社,不出版文盲写的书行吗?... 1
抄袭拼凑... 2
欺骗读者... 2
硬伤累累,错谬概念层出不穷... 3
关于关键字... 3
关于标识符... 3
关于常量与变量... 3
关于运算符... 3
关于数据类型... 4
关于表达式... 5
关于声明... 6
关于语句... 6
关于数组... 8
关于初始化... 8
关于函数... 9
关于外部变量与局部变量... 10
关于作用域与生存期... 10
关于字符串... 12
关于库函数... 12
关于指针... 14
关于main() 15
关于输入输出... 15
关于结构体、共用体、枚举... 16
关于链表... 16
关于typedef 16
关于文件... 16
关于对齐... 17
不会数数... 17
重复谭浩强《C程序设计》中的错误... 18
缺乏计算机基础常识... 18
滥造术语... 18
信口开河... 19
代码错误... 20
第1章:... 21
第2章:... 21
第3章:... 22
第4章:... 23
第5章:... 24
第6章:... 25
第7章:... 26
第8章:... 28
第9章:... 31
第10章:... 32
第A章:... 33
第B章:... 33
最后一个小插曲... 33
策划和副总编的问题... 34
呼吁与期待... 40
人民邮电出版社,北京
季仲华 社长
顾冲 副社长
综合出版中心 马嘉 社长
信息分社 刘涛 社长:
2012年10月,贵社出版并发行了一本《C程序设计伴侣》(以下简称《伴侣》)。经阅读之后,我们发现,这是一本涉嫌抄袭和欺骗读者、由C语言文盲(最多是一个C语言半文盲)粗制滥造的、严重误导初学者的C语言书。这本误人子弟的劣书最终得以出版流入市场,乃该书策划编辑、贵社图灵公司副总编陈冰一手促成。
该书至少存在如下一些问题:
抄袭拼凑
该书第一章的许多部分,是将那些从网上搜集的劣质资料复制粘贴或稍作文字修饰而成的(至少有12处:p.4、5、6、7、23、24),很多地方都是成段地抄袭。
这些资料中的大多数,本身就漏洞百出;有些资料彼此矛盾,却被《伴侣》原封不动地搬了过来,甚至其中的病句也照抄不误;有的资料原本是正确的,却被《伴侣》改错了。
例如:“C++语言……比C语言更容易为人们所学习和掌握”(p.5);“编译器只是将输入的.cpp等源代码文件生成.o为后缀的目标文件”(p.24)。
类似情况,第二章至少有3处(p.28、31、36);第四章至少一处(p.78);第七章至少一处(p.143)。
第B章类似情况则数不胜数,至少有24处(p.292、293、294、295、296、297、298、299、300、301、302、303、309、310)。该章基本是由各种网上资料拼凑而成:整段地复制豆瓣书评、网店图书介绍、百度百科、百度文库、甚至有其他作者原创并发表在网上的读书笔记和图书评论。(详见http://bbs.chinaunix.net/thread-4066105-1-1.html)
然而《伴侣》并没有以参考文献形式呈现上述资料,也没有在书中提及这些内容的出处,只在“致谢”中轻描淡写地说了一句:
“参考了一些网络上的资料,在此也一并感谢这些不知名的默默无闻的分享者”。
实际上,这些资料绝大多数都有署有原作者之名。在这种情况下,该书的编者贸然地将其列入“图灵原创”丛书并将其以“著”归于一位作者之名下,我们认为有欺世盗名之嫌。
欺骗读者
该书“怎样使用这本书”部分(p.6)告诉读者:“添加/Tp.编译选项进行编译”。
实际上使用/Tp. 参数编译的含义是:无论何种扩展名的源文件都会被当作C++源文件编译。换句话说,书中的很多代码是C++代码而压根不是C代码。这实际上是用C++代码来假冒C代码。
198页声称“我们先来编写这样一段使用函数指针的C语言代码”,然而其后及199页的两处“#include “stdafx.h” ”,及199页的两处“Functionpointer.cpp”都说明实际上它们是C++代码。
290页的插图赫然表明在此被调试的是一段C++代码;314页的代码同样说明了这一点,因为这段代码若以C程序对待,则根本就无法通过编译。
118页号称“开发一个嵌入式的程序”,实际上此程序和嵌入式八竿子打不着。
在我们看来,该书公然地出现上述内容是对读者的严重欺骗。然而为了掩饰用C++代码冒充C代码,《伴侣》“怎样使用这本书”部分竟然把这种谬误说成是“因为用到了一些特殊的编译器扩展”,却绝口不提这是在编译C++代码。
硬伤累累,错谬概念层出不穷
关于关键字
“另外,这个头文件中定义的表示逻辑真假状态的关键字true和false的使用,可以让我们的程序更具可读性。” (p.73)
“使用true和false这两个关键字来表示逻辑上的真和假”(p.74)。
事实上,true和false从来都不是C语言的关键字。
关于标识符
“C语言中的标识符实际上就是给变量取一个名字,便于我们访问这个变量”(p.48)
事实上,标识符绝不是仅仅用于命名变量,还用于命名函数、命名类型、命名标号、命名常量……,也绝非仅仅是为了“访问”“变量”。
关于常量与变量
“在C语言中,除了使用const关键字来定义常量之外” (p.47)
实际上尽管不可显式改变const变量的值,但const变量依然是变量,与常量有本质的区别。
这说明:该书作者对C语言的“常量”(Constant)概念几乎完全无知。
“……在定义变量之前加上const关键词……所以这个变量……而成为一个常量” (p.47)
变量变成常量,比水变汽油还神奇。
“优先选择用const关键字来表示变量。” (p.48)
关键字不能“表示”变量。
“不能通过指向常量的指针修改它所指向的常量”(p.182)
根本不存在指向常量的指针。
关于运算符
荒谬地宣称“所有的二元运算符都可以与赋值运算符(“=”)相结合而形成组合赋值运算符”(p.60)。
事实上,很多二元运算符都不可能与赋值运算符组合成新的赋值运算符,例如&&、==、>=等二元运算符就不可能与赋值运算符组合成新的赋值运算符。
“但是,我们没有必要去死记硬背那些各个运算符之间的优先次序关系,最安全也是最简单的方法是,合理使用小括号,在条件表达式中清晰地表达你想要的运算顺序,而不是去依赖于各个运算符之间的优先顺序。”(p.71)
这是似是而非的误导,原因在于作者不懂得优先级和结合性的确切含义。
实际上,小括号跟运算次序没有关系,优先级与运算次序也没有必然的关系。运算次序是由实现确定的。
该书作者完全不清楚&&运算的短路性质。例如:
“(a>0)&&(b>0)
在计算这个逻辑表达式的值的时候,会根据小括号确定的运算次序(或者表达式的默认运算顺序),首先计算a>0和b>0这两个关系表达式的值,然后逻辑运算符“&&”会根据这两个关系表达式的值最终得出整个逻辑表达式的值。” (p.71)
事实上,&&运算总是先求&&运算符左侧操作数的值。
“使用“==”关系运算符比较两个浮点数的结果,取决于计算机硬件和编译器的优化设置。”(p.54)
这是该书作者毫无根据的信口开河。
“()不仅是某些情况下改变运算符优先级,”(p.56)
这是《伴侣》中的胡扯!优先级是运算符固有的性质,在任何情况下都不可能被改变。
关于数据类型
夸张且荒谬地声称
“在现实世界中,数据除了有是否可以改变的区别之外,我们发现数据还有不同的类型。有的数据只是一个简单的整数,例如人的身高;有的则是一个精度很高的浮点数,例如圆周率;有的数据很大,例如地球的直径;有的则很小,例如一个细胞的体积;有的是一个数值,而有的则是一个字符串。为了描述这大千世界中的各种数据,C语言提供了丰富的数据类型,而现实世界中的每一种数据都可以在C语言中找到合适的数据类型来表达。” (p.48)
事实上,数据类型是程序设计语言的范畴,在现实生活和数学理论中根本就没有“数据类型”这种概念,而且也不是现实世界中的任何数学量都可以在C中找到合适的数据类型来表达,譬如圆周率就无法用任何数据类型精确地表达。
完全不清楚C语言数据类型的分类,荒谬的宣称
“以上我们所介绍的三种数据类型(注:指基本类型、枚举类型和void类型),其数值都是使用单个数字表示的,所以被称为纯量类型” (p.50)
事实上,void类型根本不是“纯量类型”(scalar type);而且“数值都是使用单个数字表示的”这种说法也是错误的,123、ABC、-123这些数值都不是使用单个数字表示。
该书甚至对常用的整数类型的表述也有很多常识性错误。例如:
"整型数据……按照占用内存字节数多少,也就是能表示的整数范围的大小,C语言中的整型被分为基本整型(int)、短整型(short)、长整型(long)和双长整型(long long)这四种。而根据是否具有符号位、能否表示负数,这四种类型又可以分别加上signed和unsigned修饰,成为各自的有符号整型和无符号整型。"(p.51)
这里至少有三个错误:首先,C语言中的整数类型并不是“按照占用内存字节数多少,也就是能表示的整数范围的大小”划分的;其次,C语言的整数类型绝非这几种,至少还char、_Bool、枚举等类型;第三,所列举的四种并不是“分别加上signed”而“成为各自的有符号整型”,他们本身就是“有符号”整数类型。
"char类型被专门用来表示C语言的字符集中的各种字符,不要把它当成一个整型数据类型来使用"(p.52)
一句话两个错。“C语言的字符集”是莫名其妙且似是而实非的概念,不知所云。char类型本来就是整数类型的一种,若不作为“整型数据类型来使用”,难道还能当成别的什么类型吗?
“如何确定常量的数据类型,这是编译器的事,我们就没有必要在此多费脑筋、浪费时间了。”(p.55)
外行话,误导读者。程序员不清楚常量的类型而写出的程序代码,最多只能是瞎猫碰上死耗子的代码。
“出于控制常量内存分配的目的,我们可以在常量后面加上特殊的字符,强制指定常量的数据类型,这样可以控制常量的内存分配和存储方式。例如:
float pi = 3.14f;//将3.14当作float类型处理”(p.55)
常量不存在分配内存问题;也并不存在什么“在常量后面加上特殊的字符”,这里所说的“特殊字符”,本来就是常量表示方式的一部分。
“……数据类型,它是用来表示数据的”(p.61)
数据类型不是用来“表示”数据的。
“struct student
{
//……
};
……形成了student这个新的数据类型。”(p.223~224)
student并不是数据类型。《伴侣》在这里混淆了C++和C语言。
“如果我们需要表示一个大数值……那么我们应该选择long long类型”(p.51)
误导读者。关于数据类型的选择,是一个综合性的策略问题,不是仅仅根据数值大小决定的。
关于表达式
“所谓的表达式……其作用是描述一个计算过程”(p.55)
错。表达式的作用是让实现求它的值和产生副效应,不是描述计算过程。
“我们可以用表达式表示对一个变量进行各种算术运算最后获得结构的过程:
float pi = 3.14f;
float r = 2.0f ;
float area = pi * r * r ;
这三个表达式就描述了一个计算圆的面积的过程。”(p.55~56)
这是三个变量声明(定义),不是三个表达式。
“C语言中的表达式,包括运算符的优先级等等,跟我们在数学中学到的各种表达式在本质上是一样的”(p.56)
严重的概念错误及误导。若在数学中出现x=x+1这样的表达式,它将被认为是错误的命题,何谈“本质”?
“用C语言表达式描述就是:
float pi = 3.14f;
float r = 2 ;
pi * r * r ;
然而,这仅仅是个表达式”(p.58)
错!这是两个声明和一条语句。
关于声明
“float area=pi*r*r;
这就C语言的语句。”(p.58 )
错!这是一个声明(declaration),不是语句(statement)。
"如果const关键字在“*”指针符号之后,则表示const关键字是对这个指针变量本身进行修饰。"(p.181)
const从来不是修饰变量本身的。
关于语句
“C语言中的语句……分成以下几种:控制语句,函数调用语句,表达式语句,空语句,复合语句”(p.58~59)、
至少有两个错误:
1.不全,漏掉了标号语句;
2.函数调用本身就是表达式,《伴侣》硬造出一个“函数调用语句”,而且将其与表达式语句并列。这跟把萝卜和蔬菜等量齐观没什么区别。
更荒唐的是,作者连什么是语句都不清楚。在举例说明什么是函数调用语句时,竟然说
“char upper = toupper('a');
这个函数调用语句实现的功能就是将参数字符a转换为对应的大写字符A,并将其保存到变量upper中。”(p.59)
实际上这行代码根本连语句(Statement)都不是,这是一条声明(Declaration)。
“表达为一个完整的C语言语句就是:
float area = pi * r * r ;
这就是C语言的语句。”(p.58)
这不是语句,而是一个关于变量的定义及初始化。
“这里需要注意的是,switch语句的每一个分支条件必须是一个常量,它可以是某个数值常量,也可以是某个枚举值。”(p.78)
switch语句描述有误,case的lable必须是整数常量表达式,而不是什么“数值常量”。C语言中根本就不存在“数值常量”这样的概念,这个概念是作者臆造的一个伪概念。其次枚举常量本来就是一种整数常量,根本就谈不上什么“也可以”。
“循环结构的四个要素”(p.82)
捏造。
“do...while语句是while语句的一个变种” (p.86)
胡扯。do-while语句和while语句彼此独立,是两种不同的语句。
“for( 初始化语句 ; 条件表达式; 更改语句 )
{
循环体语句;
}“(p.87)
捏造。C语言中根本不存在的所谓“初始化语句”“更改语句”“循环体语句”。更荒谬的是,作者说:
“初始化语句可以是C语言中的任何语句”(p.87)。
“由一个分号构成的空语句并不具有太大的实际用途,用得最多的就是用它来占位置,表示这里的代码尚未完成,还有待进一步补充完善。”(p.59)
看来《K&R C》的书里的很多代码“尚未完成,还有待进一步补充完善”。
“而do-while则是先进行循环,然后再进行条件判断,所以它更多地应用在那些可以无条件进行循环的场景。”(p.90)
没有“更多地应用在那些可以无条件进行循环的场景”这个事情。
“函数内的局部变量定义语句(例如后文的“static int total=0;”)”(p.161)
混淆声明和语句。
“……定义了一个静态局部变量total……当eat()函数首次被调用的执行的时候,这个变量即被创建并保存在静态存储区……“static int total = 0 ; ”这条语句……”(p.162)
total不是在“eat()函数首次被调用的执行的时候”“被创建”;static int total = 0 ; 不是语句。
“我们也没有必要像孔乙己一样去深究什么样的语句应该是声明,而什么样的语句又是定义。”(p.166)
“我们将不需要建立存储区域的语句看成是声明……而将需要建立存储区域的语句称为定义” (p.166)
典型的以其昏昏使人昭昭。事实上,声明和定义都不是语句(Statement)。
“从代码中删去“#define DEBUG”语句”(p.180)
那根本就不是语句。
关于数组
"数组名实际上就是一个指针变量"(p.187)
尽管数组名有时可以被视为指针,但并非总是如此,更不是“指针变量”。
"数组名就是指向第一个数据元素的指针"(p.188)
以偏概全,误导。
"如果我们有一个二维数组“scores[5][30]”"(p.191)
在C语言中,scores[5][30]不是二维数组,什么都不是。因为数组必须描述数组元素的类型。
"数组名的类型无非就是在它所保存的数据类型后加一个“*”号"(p.214)
显然,作者压根就没看过在《伴侣》第295页中他自己向读者推荐的《C专家编程》。否则绝对说不出如此外行的话。
关于初始化
“6.1.3使用memset()函数进行一维数组的初始化”(p.98)
标题就错了。这根本就不是C语言中初始化的概念。这是一种荒谬的原则性错误,彻头彻尾的误导,荒谬绝伦。《伴侣》错误地宣称
“它是对较大数组进行初始化清零操作的一种最快方法”(p.99)
实际上memset()函数并不能一般性地"在一段内存区域中填充某个给定的值"(p.99)。
“二维数组……实际上并不适合使用初始化列表对其进行初始。”(p.103)
荒唐的言论,误导。
“在实际的开发中,我们很少用初始化列表来完成二维数组的初始化” (p.103)
这不是事实。
“我们优先使用memset()函数来完成其初始化工作。例如:
//使用memset()函数完成二维数组的初始化
int scores[6][100];
memset(scores,0,6*100*sizeof(int));”(p.103)
第一,“使用memset()函数完成二维数组的初始化”在概念上就是错误的。因为memset()函数的作用是填充一块内存中的各个字节,而不是初始化各个int类型对象;
第二,对scores数组的(清零)初始化可以极其简单地通过
int scores[6][100] = { 0 } ;
来完成。
“这样,我们就可以一次性地给二维数组中的多个数据赋予初始值,完成其初始化工作。……而使用初始化列表,还需要我们去数它到底是对哪一个数据赋值。所以,我们应该更多地使用memset()函数来完成二维数组的初始化,而尽量少使用初始化列表。”(p.103)
这些说法是错误的,是对初学者的误导。实际上memset()函数根本不可能一般性地对数组进行初始化。
“字符数组的定义、初始化以及数据元素的引用方面,它同一维并数组无差别”(p.109)
错。字符数组的初始化有一种特殊形式。
“//现在,msg是一个字符数组,其中的部分数据已经被分别赋值
char msg[256] = {'I',' ','l','o','v','e',' ','J','i','a','w','e','i'};
//将字符数组的某个字符赋值为字符串结束符'\0'
msg[6]='\0';
//含有字符串结束符的字符数组已经成为一个字符串”(p.109)
两个错误:事实并非“部分数据已经被分别赋值”,而是全部数据都被初始化;msg不是“msg[6]='\0';”之后"成为一个字符串",而是一直存储着字符串。
与此对应第110页的图同样是错误的。
关于函数
“当我们要调用某个函数时,必须知道这个函数的声明,也就是要知道这个函数的函数名、参数以及返回值等等,这样我们才知道如何对其进行调用。否则,编译器在编译这个函数调用时,并不知道它是一个函数调用,就会产生编译错误”。(p.16)
“会产生编译错误”的说法实际是作者臆想出来的。
"在程序调用函数的时候,可以根据函数声明找到这个函数"(p.23)
“程序调用函数的时候,也正是通过函数声明来找到它所调用的函数,……为了在函数时能够找到被调用的函数,我们需要函数声明。”(p.124)
无稽之谈。完全不懂得函数声明的含义。
自相矛盾的是,紧接着又说:
“函数声明……会告诉函数的使用者(调用者)这个函数有什么作用(函数名)……所以我们需要函数声明” (p.124)
“函数声明的本质意义,就是为了告诉函数的使用者(调用者)这个函数是做什么用的(实现了什么功能,解决了什么问题)、他需要哪些输入数据(函数的前置条件是什么)以及它最后向函数调用者返回的结果是什么”(p.125)
这完全是一个外行的胡扯。从函数声明本身根本不可能了解“这个函数是做什么用的”。函数声明也不是给调用者看的。
"从函数返回数组"(p.185)
"另外,如果函数的返回结果数据较大(例如,从函数中返回一个数组)"(p.129)
C语言函数不可能返回数组。
"……这个函数的入口地址以及需要的参数……。而这些信息都包括在了函数声明中"(p.138)
信口开河。函数声明中根本不可能包含函数的入口地址这样的信息。
"int maxval = max(c,max(a,b));
这就是一个典型的函数嵌套调用"(p.152)
“函数嵌套调用的本质,就是将函数调用表达式当作另一个函数调用的实际参数,就形成了函数的嵌套调用”(p.153)
这是对函数嵌套调用的错误理解。谭浩强在《C程序设计》中所说的“函数嵌套调用”,实际上是指在函数定义中调用函数,而不是用函数调用表达式做实参。伴侣作者自己连《C程序设计》中“函数嵌套调用”的意思都没搞清楚,居然为该书写“伴侣”,诚可笑也。
“虽然函数的嵌套调用可以在一定程度上简化代码,但是却降低了代码的可读性,应当慎用” (p.153)
如前所述,《伴侣》作者自己根本没搞清函数嵌套调用的意思,这段话完全是对读者的误导。
关于外部变量与局部变量
"任何指针变量刚被创建时不会自动成为NULL指针"(p.219)
很明显不知道C语言还有外部变量和static变量。外部指针变量和static指针变量变量被创立后都被初始化为NULL。
“全局变量隶属于整个源文件,在整个源文件内均可见,……局部变量定义于某个由大括号“{}”所形成的局部代码之内,而全局变量是直接定义在源文件中,” (p.163)
实际上不存在“在整个源文件内均可见”的标识符。形参属于局部变量,但并非“定义于某个由大括号“{}”所形成的局部代码之内”;“直接定义在源文件中”这种说法显然不知所云,因为局部变量也不可能定义源文件之外。
“而局部变量是在程序运行到它所在的代码区域(通常是函数)时才被创建,而一旦退出它所在的代码区域即被销毁。” (p.165)
显而易见的错误。static局部变量是程序运行之前创建的,而且它在程序运行期间始终存在。
关于作用域与生存期
"如果我们将指针指向某个局部变量,而在这个变量的作用域之外……如果我们还尝试使用指针访问这个变量,就会产生错误"(p.220)
严重概念混乱。很明显把作用域和生存期这两个概念给弄混淆了。
"不要将指针指向作用域小于自身的变量(或者反过来说,指针所指的变量,其作用域必须大于或等于指针自身的作用域)"(p.221)
一派胡言。事实上,指针的作用域与所指向变量的作用域可能压根就不重合,更谈不上比较两个作用域的大小。
“可以对变量进行访问的代码区域,被称为变量的可见域(能够访问到变量,就像能够见到这个变量一样)或者作用域。”(p.159)
这个错得有点没边了,可见域或作用域和能否访问没有必然关系,因为在作用域之外虽然不能直接访问但可以间接访问。
“如果某个变量在程序的全局范围(在程序的任何地方都可以通过这个变量名直接访问到这个变量)内都可见,则将其称为全局变量” (p.159)
没有什么变量可以“在程序的任何地方都可以”“访问”。关于外部变量的标识符和其他标识符都只能在声明之后在其作用域内使用或曰可见,换言之,具有file scope。“作用域”只是源文件C代码层面上的概念,而“访问”则是程序执行层面上的概念。“作用域”和“访问”是不同范畴下的概念。
“全局变量和局部变量的区别……本质的区别在于它们所处的存储空间不同。一个程序在运行期间所占用的内存区域分为程序区(存放程序代码)和数据区(存放运行过程中所用到的数据),而数据区又根据其中存放数据的创建时机和生命周期等,被划分为静态存储区和动态存储区。” (p.159)
“存储空间”,用词不当,这个词通常的含义是指存储空间的大小。此外,作者于上所述的也根本不是外部变量(全局变量)与局部变量的区别。
“静态存储区中保存的数据在程序一开始即被创建,直到程序运行结束时才被释放,其生命周期是整个程序运行期,并且在此期间,这些数据保存的位置保持固定不变,因而被称为静态存储区,静态数据(用static关键字修饰的变量)、全局变量以及常量(包括字符串常量)就保存在静态存储区。” (p.159)
搞笑的是“这些数据保存的位置保持固定不变,因而被称为静态存储区”,这是一个很滑稽很荒谬的因果关系。因为所有数据对象的位置在其生命期内都保持不变。
“与静态存储区相对应的是动态存储区,其中保存的数据,往往都是在程序的运行过程中被动态创建和释放的,其生命周期通常也只存在局部代码区域,所以称之为动态存储区。根据内存分配方式的不同,这一区域又可以再细分为堆区(heap)和栈区(也称为调用栈,call stack)。” (p.159)
基本是以讹传讹和信口开河的混合物。“其生命周期通常也只存在局部代码区域,所以称之为动态存储区”的说法很搞笑,因为“生命周期”是一个程序运行时范畴下的时间概念,而“代码区域”是一个源代码层面上的空间概念。因而这里的“所以”二字用得极为滑稽。 “也称为调用栈”纯粹是不懂装懂,乱讲一气。
“函数内部的局部变量、函数参数、函数的返回地址等,则被保存在栈区。这些数据随着函数的调用被动态地创建,同时也随着函数的执行完毕而被动态地释放” (p.160)
局部static变量在函数执行完成后并不被释放。
“全局变量是隶属于整个源程序文件的,在整个源文件中可见……所以它被保存在一个全局的存储空间(静态存储区)中。”(p.160)
实际上,除了语句标号,标识符都只在声明之后可见,不可能在整个源文件中可见。
更荒唐的是那个“所以”,完全是牵强附会的胡扯。
“只是在函数被调用时,局部变量才会获得内存分配被创建,而函数执行完毕之后,就会被自动销毁,所占用的内存被回收。从这里看,局部变量是短命的变量,而全局变量则会伴随程序的一生。”(p.160)
完全是不顾事实的张冠李戴。
“静态的函数内部变量会在函数首次执行时创建并保存在静态存储区” (p.161)
一厢情愿的臆测。实际上所有static storage duration specified的数据对象都是在程序运行前就存在。
关于字符串
“引用了一个字符串变量”(p.197)
C语言没有字符串变量。
“gets()……是我们在处理字符串的时候最常用的函数。”(p.111)
这不是事实。更为讽刺的是,现在C语言标准库函数已经没有这个函数了。在此之前很长一个时期,程序员也已经很少用它了。
“puts()函数可以接受一个字符串变量(字符数组或字符串指针)为参数,……而gets()函数同样是接受一个字符串变量为参数,将接受到的字符串保存到这个变量中。”(p.66)
实际上C语言中没有“字符串变量”这种东西。puts()函数的实参远不于“字符数组或字符串指针”,而gets()函数所能接受的参数也绝对不是什么“字符串变量”。很显然。“接受到的字符串”根本不可能保存到“字符串指针”之中。
关于库函数
“qsort()函数……对数组进行快速排序,通过指针移动实现排序”(p.99)
这是毫无依据的臆测。
“qsort()函数不仅可以进行数值数组的快速排序”(p.101)
C语言并没有要求这个函数使用何种算法排序。
声称“C语言标准函数库还提供了两个特殊的字符输入函数——getch()函数和getche()函数”(p.65)
实际上这两个函数根本就不是C语言标准库函数。
“assert()函数”(p.178、179)
实际上assert()根本就不是函数而是一个宏。
"谭老师在这个小节还介绍了动态分配内存的alloc()函数和remalloc()函数"(p.217)
C语言标准中并没有remalloc()函数。该页一口气连续出现了三次“remalloc()函数”,说明这不是普通的笔误,而是作者压根就不知道realloc()这个函数。
"remalloc()函数……它会在新的内存位置重新申请一块给定大小的内存"(p.217)
显然作者非但不知道realloc()函数的名字,对其功能也并不清楚,完全是在信口开河。
"void* array = alloc(1000,sizeof(int));
等价于:
void* array = malloc(1000*sizeof(int));
这两者的功能都是一样的"(p.217)
实际上这两者并不等价。
“C语言函数库还提供了多个实现相似功能的字符串比较函数,例如stricmp()函数……”(p.115)
stricmp()函数不是标准库函数。
“fflush()函数,它会清空键盘输入缓冲区中的内容”(p.64)
误导。这个说法仅在个别编译器下成立。
"在产生随机数之前,我们需要利用当前时间(利用time()函数获得)来初始化随机种子,否则我们用rand()函数得到的是一个伪随机数,导致我们每次运行都会得到相同的数。"(p.67)
完全不懂得rand()函数。
“ //而srand()的
// 意义就在于用一个真实的随机数(也就是所谓的随机种子),通常是当前系统时
// 间,作为初始条件,然后用一定的算法不停迭代产生真实的随机数” (p.67)
完全不懂的srand()的含义,而且“产生真实的随机数”的说法荒诞可笑。
荒唐地声称:
“在产生随机数之前,我们需要利用当前时间(利用time()函数获得)来初始化随机种子,否则我们用rand()函数得到的是一个伪随机数,导致我们每次运行都会得到相同的数。”,“srand()的意义就在于用一个真实的随机数(也就是所谓的随机种子),通常是当前系统时间,作为初始条件,然后用一定的算法不停迭代产生真实的随机数”。(p.69)
实际上无论是否用srand()设置种子数,rand()函数得到的都只能是伪随机数序列,不可能得到“真实的随机数”。
“//rand()函数会产生一个0 到 RAND_MAX
// (不同的编译器对这个值有不同的定义,但是都会大于32767)之间的整数”(p.67)
“都会大于32767”,错。实际上应该是“不小于”,“不小于”与“大于”完全是两回事。
“RAND_MAX定义在stdlib.h中,其值为2147483647”(p.69)
信口开河且与前面的说法相矛盾。
“C语言标准库函数提供了三个与文件读写位置移动相关的函数”(p.266)
实际上不只三个。
“判断某个文件是否存在、获得文件的创建日期和体积大小等等。对于这些常见的文件属性的访问,我们都可以用C语言标准函数库所提供的属性访问函数来完成。”(p.268)
C语言标准函数库压根就没有这些函数。
“C标准函数库专门提供了一个access()函数来完成这一任务。”
子虚乌有的捏造。
关于指针
"指针的本质是一个整数类型(大小为系统虚拟存储器的位宽),这个整型数值……"(p.172)
一句话俩错。
首先,指针本身就是某些数据类型的泛称,这类数据类型和"整数类型"完全不是一回事。
“大小为系统虚拟存储器的位宽”更是胡诌八扯,有些系统根本就没有虚拟存储器。
一句话就露了底。完全不懂指针。
“既然指针是内存单元的编号(内存地址),它自然也就是整型数据” (p.173)
错!指针类型和整型完全不同。
"指针变量的本质就是一个整型数据"(p.185)
参见前条。
“因为指针所保存的内存地址,通常是一个非常繁琐的整型数值(例如,0x0016FA38等),这种数值在程序不宜于使用也不宜于传递,因而我们使用指针变量来保存内存地址的这类整型数值,而通过指针变量,一个具有明确意义的标识符,我们就可以直接访问它所保存的内存地址上的数据,进而还可以对指针进行运算,使指针发生偏移,访问这个指针附近的内存区域。更进一步地,我们还可以在函数间传递指针,达到传递数据的效果,如果指针指向的是某个函数,我们甚至可以通过指针来调用它所指向的函数。” (p.173)
首先是指针类型与整数类型混淆,这对初学者误导极大。
“因为……因而”的因果关系是捏造的,指针类型数据并不都需要用“指针变量来保存”。
“可以直接访问它所保存的内存地址上的数据”荒谬,通过指针访问叫“间接访问”,而且“内存地址上”也不可能保存数据。数据保存在内存单元之中。
“指针发生偏移,访问这个指针附近的内存区域”,“偏移”一词不知所云,“指针附近的内存区域”指称错误。
“在函数间传递指针,达到传递数据的效果”,不知所云,“传递指针”本身就是“传递数据”。
“甚至可以通过指针来调用它所指向的函数”,毫无意义的夸张,任何的函数调用过程都是通过指向函数的指针完成的。
“指针的实质就是某个数据所在的内存地址。”(p.175)
直接把指向函数的指针和void *类型指针忽略了。
“实际上,NULL是一个宏,在C语言中,它的定义是:
#define NULL 0
可以看到,NULL实质上就是整数0。因为0值的特殊性,所以我们常常用NULL值表示一个指针变量是一个空指针,它并没有指向一个正确的内存地址,而是一个无效的不可访问的指针。”(p.178)
完全是信口开河。实际上,在C语言中,NULL实际上是由实现定义的。也根本没有什么后面所谓的“因为0值的特殊性”的因果关系。
“可以通过这个指针直接访问它所指向的数据。”(p.184)
那不叫“直接访问”,叫“间接访问”。
"任何类型的指针变量……其长度都是4个字节"(p.185)
以偏概全。C语言从没规定各种类型指针具有同样长度。
"给指针加上(减去)某个数,也就是让指针向前(向后)偏移相应的数据的个数"(p.185)
忽视指针运算的前提条件。
“ void* result = NULL;
// 将指针偏移size个单位,指向数组中的下一个数据
// 因为这里的array是“void*”类型的指针,我们不能对其进行简单的
// 自增运算使其指向下一个数据,而需要加上具体的数据元素的字节数
array += size; ” (p.205)
根据C标准,void *类型数据没有加法运算。这个错误在书中多次出现(如209页)。更可笑的是在218页,作者自己打脸,又自相矛盾地说“void *类型的指针并不具备字节信息,所以这样的运算(注:指加减运算)对它不成立”。
“不要返回指向局部变量的指针,……,局部变量会在函数返回后被释放内存”(p.210)
没有这事情。可以返回指向局部变量的指针;局部变量在函数返回后也不一定被释放其占用内存。
"普通数据和指针数据都同样占据内存"(p.212)
没有这个事情,有些数据很难说是否占据内存。
"将“*”修饰的数据类型看成是一种新的指针数据类型"(p.217)
本来就是指针数据类型,“看成是”“指针数据类型”是什么话?难道还能看成别的什么不成?
关于main()
"add.exe 4 5 (以Windows平台为例)
……在主函数中,我们可以获得argc等于3,表示这个指向命令有3个命令选项。而argv指针数组中保存的字符串指针,分别指向“add.exe”、“4”、“5”这三个字符串"(p.216)
“指向“add.exe””是想当然,实际不是这样。此外作者显然不清楚什么叫“命令选项”。
关于输入输出
“我们接触到的程序都只是处理用户从屏幕输入的数据”(p.250)
“所以我们才能使用scanf()函数从屏幕获得输入”(p.254)
“就像我们使用scanf()函数和printf()函数来从屏幕读取数据或者输出数据一样”(p.255)
相信每个人都会被“从屏幕读取数据”雷到,所以就不解释了。
关于结构体、共用体、枚举
“结构体只是概念上的一种整合,对于它的访问,还是通过它的各个数据分量来实现的”(p.226)
“对结构体数组的访问,并不是为了访问其中的结构体数据元素本身,更多的,我们是为了访问这些数据元素的各个数据分量”(p.230)
显然不懂得什么叫“访问”。
“结构体变量和指向结构体变量的指针并无什么本质的差别”(p.231)
完全不一样的东西,居然被说成无本质差别。
“对于指向结构体变量的指针,其意义就在于可以在函数间传递结构体变量数据时,见识传递的数据量,自然也就提高了程序的效率”(p.232)
以偏概全。指向结构体变量的指针还用于构造链表、树等数据结构。
“共用体的这种特性(修改一个数据成员,其他数据成员也跟随变化),往往给程序造成一定的混乱,所以共用体在C程序中很少使用”(p.246)
外行话。共用体的成员并不同时存在,谈不上“修改一个数据成员,其他数据成员也跟随变化”,给“给程序造成一定的混乱”的是不会使用共用体的人而非共用体。所以压根谈不上什么“所以”。
“且枚举值可以作为常量使用”(p.248)
废话。枚举值本来就是常量。
关于链表
“动态链表的建立只需遵循以下几个简单步骤就行了:
- 1. 定义指向链表头和尾的指针
……链表总是在末尾位置添加新的结点,所以我们要保存指向链表尾结点的指针。”(p.234)
从来没有“链表总是在末尾位置添加新的结点”这回事情。需要头尾两个指针的数据结构根本就不是链表,而是Queue等数据结构。
关于typedef
“业界的普遍做法是,将复杂的类型名前用“_”区别,而用typedef定义的,真正使用的新类型名保持原样。”(p.249)
首先是不知所云。语文问题。
关于所谓的“业界的普遍做法是”,该书同一段中的两句话刚好可以拿来做回应:“各位,这只是他个人的习惯而已,不要被他忽悠了……他的这种习惯……根本不是业界的普遍做法”。
关于文件
“文本文件的读写……其读写方式也被称为顺序读写”(p.251)
“二进制文件的读写是随机的(……所以针对二进制文件的读写也称为随机读写)”(p.252)
荒腔走板的胡说八道。
“文本文件……如果保存相同的信息,它所需要存储的数据量也比二进制文件更多”(p.252)
毫无依据的信口开河。
“//以“wr”方式打开文件,既可以写入数据,也可以读取数据
FILE* fp = foen(“data.txt”,”rw”);”(p.264)
这是臆想天开,C语言从来就没有“wr”或“rw”这种打开文件的方式。
在书中,feof()函数的使用全部是错误的。更雷人的是:
“如果我们是以二进制方式打开文件的,那么,使用fputc()函数和fputs()函数输出的数据会在文件中被保存成二进制形式。例如:
……
当程序运行完成后,我们可以用一些二进制查看工具打开刚刚创建的data.bin文件,就可以看到刚刚输出的字符数据,都被转换成了对应的二进制形式:
49 20 77 69 6C 6C 20 6C 6F 76 65 20 4A 69 61 77
65 69 20 66 6F 72 65 76 65 72 2C 0A 49 20 70 72
6F 6D 69 73 65 2E
这样,一封情书就编程了谁也看不懂的二进制天书了。……用fgetc()函数或fgets()函数解密这封情书”(p.265~266)
这是连三岁的孩子都唬不住的。因为任何人用“记事本”打开data.bin都可以看到其中的内容,根本不需要“用fgetc()函数或fgets()函数解密”。至于“用一些二进制查看工具打开”更是煞有介事,所有的文件用这些工具打开都是那个样子。
关于对齐
“关于结构体的定义,这里需要特别提到的一点就是结构体的字节补齐问题,就像谭老师在书的脚注中介绍的那样,现代计算机系统对于内存的管理(读写访问),在32位平台上都是以4个字节为一个“字”,然后以“字”为单位进行的。也就是每次读写的都是若干个字的内存,所以为了方便内存的访问,如果结构体中的数据成员的体积不是“字”(4个字节)的倍数,编译器在编译结构体时,会在相应的位置补齐字节,使得结构体的每一个数据成员的位置,都在“字”的倍数位置上,以利于程序对于结构体数据的读写访问。”(p.225)
这个纯粹属于不懂装懂,把谭的错误又重复了一遍。对齐根本就不是针对结构体的,且是由具体实现确定的,根本就不是什么“以“字”为单位进行的”。这段话的错误还有很多,就不赘述了。
不会数数
插图中说C语言“屹立江湖50年”,(p.8)
这个“50”是八进制吗?
p.23 图中文字“定义了三个变量并进行初始化,分别保存三个整数和最大值”(p.23)
那是定义了四个变量。
"在这一小节中,谭老师为我们列举了三个简单的算法并对算法进行了分析"(p.36)
事实上,谭书列举了五个。
“只有在两个条件都同时满足的情况下才与之交往”(p.74)
但在两段实现代码中,却并非是针对“两个条件”,而是一个写了三层if语句的嵌套结构,另一个“用一个逻辑表达式就完成了三个条件的判断”。
p.188页图,10个数组元素被画成了11个
p.189页图,10个数组元素被画成了11个
重复谭浩强《C程序设计》中的错误
“谭老师对这个问题给出的答案是:“人和计算机交流,也需要解决语言问题。需要创造一种计算机和人都能识别的语言,这就是计算机语言。”这段话给计算机语言下了一个定义”。(p.1)
事实上根本就不存在“计算机和人都能识别的语言”。不知道该书“内容提要”所说的“是对原书有益且神奇的补充”从何谈起?
“谭老师将函数分成三种类型(无参函数、有参函数和空函数)……”(p.124)
这种分法很滑稽,就如同把人分成男人、女人和工人。
缺乏计算机基础常识
“第三步:计算机执行可执行程序,最荒唐的一句话是“……这些指令会被调入计算机,虽然我们人类看不懂这些指令,但是计算机却能够看懂,最终将其翻译成“0101”的机器语言”,(p.4)
都开始执行程序了却还要"最终""翻译",这是任何学过《计算机基础》的人都不会犯的错误。
“1.4.A最简单的C语言程序背后的故事——它的汇编代码是如何被执行的”(p.12)
汇编代码是不能执行的。
滥造术语
“无论一个常变量在我们的代码中出现多少次,它都只占一个数据体积”(p.48)
不知道作者能否求出数据的长、宽、高各为多少?
“加和运算”“加和的规则”(p.228)
臆造的术语,恐怕只有他自己才知道是什么意思。
“因为外部全局变量pi(func.c源文件中)所指向的全局变量pi(main.c源文件中)已经定义并初始化,所以这里不需要再次对外部全局变量进行初始化就可以直接使用。” (p.164)
这里的“外部全局变量”是个莫名其妙的“发明”,C语言中没有这种东西。
“因为……所以……”是错误的,不是“不需要”,而是非定义性声明本来就不允许初始化。
信口开河
“C语言在某些需要对硬件进行操作的应用场景下,例如嵌入式系统中,成为程序员们的不二之选。”(p.6)
不顾事实。
“但是,在开发一些更加复杂的业务型系统的时候,因为其抽象层次比较低,这样的设计方法却可能让整个项目陷入“需求变化”的深渊,一旦需求发生变化,则可能需要对整个系统的设计进行变更,这使得C语言无法很好地支持复杂的大型系统的开发,极大地限制了C语言的应用。这也是为什么后来出现了面向对象的设计思想以及C++语言。”(p.7)
面向对象程序设计在1960年就已经出现了,1970年代已经成熟,而C语言出现于1970年代。“后来出现了面向对象的设计思想”完全是想当然。
“C++语言比C语言优秀”(p.8)
无语。
图 “算法与程序设计是好兄弟”(p.17)
程序设计包括算法设计和数据结构设计两部分。部分居然成了总体的兄弟。
“正是GCC编译器应用的广泛性,使其成为C以及C++编译器的事实上的标准”(p.24)
完全不懂得什么叫标准。
“在C语言程序设计当中,“自顶向下,逐步求精”就像一句具有魔力的咒语,只要我们一念这个咒语,任何复杂困难的问题都会迎刃而解”(p.40)
“任何”?能证明哥德巴赫猜想吗?
"一个程序总是要包括输入" (p.45)
错!程序可以没有输入
“任何程序,无论是复杂的还是简单的,从总体上看,都是一个顺序结构的程序” (p.45)
胡扯。
“变量,……它被用来描述我们现实世界中可能会发生变化的数据”(p.46)
现实中是否变化和程序中是否变化是两回事。而且代码中的很多变量,在现实世界中根本就没有对应物。
"随机数在C语言程序中有着广泛的用途。它常常用于模拟、测试、仿真等等,所以生成随机数也是学好C语言的一个必备技能"(p.69)
“随机数在C语言程序中有着广泛的用途”,信口开河。
“所以生成随机数也是学好C语言的一个必备技能”,严重误导,而且作者自己根本就不懂rand()函数。
“C语言中的数据都是有具体类型的,唯独空类型(void)有些例外,它表达了某种抽象的“空”的概念,我们可以把它理解成一个占位符,它只是表示这里有一个数据,而至于具体是什么数据、该如何分配内存等等,它并不关心。” (p.49)
这个属于压根不懂得void的含义,甚至语言上都是病句。例如,“C语言中的数据都是有具体类型的,唯独空类型(void)有些例外”,前后主语都不一致。
“……程序是对现实世界的表达。正是为了表达现实世界中普遍存在的各种循环往复的现象……,C语言才提出了循环结构。”(p.82)
荒谬。程序是对解决问题的方法的描述,不是为了表达现实世界。所谓“正是为了表达现实世界中普遍存在的各种循环往复的现象……,C语言才提出了循环结构。”是捏造。
“由于C语言应用的广泛性,C语言程序的输入输出远不止键盘和显示器。”(p.61)
这逻辑!
“……例如圆周率等),我们应该使用全局变量” (p.165~166)
圆周率是常量,小学生都懂。
“毫无疑问,性能就是C语言的生命。”(p.156)
无稽之谈。任何语言都不可能片面强调性能,更不可能强调到“生命”这种程度。
“C语言中有了对于一维数组的定义和引用,必然也会有对二维数组的定义和引用。”(p.101)
逻辑很荒谬。
“通过对比我们就可以发现,指针的使用,可以使得我们的代码更简洁。同时,指针代表了数据所知的内存地址,通过它来访问数据也更加高效。”(p.175)
没有这回事。
“数组的下标计算是一件比较繁琐的事情……必须防止下标出现非法的值……指针的使用……简化了下标的繁琐运算……就可以很好地防范访问越界的错误。……整个过程没有下标的繁琐运算,也不会发生越界访问的错误,简洁而安全。”(p.175)
天方夜谭。
以上列举的事实,涉及到C语言基础知识的几乎每个方面。这说明作者在C语言基础知识整体上非常薄弱,基本属于门外汉的水平。这本书绝非如陈冰在“编辑的话”中所说的那样,“对概念、知识和疑难点讲解地非常透彻”,而是恰恰相反,在基本概念和知识方面错误连篇,就其严重程度很多已经到了荒腔走板的程度。
除此之外,作者的编程能力同样没有达到一个初级程序员的水准。书中代码多数完全经不起仔细推敲,有许多甚至不用推敲就知道是错误的。
代码错误
《伴侣》一书中代码的错误更是不可胜数。限于篇幅,各章的错误代码和垃圾风格最多各举两例:
第1章:
第3页的代码,这是书中第一段代码。单从
#include "stdio,h"
printf("Hello World");
这两行,任何有经验的C程序员一眼就能看出作者的技术成色。
臆造了一个所谓的“月薪10000元的程序员的代码”(p.20),因其毛病多多,多数程序员对其嗤之以鼻。实际上这段代码连业余水平都达不到。
char str[100] = "";
这个初始化是毫无意义的
printf("please input a string:\n");
位置不当,因而是一个逻辑错误。这条语句应该写在循环体内而不是循环体外。换句话说,它并没有起到完整地“提示用户输入”作用
do { } while(1);
结构丑陋,本质上这里应该是一个while语句而不是do-while语句。写成do-while语句必然得到一个很丑陋的结构。
循环的结束条件也设计得丑陋不堪。
最严重的问题是滥用gets(),存在安全隐患。
作者在该书最后“最后一个小插曲”中对此曾辩解说:
“在本书前面的1.4B节中,我曾经给过一段月薪100元和月薪10000元的程序员的代码对比。事实上,在真正的开发中,月薪10000元的程序员的代码更可能是下面这样的。我之所以没把这个版本在前面给出,是因为我不希望让太多的细节干扰了我真正想向你传达的思想”
但事实上该书最后的这个代码连编译都无法通过。
第2章:
第29页的代码:
// 找出一组学生成绩当中最高的成绩
#include <stdio.h>
// 定义学生总数,这里的const表示COUNT是一个常量,不可以修改
const int COUNT = 28;
int main()
{
// 使用数组来保存所有学生成绩
int scores[COUNT];
// 输入学生成绩并保存到数组中
printf("p.lease input the scores.");
int score = 0;
for(int i = 0;i < COUNT; ++i)
{
scanf("%d",&score);
scores[i] = score;
}
// 对数组中的学生成绩进行处理,获得最高成绩
int max = scores[0];
for(int i = 1;i < COUNT; ++i)
{
if(scores[i] > max)
max = scores[i];
}
// 输出最高成绩值
printf("the hightest score is %d.",max);
return 0;
}
滥用外部变量,代码结构垃圾。滥用VLA。最可笑的就是
scanf("%d",&score);
scores[i] = score;
第30、31页的代码:改编自《编程珠玑》的中的伪代码,但由于《伴侣》在介绍问题时忽略了一个重要前提条件,问题本身就不完整明确,因而代码完全失据,全是错误的。在数组元素全部为负整数时,将产生严重错误。
第3章:
第49页的代码:
定义了一个enum类型,却自相矛盾地将today定义为int类型,进一步用枚举值与today进行比较。但这个不算本章最垃圾,本章最垃圾的代码应该是63页的
char name[20] = "";
scanf("I love %s",name);
这里要求输入的“I love”几个字符完全是无事生非地给用户下绊子。
第61页的代码:
float score[6] = {610.103,19.8,3.07703,2,4,40};
for(int i = 0; i< 2 ;++i)
{
for(int j = 0; j< 3;++j)
{
float cur=score[i*2 + j];
printf("%6.1f\t",cur);
}
printf("\n");
}
错误地把i*3 + j 写成了i*2 + j 。再次说明作者不会数数。
第4章:
第76页的代码:
void wear(int wea)
{
char strwea[10] = "";
char strcloth[20] = "";
// 使用switch结构进行多支选择
switch(wea)
{
case sunny: // 如果是晴天
{
strcpy(strwea,"sunny");
strcpy(strcloth,"T-Shirt");
}
break;
case cloudy: // 如果是阴天
{
strcpy(strwea,"cloudy");
strcpy(strcloth,"shirt");
}
break;
case rainy: // 如果下雨
{
strcpy(strwea,"rainy");
strcpy(strcloth,"overcoat");
}
break;
}
// 输出结果
printf("Today is %s, I will wear %s.\n",strwea,strcloth);
}
定义strwea和strcloth并两次调用strcpy(),要多无聊有多无聊,毫无意义。
第79页的代码:
作者自己也承认该代码“有缺陷”。这个代码根本就不满足功能要求。
第5章:
第92页的代码:
int main
{
/*……*/
}
根本无法通过编译。作者曾在网上豪气冲天地质问过一个读者:“你能在最终出版的书中找出一个编译错误?”后来这段话被作者自己给编辑掉了。
93页代码:
#include <stdio.h>
#include <string.h>
int main()
{
// 定义字符数组的长度
const int len = 128;
// 定义字符数组并为其赋值
char str[len];
strcpy(str,"I really love jiawei");
// 空格字符数
int num = 0;
// 循环遍历整个字符数组,统计空格字符数
for(int i = 0; i < len; ++i)
{
// 判断是否已经到达字符串末尾,
// 如果是,则结束整个循环
if('\0' == str[i])
break;
// 判断当前字符是否是空格,如果是,则统计在内
if(' ' == str[i])
++num;
}
// 输出字符串中的单词数,也就是空格数加1
printf("the num of word is: %d",num+1);
return 0;
}
这段代码作茧自缚地滥用VLA,无法正常初始化,不得不通过strcpy()进行“初始化”。
for语句使用笨拙。违反常规。
程序功能错误:程序的功能只是求不含连续空格字符串中空格的个数,绝非作者所说的“求单词的个数”。事实上,这个程序对于"ABC"和"ABC "两个字符串求得的结果是不同的。可见,这个程序根本不可能正确地“统计这个字符串中的单词数”。
显而易见的是作者没读过K&R,却居然在书中向读者介绍、推荐它。
第6章:
第104~108页代码:
“如果输入数据是0,则表示这个班级的输入结束”,这其实是一个很拙劣的假设,因为根据常识分数是有可能为0的。除此之外,这还会带来其他问题,导致代码错误。
<stdlib.h>被写成了<stdLib.h>,这在某些环境下会造成错误。
滥用VLA。
用memset()函数初始化,驴唇不对马嘴
getaver()函数是错误的,在某一维数组中没有0值元素的情况下,程序运行结果是一场灾难。
cmp()函数中的 fabs(avera - averb) < 0.0001 煞有介事。
main()中再次计算平均分,数据结构设计失败
if(0 == scores[i][j])
break;
不但笨拙,而且是错误的。
第116页代码:
认为“截取字符串”也是“常见的字符串处理任务”是武断的。C语言函数库中并不需要这样的函数,因为这种操作并非是对字符串的一个基本操作。
#include <stdio.h>
#include <string.h>
void strsub(char* source,char* dest,int pos, int n )
{
// 对参数合法性进行检查…
// 开始截取字符串,将截取范围内的字符从源字符串数组
// 逐个复制到目标字符串数组
for(int i = 0;i < n;++i)
{
// 将源字符串数组中的第pos+i个字符
// 复制为目标字符串数组的第i个字符
dest[i] = source[pos + i];
}
// 在目标字符串末尾加上字符串结束符,表示目标字符串完成
dest[n] = '\0';
}
int main()
{
// 源字符串
char* source = "We think in generalities, but we live in details.";
// 表示截取开始位置的字符串
char* begin = "live";
// 计算截取的起始位置和截取长度
char* ppos = strstr(source,begin);
int pos = strlen(source) - strlen(ppos); // 计算begin字符串所在的位置
int n = strlen(ppos); // 计算从begin字符串到字符串末尾的长度
// 保存目标字符串的字符数组
char substr[256];
// 截取字符串
strsub( source, // 源字符串
substr, // 目标字符串
pos, // 截取的起始位置
n); // 截取长度
// 输出截取获得的字符串
puts(substr);
return 0;
}
strsub()函数参数臃余,参数次序违背业界习惯,函数逻辑存在漏洞,存在copy多余字符的低效行为以及越界访问的错误,在某些情况下这是未定义行为。
strsub()函数的功能,其实简单地用一条函数调用
strncpy(substr,source+pos,n);
就完全能够搞定,而且后者不存在《伴侣》代码中那样的错误。
第7章:
第155~156页代码:
int strcount(char* source, char* target)
{
// 参数合法性检查...
int count = 0;
// 构造一个无限循环,查找源字符串中的目标子字符串
while(true)
{
// 查找目标子字符串并记录其位置
char* pos = strstr(source,target);
// 如果找到目标子字符串
if(NULL != pos)
{
// 将出现的次数加1,表示本次循环找到一个目标子字符串
++count;
// 将查找的开始位置移动到目标子字符串之后,开始第二次循环
// 这里也就相当于函数递归调用中的递归
source = pos + strlen(target);
}
else// 如果没有找到
// 用break关键字结束循环
// 这里也就相当于满足了递归调用的终止条件,结束递归
break;
}
// 返回结果数据
return count;
}
结构蹩脚,代码啰嗦,重复低效。
其实可以简单地写为:
int strcount( char * source , char * target )
{
int count = 0;
const size_t len = strlen(target);
char * pos ;
while( (pos = strstr(source,target)) != NULL )
{
++count;
source = pos + len;
}
return count;
}
第157~158页代码:
这个代码的毛病太多了。
1.short 这个数据类型是错的。应该用1Byte的unsigned char类型。
2.invert()返回值类型错误
3.invert()根本就不应该有返回值,函数原型应该是void invert(short *pix);
4.循环语句逐次调用函数本身低效,应该写的对整个数组操作的函数
5.img[i][j] = invert(img[i][j]);这句煞有介事,这句的功能无非就是img[i][j] = 255 - img[i][j] ; 而已。根本用不着函数。
这里作者首先写了一个低效的垃圾代码,然后抱怨“invert()函数会被反复调用”,“每次调用……还要进行函数调用时的现场环境的保护和恢复、处理函数参数的入栈出栈等等,因而它成为了整个程序的性能瓶颈”。
于是作者决定“嵌入汇编代码”,“直接操作硬件完成计算处理”“从而让整个函数飞起来”
这个想法本身就站不住脚,因为前面C代码的低效并不是C语言造成的,而是写代码的人自己造成的。性能瓶颈是自己造成的,但偏要赖到C语言上面。
更荒谬的是,嵌入汇编并没有在实质上改变什么,根本没有针对“性能瓶颈”进行改善。内嵌的那段汇编代码和编译器编译生成汇编代码没啥大的区别,但《伴侣》书中却大言不惭地声称提高了效率,硬说sub指令比“-“运算符快,由此可以看出作者并不知道C语言编译生成的二进制文件里是啥。
最搞笑的是汇编版的函数参数的数据类型还是错的。因此,作者所谓“性能上所带来的巨大提升”,纯粹是闭着眼睛瞎吹。代码都写错了,还谈什么性能改善呢?
第8章:
第191~194页代码:
#include <stdio.h>
#include <stdbool.h>
// 定义全局的学生数和班级数
const int stcount = 30;
const int clcount = 5;
// 统计某个班级的平均成绩
// 其参数是保存成绩的一维数组的首地址和元素个数
double getaver(int* scores, int count)
{
// 参数检查
if(0 == count)
return 0.0f;
int total = 0;
for(int i = 0; i < count; ++i)
{
// 通过下标形式访问数组中的元素
total += scores[i];
}
return total/(double)count;
}
// 输出显示某一个班级的所有成绩和平均成绩
// 参数是保存班级成绩的一维数组的首地址和元素个数
void display(int* score, int count)
{
// 首先计算平均成绩
double aver = getaver(score,count);
// 输出所有成绩
puts("the scores are:");
for(int i = 1; i <= count; ++i)
{
// 每五个数据输出一个换行符,即五个数据一行
if(i%5 == 0)
printf("\n");
// 通过指针访问了数组中的数据元素
printf("%5d",(*score));
++score;
}
// 输出平均成绩
printf("\nthe average score is %.2f\n",aver);
}
// 显示输出所有成绩
void displayall(int (*scores)[stcount], int row, int col)
{
puts("all the scores are:");
int* p = (int*)scores; // 将二维数组指针转换为一维数组指针
for(int i = 0; i < row; ++i)
{
for(int j = 0; j<col;++j)
{
// 通过数组首地址,计算当前应该输出的数据
printf("%5d",*(p + i * col + j));
}
printf("\n");
}
}
// 输入每个班级的成绩
void input(int (*scores)[stcount], int count)
{
puts("please input scores:");
int* p = (int*)scores; // 类型转换
for(int i = 0;i<count;++i)
{
// 通过指针访问数据元素
scanf("%d",p);
++p;
}
}
int main()
{
// 定义数组
int scores[clcount][stcount];
// 定义一个可以指向含有stcount个数据元素的一维数组的指针,
// 也就是它可以指向scores中的各个一维数组
int (*pclass)[stcount];
// 让pclass指向scores中的第一个一维数组
pclass = scores;
// 逐个遍历scores中的clcount个一维数组,实现数据输入
for(int i = 0; i < clcount;++i)
{
printf("class %d:\n",i+1);
// 输入数据到pclass所指向的一维数组
input(pclass,stcount);
// 指针的自增运算,也就是让pclass指向scores中的第二个一维数组
++pclass;
}
// 输出所有数据
displayall(scores,clcount,stcount);
// 查询
while(true)
{
int i = 0;
// 输入查询的班级号
printf("which class's scores do you want to view?(1 - %d,0 for exit.)\n",clcount);
scanf("%d",&i);
// 对输入进行检查,根据不同的输入采取不同的处理
if(i>clcount) // 输入不合法
{
printf("the class %d does not exist. please try again.\n",i);
continue;
}
else if(i<=0) // 退出查询
{
puts("Thansks for your using. Byebye.");
break;
}
// 显示用户所查询的班级的成绩
display(scores[i-1],stcount);
}
return 0;
}
这个代码首先定义了两个外部变量,破坏了代码的总体结构。这样做的原因是因为作者根本不懂如何使用VM (variably modified) 数据类型。他把const变量当成常量,在main()中定义了一个二维VLA。但在写displayall()函数时发现无法编译,于是不得不用破坏程序结构的代价蹩脚地把 stcount 和clcount 定义为外部变量。也就是说,他根本不懂得如何向一个函数传递VLA。displayall()的第一个形参已经包含了一维数组尺寸的信息,但他却不得不再额外加一个row形参,造成参数臃余。
尽管如此,这个代码依然是错误的。函数中的printf("%5d",*(p + i * col + j));实际上是没有确切语义的未定义行为。也就是说,即使这种代码通过编译且“正确”执行,最多也只不过是瞎猫碰上死耗子而已。
第214页代码:
// 通过指针运算,使指针偏移到用户选择的字符串
char** pchose = menu + index;
// 通过指向指针的指针pchose,访问它所指向的字符串指针(*pchose)
printf("you have chosed %s.\n",*pchose);
明显的矫揉造作。其实无非
printf("you have chosed %s.\n", menu [index]);
而已。根本就不需要画蛇添足地定义那个pchose。
第9章:
第229页代码:
int main()
{
// 定义结构体数组
const int count = 5;
student stu[] = {{10101, "ZhangLei",78}, {10103,"WangMeimei",98.5} , { 10106 ,"LiBo" , 86 } , {10108,"LingCong",73.5},{10110,"Jiawei",100}};
// 调用qsort()函数,利用cmpscore函数定义的规则对数组进行排序
qsort(stu,count,sizeof(student),cmpscore);
// 输出排序结果
for(int i = 0; i < count;++i)
printf("%6d %8s %6.2f\n",stu[i].num,stu[i].name,stu[i].score);
return 0;
}
这是一种“假肢体”代码。qsort()的第二个参数本应该是stu数组的尺寸——sizeof stu/ sizeof *stu,但却用与数组风马牛不相及的count来冒充,逻辑错乱。如同给四肢健全的狗装假肢。
第229页代码——9.4.B 链表数据的处理:查找结点
《伴侣》批评顺序查找低效,于是采用“更加有效的算法——折半查找算法”。
如果说该书有“原创”,那么这个代码显然是太“原创”了。前不见古人、后不见来者,计算机问世几十年间从来没有人这样干过——将折半查找用于链表。
“神码”甫一披露,CU和博客园的众多程序员一片哗然——原来还可以这样干!因为在此之前,他们从来没见过有人用菜刀修脚,打破脑袋他们也想不到这种主意。
第10章:
第258~263页代码:
声称要读写图片,但本质上是伪图片——字符数组。代码中的这几行比较雷人:
FILE* data;
data = fopen(name,"wb");
// 首先,用fprintf()函数写入图像的宽和高信息
fprintf(data,"%d %d",img->w,img->h);
// 将结构体中的bits指针所指向的图像数据当作一个整体,
// 一次性地写入文件,这里,“sizeof(int)”表示每一个图
// 像数据(每一个像素)是int类型的数据,而这些数据
// 一共有宽×高个,也就是“img->w*img->h”个。
fwrite(img->bits,sizeof(int),img->w*img->h,data);
用"wb"方式打开文件,却用fprintf()函数写文件,然后一转眼又用fwrite()写文件。这种“杂交”操作文件的方式,有C以来,没人敢用。因为这样写出的文件根本无法再读取。
第257页代码:
这个代码最有喜感,兹录如下:
“如果我们是以二进制方式(在打开方式后添加字符b)打开文件的,那么,使用fputc()函数和fputs()函数输出的数据会在文件中被保存成二进制形式。例如:
//以二进制方式打开一个只写的文件data.bin
FILE* data;
data = fopen("data.bin","wb");
//写入一个字符串
char* msg = "I love Jiawei.";
fputs(msg,data);
//输入一个换行符
fputc('\n',data);
//直接输出一个字符串
fputs("I promise.",data);
//关闭文件
fclose(data);
当程序运行完成后,我们可以用一些二进制文件查看工具打开刚刚创建的data.bin文件,就可以看到刚刚输出的字符数据,都被转换成了对应的二进制形式:
49 20 77 69 6C 6C 20 6C 6F 76 65 20 4A 69 61 77
65 69 20 66 6F 72 65 76 65 72 2C 0A 49 20 70 72
6F 6D 69 73 65 2E
这样,一封情书就变成了谁也看不懂的二进制天书了。”
该书策划陈冰说“这本书比《C程序设计》更加的有趣”,这话一直令人无法相信。但看到了这段“神码”之后,信了!
第A章:
第A章代码是一个综合性例子——集各种错误之大成,详见博客园系列博客
垃圾“程序是怎样炼成的”——关于《C程序设计伴侣》第A章(一)
http://www.cnblogs.com/pmer/archive/2012/12/10/2812205.html
垃圾“程序是怎样炼成的”——关于《C程序设计伴侣》第A章(二)
http://www.cnblogs.com/pmer/archive/2012/12/12/2815300.html
垃圾“程序是怎样练成的”——关于《C程序设计伴侣》第A章(三)
http://www.cnblogs.com/pmer/archive/2012/12/13/2817180.html
垃圾“程序是怎样练成的”——关于《C程序设计伴侣》第A章(四)
http://www.cnblogs.com/pmer/archive/2012/12/15/2819274.html
垃圾“程序是怎样练成的”——关于《C程序设计伴侣》第A章(五)
http://www.cnblogs.com/pmer/archive/2012/12/18/2823626.html
垃圾“程序是怎样练成的”——关于《C程序设计伴侣》第A章(六)
http://www.cnblogs.com/pmer/archive/2012/12/20/2825733.html
第B章:
谢天谢地!这章没代码
最后一个小插曲
const int LEN = 100;
char str[LEN] = "" ;
这里的str根本不可能初始化。无法通过编译
if( NULL != fp)
{
fclose(fp);
fp = NULL;
}
画蛇添足。程序能执行到这里,fp不可能为NULL,否则早就退出了。
除此之外,全书代码明显存在“膨化”问题,即加入大量既不必要也不美观的注释,有注水占篇幅之嫌。
由此,我们不难看出,该书作者基本不具备使用C语言编程的能力,或者更严格地说不具备编写基本正确的C代码的能力。该书绝非像其“内容提要”中所说的那样:“把……作为初学者或称为为了程序员绝对应该知道的基础知识、编程素养、编程思想,以及业内行规一并讲清楚了”,更不是该书策划编辑陈冰在“编辑的话”中所说那样,“可以让你养成良好且符合业界标准的编程习惯和编程思想,为今后的继续前行打下必要而优秀的基础”。
需要说明的是,以上所列举的远非该书错误的全部,而仅仅是其中的一小部分,只是初级程序员都能判断出的一些常识性错误而已。但从中已经不难看出该作者的写作态度、学术品质以及在C语言和程序设计方面的技术水平了。
很明显,作者的写作态度并不端正,学术品质也大有可疑之处,更为重要的是其技术水平远未达到为初学者写C语言参考书的程度。《伴侣》只是一个对C语言一知半解、对编程实践半生不熟者的涂鸦之作而已。
诚然,我们都知道,任何一本书都不可能“十全十美”。但是《伴侣》一书的错谬百出、误导连篇而且已经到了比比皆是、令人作呕的程度,绝对不是一句轻描淡写的“错误仍不可避免”(该书策划陈冰语)所能解释和开脱的。
可笑的是,在该书的封底竟然赫然印着这样一段文字:
“武总,我服了你了,这本书不算我改的两遍,光你就又改了两遍了。还没改够?”“认真一点没错地。”“好吧,不过,虽说你已经改的错都对,但我还是希望你接下来用铅笔来改,这样万一你改错了,我还能擦掉。”“行~。”
这样一本错谬百出的劣书图灵总经理和副总编竟然分别加工修改过两次,实在让人感到不可思议。
策划和副总编的问题
这样一本劣书能够得以出版,显然说明该书的该书策划编辑要么失职要么根本就不称职。因为选择作者、取舍稿件是策划编辑的主要工作内容,策划编辑应该对图书的内容和质量,以及社会效益和经济效益负责。出版这样一本由不合格作者所写的误人子弟的劣书,显然是对社会效益极不负责。然而,令人无法想象得到的是,除了失职,该书的策划编辑,图灵公司副主编陈冰甚至还有更为出格出格的表现。
实际上,作者能力严重不足以及《伴侣》中存在很多错误的问题,并不是在该书出版之后才被发现的。早在2012年4月,《伴侣》一书作者到CU论坛宣传自己的《我的第一本C++书》(策划 陈冰)时,很多网友就发现该在《我的第一本C++书》 “只是迎合了浮躁”,“低级硬伤频出,误导嫌疑严重”,作者“责任心薄弱”,“视野”“狭窄”,而且对待网友指出的书中的大量错谬“死不承认,不见棺材不落泪”。
此外,从CU论坛的《垃圾代码分析——C语言二维数组范例》(http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=3705075&fromuid=22996974)中不难发现,作者直到2012年4、5月间还不懂得C编程,不但对自己代码中的大量错误懵然无知,甚至网友指出之后依然对自己的很多错误无法理解。
因此,2012年8月,在《伴侣》刚刚开始宣传、其样张刚一披露之后,各界读者便纷纷来到图灵社区,其中很多网友甚至是专为此事特意在图灵社区注册,据实指出作者根本不懂C语言和程序设计,而且样张中存在很多严重的原则性、概念性错误,其中的某些代码甚至无法通过编译。
图灵社区“魔力猫”网友就是在CSDN看到《伴侣》部分章节之后特意来到图灵社区并专门撰文——《因<C程序设计伴侣>的争执,谈谭浩强<C程序设计>的批评》,好意提醒图灵公司“这回弄不好图灵要出本糟书”。
至此,该书作者能力不足问题以及该书的质量问题已经昭然若揭,图灵公司对此也心知肚明。然而读者的这些意见非但没有得到恰当的回应和认真的对待。相反,很多提出意见看法的读者在图灵社区反而被删帖、封号。图灵公司的这种行为,无异于讳疾忌医、掩耳盗铃,是对图书质量和读者不负责任的表现。更为恶劣的是,该书策划编辑、图灵公司副总编陈冰对此恼羞成怒、气急败坏,抛出了“陈三篇”,刻意回避、掩饰作者能力不足和样张存在严重质量问题,进行强词夺理的狡辩:
我是《C程序设计伴侣》的策划编辑,有话在这里说(一)
陈冰http://www.ituring.com.cn/article/9141
《伴侣》这本书我最初是在2010年初的时候有构想的,当时我原本想请左飞来写,但他因工作太忙脱不开身,而陈良乔当时正在写我策划的另外一本书《我的第一本C++》(这里我要说明一下,这本C++的书我最初策划的时候是39章,是一本超过850页的大部头,而后来实际出版时只有13章,个中缘由与本文无关,不扯远了。)在没有其他更合适的作者的情况下,这个选题就搁置下来了,直到2011年初陈良乔写完那本C++书后,我跟他详谈了这本伴侣的构思,他开始创作。(关于这本书的策划初衷,可以看我为本书所写的“编辑的话”。)
因为身体原因,陈良乔2011年辞去了工作,全身心投入这本书的写作,因此才得以在不到一年的时间内完成这本书的写作。实际交稿是在2011年12月(其后进行过多次修改)。
社区里这个叫薛非的人在没有任何证据全凭一股戾气直冲百会的情况下,口口声声说陈良乔是抄他的,只能说明此人的自我虚荣心和自卑心已经膨胀到极点。在这个薛非在图灵社区上不请自来的以提交勘误做药引子积极活跃之前,我根本就不知道有他这么一号人,其人品如何,我此前不知道,但现在倒是有些体会了,而陈良乔的品性我则有更多了解。到底抄没抄,伴侣这本书即将上市,到时一目了然。
陈良乔的C/C++技术很好(足可以驾驭这本书),但还称不上是无可挑剔,这点我和陈良乔都很清楚,因为,事实上,在写作和编加过程中,他经常电话打给我说“陈老师,有几处代码还要改一下,我已经给你发了邮件,请你看一下。”而且,这本书直至最终出版时,我也仍认为它距离我希望的状态还有差距(我在书的封底上就写明了这一点),但这并不影响我认为这是一本很有价值的书,尤其对目标读者群是有很大帮助的。
有些人错误地以为这本伴侣是对谭老师那本书的评注,那我可以明确地告诉你,你完全的彻底的没有任何想象余地的错了。我举个例子,牛奶是咖啡的伴侣,但你能说牛奶是咖啡的评注么。它们唯一的共同之处是它们都是饮料,可以饮用,而当它们搭档着饮用时,口感更好。这就是伴侣的作用。
这里,陈冰把选择该作者说成是一种偶然和无奈的结果,实际上恰恰不打自招地坦白了他在选择作者时并没有认真考察作者的技术水平,是在推卸策划编辑选择作者失误的责任。
陈冰无意地透露了这本书早已完成,但拖了很久才得以出版。据我们所知,其实是人邮社内部对此书及其作者早有异议,他们对此书是什么货色心知肚明,并进行了力所能及的抵制。
他在这里刻意地编造了“口口声声说陈良乔是抄他的”这个谣言,然后装腔作势地进行反驳,是在转移视线回避作者水平根本不足以写C语言书的问题。至于“到底抄没抄”,现在倒是确实已经 “一目了然”了。
至于“陈良乔的C/C++技术很好”,“认为这是一本很有价值的书”,则完全是毫无根据的自说自话。前面所列举的《伴侣》中的大量错误,充分说明陈冰的“技术很好”“很有价值”之说完全是在信口开河。
所谓“只能说明此人的自我虚荣心和自卑心已经膨胀到极点”,分明是理屈词穷后的人身攻击,是通过攻击读者来回避自己的策划失误。堂堂副主编失格竟至于斯,实在让人齿寒。
最后一段不知所云,因为《伴侣》一书的问题并非“是对谭老师那本书的评注”,而是作者能力不足和书中错谬百出的问题。
我是《C程序设计伴侣》的策划编辑,有话在这里说(二)
陈冰http://www.ituring.com.cn/article/9175
还有人说,谭浩强的书很垃圾,垃圾的伴侣,也只能是垃圾。我要说的是,说这种话的人不仅无知,而且无耻。要知道,不仅很可能你就是学这本书才第一次接触了C语言,而且就连你的老师都很可能是通过学习这本书才第一次知道了有计算机语言这回事。
谭浩强在那个时代能写出这样一本书让大家来阅读,实属不易,作为第一本书,无论它有多少不足,都不能抹杀其正面的力量。试问,如果那个时候没有这么本书来读,学校连这门课都没得开,广大的爱好计算机编程的学生如何来开始他们的编程之旅。要知道,要开始编程之旅,你首先得知道有编程这回事。
我上学的时候学的就是这本书,我当时认为这本书写的不好,很难看懂,虽然我最后正是通过阅读这本书通过的高级程序员考试(那还是98年的时候了),但直到现在,我依然认为这不是一本容易看懂的书,这也正是我策划这本伴侣的缘由。但难以看懂,绝不意味着它就是垃圾。因为至少有一点,如果没有这本书,我不会知道我喜欢计算机。
但这本书确实存在问题,至少这本书让我读起来感觉艰涩,我希望它能变得更容易理解一些,而且我也希望一些我在阅读这本书时所产生的疑问能够得到解释,一些背后的东西。
但这本书的问题绝不像个别人认为的那么多,有一些表面上不严谨的地方并不能简单的理解为错误。谭浩强有些东西没有写,有些东西简化了,有些地方不符合业界规范,我想这未必就表示他就不知道这些地方该怎么写,就算他最初不知道,20年了,无数次的勘误也早会令他知道,我想他对这些地方维持简化或者看似不符合规范的做法更多的是不想让过多的技术细节干扰了刚上大学第一次接触编程的新生们的头脑。要知道,这本书是给高三毕业,刚上大一的新生们看的,让这些崭新崭新的编程新手们纠缠于细节,就好比要求去麦当劳肯德基吃饭还必须穿西装打领带一样扯淡。
晚饭时间,作为码农,你高高兴兴地穿着T恤短裤去麦当劳吃饭,刚一进门,旁边就有个狂躁的家伙跳出来说:“哈,可让我逮到你了,晚餐是正餐,你居然在吃正餐的时候穿短裤,你错了!你错大发啦!你完了!乡巴佬!不懂用餐礼仪的乡巴佬!”你能说什么呢,我想你大概也只能老老实实的给他5毛钱,然后继续穿着这套不合时宜的用餐套装去点餐付款吃你的汉堡包。
事实上,谭浩强的这本书的主要问题在于把一些简单的问题给搞复杂了,在一些如果采用业界规范会让事情更容易理解更清晰思路的地方用过于简化的方式来处理反而让事情看不清了,从而给学生造成了更多的误会和产生理解困难。还有一些地方,因为没有展示背后隐藏的东西,也会让学生因为必然会产生的疑问得不到解答而使理解变得艰难。
我策划这本书的目的就是为了解决上面的这些不足。让这本书的目标读者在搭配着这本书来读谭浩强的那本书时,生活变得更容易一点。
最后,我还有一点想说的是,任何人写任何书都不可能没有错,如果你抱着从别人的书里挑错的心态去读书,那你从任何书里吸收到的都只能是错误的东西,因为你眼光盯住的正是这些地方。
归根到底,一个人写的书,是否值得一读,起决定因素的首先是这个人写书的态度,而不是他水平的高低。
这篇声明充满着显见的谎言、滥用的比喻和荒唐的狡辩。开头第一句就是捏造,因为没人说“谭浩强的书很垃圾,垃圾的伴侣,也只能是垃圾”。
所谓“第一本书”云云,并非事实。如果这不是由于陈副总编无知,那就只能是为了掩饰自己策划立意的荒谬而故意捏造的谎言(参见博客园博客《没有谭浩强的书我们就不知道编程?——驳图灵公司副总编陈冰无知无耻的“无知无耻论”》、《【转】【随笔(或曰“扯文”)】记念我22年前的广播室Operator生涯——陈副总编的“死穴”》、《从“站在巨人的肩上”到“跪到侏儒之脚下”——图灵公司副主编自供(二)》)。陈副总编还假充内行地刻意否认谭书中存在大量和严重错误的事实,以粉饰自己策划方案的失误。
但这依然无法解释他为什么找了一个C语言文盲来写这本书的问题,于是他只好用“任何人写任何书都不可能没有错”来为一个外行写的书的错漏百出进行胡搅蛮缠式的狡辩,并无理地要求读者对《伴侣》中的大量错误视而不见,甚至荒腔走板地胡说什么“一个人写的书,是否值得一读,起决定因素的首先是这个人写书的态度,而不是他水平的高低”。
这种要求无异于“三鹿奶粉”要求消费者对奶粉中的三聚氰胺装做不知道,要求消费者不考察厂家资质和产品质量而只看产品生产者的工作态度,是极其荒唐无理且无耻的要求。陈冰作为副总编,他的这些言论已经荒谬到了完全丧失了职业良心和职业道德、全面突破了职业底线的程度。
事实上,初学者并不具备辨别劣书中各种谬误的能力,要他们无视《伴侣》中的大量错误就如同让婴儿自己去辨别毒奶粉一样荒谬。
致个别“提勘误”走火入魔的人。我所指明确,你可对号入座!
陈冰http://www.ituring.com.cn/article/9258
自古以来,就是时势造英雄!但这个英雄首先也得具备能成为英雄的素质和眼光,这样才不会被时势错过。
总说什么谭浩强能取得那样的成绩,是因为他生了在那个时代,我不禁想轻笑两声,就算把这里的那位最能挑错的所谓的牛人扔到那个时代,也是个废物点心。只会给别人的代码纠错的人,说到底就是个机械的编译器,除此以外你什么都不是。如果谭20年前没写出这本书,20年后,你甚至连该写本什么书都没主意。你空空如也的大脑,除了机械地反复咀嚼味同嚼蜡的C标准外,完全没有自己的思想,中国式教育的可悲和失败在你身上体现的淋漓尽致!
你到现在还根本没明白什么才是一本书该写的内容!你所写的那些东西,需要人来写么,随便翻本C标准就清清楚楚!任何一个C编译器都能比你干得更好!
你是勉强合格的编译器,却是连一个产品也写不出的废物程序员!
你把你作为人的生命以一个机器的身份给耗尽了!
A:黄皮的普通鸡蛋最便宜,我想多买点。
某牛人:黄皮鸡蛋胆固醇高得很,你有没有点常识?!
A:那就买点白皮的鸡蛋吧,虽然贵点。
某牛人:白皮的鸡蛋你也敢吃?!你知不知道白皮的鸡蛋都是基因改良的?!根本不符合草蛋鸡标准!
A:那我就狠狠心买最贵的柴鸡蛋。
某牛人:柴鸡蛋?你还真是个技术侏儒啊!你以为最贵的就是最有营养的?!你知道那些柴鸡为什么比草蛋鸡跑得快?全都是打了激素的!
A:那你说什么鸡蛋好吃吧?
某牛人:我没吃过,我只看别人吃过。
由于种种原因,某牛人不得不在不同的地方使用不同的账号,以下为某牛人泻出的三个账号:
薛非(图灵社区被禁账号)、
garbageMan(被评为博客园No.1小丑。请移步 http://www.cnblogs.com/pmer/archive/2012/07/22/2604229.html)、
pmerofc(吹毛求“屁”的最佳典范、人工编译器的性能表现。请移步 http://bbs.chinaunix.net/thread-3748157-1-1.html)
这个东西陈冰副总编在不同的地方至少贴了5回。鉴于这已经完全沦落到了骂大街的层次,所以在此我们不予置评。除此之外,陈冰副总编还在CU论坛发表了很多与职业编辑和副总编身份完全不相称的言论:
“CU的喷这本书的人根本就不是开这门课的大一新生,不是这本书的目标读者群,而是一群既不会看这本书,也不会买这本书的半大中年人。他们喷这本书无非是带着一种得瑟的甚至是不得志的心情来找个茶余饭后的谈资。”
“我策划这本书付出了多少精力,作者生病期间写这本书付出了多少,都是这些人所不了解的,也和他们没有任何关系。无论这本书怎样,我天生就是一个倔强执著的人,无论这本书有怎样的市场反应,我都会把它送到市场上去,这就是这本书要面对的。”
“欲使其灭亡,必使其疯狂。我可以向你保证,有你哭的时候。在此之前,我会继续看你的疯狂表演。”
“你记住我跟你说过的那句话。我是言出必行的人。”
“对你这种垃圾人来说,你的垃圾时间很多,所以可以扯那么多蛋,对我,没有那么多时间和你闲扯这种淡。所以我简单回答如下:
1 · 这本书即将出版,并且出版时会是一本优秀的计算机畅销书。作者陈良乔的水平和见识远在你之上。你和陈良乔没有可比性。事实上,你不仅不配做IT书作者,你连做人都不配。
2 · 图灵的品牌是靠每一个图灵人对IT图书出版的责任、耐心、诚意和始终如一的精神打造的,而不是靠拍胸脯来保证每本书都一个错没有拍出来的。图灵这么多年来勘误多的书并不止一本两本,如果一两本勘误多的书就能毁掉图灵的品牌,那也不会有图灵的今天。一巴掌就能拍碎的品牌不叫品牌。会被无耻之徒的污蔑摧毁的口碑也不叫口碑。
3 · 我确定这本书可以也应该出版,决不是因为我是这本书的策划编辑,而是因为我看过全书,知道这本书的总体质量。如果这本书是别的策划编辑策划的,我也同样会坚定的支持这本书的出版。
4 · 你会为你的言行付出代价。”
在这些言论中,陈冰副总编以读者对象是“大一新生”为借口,完全无视有专业知识的读者的评论,恼羞成怒地用恶毒的咒骂予以“反驳”。充分说明了他想利用“大一新生”缺少专业知识推广《伴侣》这本劣书的不良动机,这是对读者明目张胆的公然欺诈行为。
陈冰副总编公然宣称“无论这本书怎样”“无论这本书有怎样的市场反应,”,“都会把它送到市场上去”,仅仅因为副总编和作者“付出”了“精力”,并且陈副总编“天生就是一个倔强执著的人”,完全是以自我为中心,毫无“从事出版活动,应当将社会效益放在首位”基本意识。
陈冰副总编甚至悍然对对《伴侣》持反对意见的读者进行造谣污蔑、恶毒谩骂、人身攻击,甚至发出了人身威胁。尽管我们都经历过“0 BUG”那样恶劣的IT出版事件,但陈副总编的所作所为,还是大大超出了我们的想象。为了推行由他自己策划的、不合格作者完成的劣书,陈冰副总编非但全然不顾其职业身份和职责,甚至已经到了不顾个人形象对读者展开谩骂攻击、人身威胁的地步。据我们所知,在出版史上,如此恶劣的行径,这还是首次。
至此我们不难看出,这本劣书的出版,与陈冰副总编完全不顾劣书的负面社会效益的一意孤行密不可分。作为主编,这是一种乱用职权的渎职行为。陈冰的所作所为,已经完全丧失了一个出版工作者的职业底线和出版业最基本的职业道德操守,也极大地损害了人民邮电出版社在我们心目中的形象。
呼吁与期待
C语言自被设计至今,已逾四十年,其间无数程序设计语言问世,却无一有完全取代C语言的地位。究其原因,在于C语言具有高超设计理念与内在形式美。由此,C语言成为关于计算机基本系统的通用设计之首选。而之于计算机专业学科教学,C语言自然成为基础科目 —— 学生藉由学习与运用C语言,可以自觉地培养与训练其关于计算机学科的理性思维,又可以掌握一个深入了解计算机基本系统运行原理的重要工具。我国几乎所有关于计算机学科之高等教育成分,也审时度势,约20年来,始终将C语言作为学科之基础。C语言之于相关专业学生之重要性不言而喻。
贵社以“立足信息产业、面向现代社会、传播科学知识、服务科教兴国”为出版宗旨,长期以来,为国内业界生产与输送大量优秀C语言书籍,在广大读者中建立了良好的口碑。然而,贵社于去年兀然推出的《C程序设计伴侣》一书,实乃与贵社以往风范严重不符。此书内容本身之于C语言有重大并不可忽略之大量谬误,此前已尽述,且此书之炮制过程中有关责任人员亵渎职守、自贱品格,亦于出版道德规范相牴,令关心贵社以及国内计算机学界之我等众多读者寒心抱屈。
《C程序设计伴侣》傍附于谭浩强《C程序设计》,然而非但未 “把原书中遗漏的、忽视的、错误的,以及那些未曾涉及的但作为初学者或称为未来的程序员绝对应该知道的基础知识和素养一并讲讲清楚”,反而叠加误导、迁延谬识。初学者若期冀此书为盼通茅塞,则不啻自落井石!该书的出版,不但背离了《出版管理条例》“传播和积累有益于提高民族素质、有益于经济发展和社会进步的科学技术和文化知识”的要求,产生了很差的社会效益,也与贵社的出版宗旨全然背道而驰。故此我们相信,贵社对此当不至于漠然坐视,放纵姑息。
同教育、医疗、食品等行业一样,出版也应该是一个良心产业。出版劣书同生产地沟油、三鹿毒奶粉没有什么本质区别。因此,作为对C语言劣书有识别能力的读者,我们认为我们有义务指出这本书的谬误,告诉那些不懂C还没有自己判断能力的初学者这本书的真相。提醒对C语言一无所知或者基本一无所知的读者予以戒备。因为我们深知,对于初学者来说,一本坏书的作用难以估量。
同时我们也呼吁贵社,本着对读者负责的精神,承担起对社会应有的责任,采取切实有效措施,不要让这种劣书再危害到更多的初学者,杜绝《伴侣》一类事件再次发生,以精益求精为旨,为社会提供更多的高质量图书。
此致
敬祝编安!
读者(排名不分先后):
cokeboL
btdm123
丁正宇
__BlueGuy__
starwing83
fallening_cu
KBTiller (键盘农夫,《狂人C:程序员入门必备》作者)
anders0913
gvim
gtestm
srdgame
pitonas
ecloud
justme0
libc334
ayh423
薛非 (《品悟C:抛弃C程序设计中的谬误与恶习》作者)
二〇一三年三月十五日
注:本公开信无限期征集签名