现在的位置: 首页 > 综合 > 正文

315,关于《C程序设计伴侣》一书致人民邮电出版社的公开信

2013年11月25日 ⁄ 综合 ⁄ 共 14921字 ⁄ 字号 评论关闭
文章目录

邮电社,不出版文盲写的书行吗?

目录

邮电社,不出版文盲写的书行吗?... 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. 1.

抱歉!评论已关闭.