概述
邮电社,不出版文盲写的书行吗?
目录
邮电社,不出版文盲写的书行吗?... 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'};
//将字符数组的某个字符赋值为字符串结束符'