May 27

接着上次的学习。之前在读谭师傅的书时,有的地方跳过去了。现在重新回过头来看看第四章,《最简单的C程序设计--顺序程序设计》。

第4.1节是《C语句概述》。我们来看看谭师傅的图4.1。

figure4-1

 

 

 

 

在这里我们可以很清楚的看到,这里少了点东西:函数声明。如果一个文件有函数声明,但没有该函数的函数体,则按这个图,这个文件不是C程序。再严格一点,这里还少了注释、typedef的位置。

谭师傅在讲解的时候说,“一个C程序可以由若干个函数和预处理命令以及全局变量声明部分组成”。这问题就更大了。如果我有一个 struct foo { int bar; } ,可能还可以勉强算是“数据声明”,但决不是全局变量的声明。静态变量的声明也没有包括在里面。

谭师傅接下来把C 语句分为5类,分别是(1)控制语句、(2)函数调用语句、(3)表达式语句、(4)空语句,以及(5)复合语句。这种分类方法和通常使用的分类方法(即 C标准中使用的分类方法)不同,很可能会造成初学者的误解。例如,谭师傅说,if()…else…这样语句的小括号里的内容是一个“判别条件”。 这个判别条件是个什么东西?它是不是一个语句?谭师傅没有讲。其实,它就是一个表达式,而且对它的返回值有一定要求(必须是scalar type)。按谭师傅的说法,for()… 的小括号里也是一个“判定条件”。这个判定条件是什么东西?它是三个用分号隔开的表达式(或按最新的标准草案,可以是一个声明加两个用分号隔开的表达式)。另外,函数调用也是表达式(这点谭师傅自己也提到了)。空语句其实也是表达式语句,理解了这点才能知道为什么可以写出 for(;;)… 这样的语句。我不明白谭师傅在搞出这么一个不伦不类的“分类”的时候有没有好好读过K&R或者C标准。

第4.3节讲的C语言的输入输出。谭师傅讲了什么是头文件:“文件后缀中‘h’是head的缩写,#include命令都是放在程序的开头,因此这类文件都被称为‘头文件’。” 这里有点小问题,h应当是header而不是head的缩写。

谭师傅还说,在包含stdio.h的时候,既可以用 #include <stdio.h>,也可以用 #include “stdio.h” 的形式,但却没有讲两者的区别。这是初学者比较容易搞混的地方,不该放过。事实上,对于编译系统提供的头文件,用第一种形式比较好。

第4.4节给了几个例子。我们又一次看到谭师傅把字符常量的类型当成了char,把getchar()的返回类型也当成了char。而实际上这两者都是 int。

第4.5节在(12)里已经学习过了,这里就不多说了。

Apr 24

之前已经学习过谭师傅的第五章。现在来学习每六章,《循环控制》。

这一章一开头就把我雷到了:谭师 傅讲的第一种循环,居然是用goto语句构成的循环。虽然谭师傅还是在两三处地方讲到,“滥用goto语句将使程序…可读性差”,“一般不采用 goto语句”,等等。不过,在谭师傅看来,为了“大大提高效率”,用goto语句也不妨。不过对于初学者,我有一句忠告:在任何时候都不要用goto, 因为任何用goto能实现的程序,不用goto也都能实现,而且程序更可读,更容易维护。

谭师傅说,goto语句“一般有两种用途”,一 种是和 if 一起构成循环,一种是从多重循环体中跳到循环体外。对此我要说:请拿红笔在上面打一个大大的叉,并把它做为最坏的编程习惯,时刻提醒自己不要把程序写成那样。第一个所谓用途,完全和 while 循环等价,又不如 while 简洁易读。第二种用途就是维护代码的程序员的恶梦。你花一分钟写了一个goto,做维护的人可能要花几个钟头才能明白这段代码是要做什么。要知道,这个做维护的人很有可能是你自己!

接下来谭师傅开始讲 while 循环。谭师傅说,当 while 后的表达式“为非0值时”,“执行 while 语句中的内嵌语句”。谭师傅又开始发明术语了。什么叫做“内嵌语句”?另外,在前面(13)里已经讲过了,严格来说,当把指针型数据作为逻辑表达式时,非 NULL时为真,但NULL的数值不一定为0。只不过这点一般程序员可以不用操心,因为编译器会自动做转换,如果 p 是指针的话,把 p == 0 语句变成 p == NULL。

谭师傅的例6.2中把主函数声明成了void(即“void main() {…}”)。这在C89里面还可以,在C99里不算错,但不提倡。使用gcc –std=c99来编译的话会给出警告。最好还是能写成 int main() {…} 的形式。

谭师傅在讲 do-while 循环时,在例6.3中又出现了奇怪的缩进。

谭 师傅又说,“对同一个问题可以用while语句处理,也可以用do…while语句处理”,但同时又说,在某些情况下,“两种循环的结果是不同的”。 我如果是初学者,一定会被谭师傅绕进去。其实说起来就是简单的一句话:do-while里的循环体至少会被执行一次,而while则不一定。谭师傅硬是花 了一个小节外加三个程序例来说明这个问题。

后面谭师傅又讲了 for 循环。我这里看不到全的,不过我相信对于这么一个最常用的循环语句谭师傅还是有能力讲清楚的。不过我对于谭师傅的“ for 语句功能最强”这个观点还是怀有疑虑。由于几种循环语句可以互相转化(包括用goto写的循环)在“功能”上似乎应该没有什么区别。区别只在于可读性。什 么样的语句写出来最好懂就应该用什么样的语句。

再往后,谭师傅又讲了break和continue。第6.8.1节中的例子里有一个 float pi = 3.14159,然后用 pi 来算圆的面积。要知道 float 只保证有6位有效数字,这个 pi 太过于精确了些,用个 3.14 就够了。如果需要更高的精度,最好用 double。例6.5中又出现了奇怪的缩进。

然后谭师傅又讲了几个例子。例6.6是计 算圆周率的近似值,里面有这样一句:while(fabs(t)>1e-6),其中变量 t 的类型是 float。ANSI C规定 float 能够表达的最小数为 1e-5,所以这个 while 可能在 t 值降到 1e-5 以下时就停下来。同样,最后输出时只有前六位是可靠的,因为ANSI C的 float 只保证6位有效数字。

例6.7是计算 Fibonacci数列的前40个数。这个例子很奇怪,因为算法实际上和开始给的公式有区别。开始的递归公式是由前两个数推出第三个数,实际的算法则是一 次由两个数推到下两个数。虽然结果是一样的,例子本身却因此变得不容易理解,而且不适用于计算奇数个数的Fibonacci数列。

例6.10里又出现了类型不匹配。getchar返回的是一个int,字符常量也是int,而谭师傅总是用char,有点望文生义的意思。

第6章就学习到这里,下次再继续学。

Apr 12

前面已经学过了第四章中的后半部分,今天继续学习第五章,《选择结构程序设计》。由于谭师傅这一章讲得比较拖沓,也没有太多细节上的错误。所以这一章我们基本上就高屋建瓴吧。

谭师傅开篇就给了一个无比经典的语句,特抄录如下:

if( x>0 ) y=1; else y=-1;

我每次看到这样的写法头就要大一圈,偏偏中国同事们还特别喜欢用。以前我总是百思不得其解。现在好了,找到根子了。

在 92页上,谭师傅本来讲关系表达式讲得挺好的,突然中间插了一句:“C++…以True表示‘真’,以False表示‘假’“。要不是谭师傅也写了 C++的书,我还以为谭师傅从来没写过C++的程序呢。当然,这也可以理解为排版工人的问题,或者是Word自动改大小写,把true写成了True,等 等。

在讲到 if 语句时,谭师傅又别也心裁地把 if 语句分成了三种类型:只有 if、if 加 else、多重 if-else (即 if else if else if … else)。其实,第三种和第二种没什么区别。只要讲清楚了 if-else 整个是一个语句(其实后面谭师傅也讲了这点),那么各种的嵌套就是不言而喻的,并不值得单独分成一类。因为如果这样分了之后,那么如果我有 if if else 算不算第四类?if if if else 算不算第五类?我看这个可能是因为谭师傅对于程序语言的定义没有一个清晰的概念。这也可以解释为什么谭师傅总是侧重于各种语法、句法上的边边角角(虽然很 多时候整得不太对)。

有一个值得一提的是,谭师傅讲到 if 后面的表达式“不限于逻辑表达式,可以是任意数值类型”,“包括整型、…、指针型数据“。这句话不太对。最重要的,就是指针型数据并不是“数值类型”。

具 体到 if 语句,指针和其它类型的区别在于,当变量 p 不为指针时,if (p) 实际上相当于 if (p != 0),而当 p 为指针时,if (p) 则相当于 if (p != NULL)。也许你要说了,NULL不就是0吗?事实上,如果编译器把NULL定义为0,那是纯属偶然。NULL只需要被定义成某个不可能出现的地址就可 以了。在有些系统中NULL的确不为0。但是为了方便,如果编译器发现你把某个指针和0作比较,它会自动把0换成NULL。所以,如果说 if (p) 是测试 p 的值是否为0,这个论断只对 p 不是指针时才准确。当然,这点对于初学者来说问题不大。只要记住指针是C语言一个特殊类型,和一般算术类型不同就行了。

谭师傅花了很大的 力气讲 if 语句的嵌套,并且在好几处地方提醒同学们,要注意哪个 if 和哪个 else 匹配。其实,if 和 else 匹配的问题之所以成为问题,完全是因为谭师傅在开头的那个经典语句。如果谭师傅从一开始就教导大家把这个语句写成下面这样,还能成问题吗?

if (x > 0) {
  y = 1;
} else {
  y = -1;
}

谭 师傅还用了整整一个小节去讲“条件运算符”(即 ?: 操作符)。固然又知道了几种茴字的写法,却让人头又大了一圈。不仅如此,有一个很重要的问题谭师傅还没有讲到,就是如果后面两个操作数的类型不同,那么这 整个表达式的类型是什么?如果只是一般的算术类型也许还好说,但加上const呢?指针呢?如果一个是结构体另一个是联合体呢?我敢说谭师傅可能想都没想 到过这个问题。其实对于初学者,最好的忠告就是,非到不得已,不要用这个操作符。

这一章里最雷人的,莫过于谭师傅对于switch的讲 解。有些已有Yush以及其他人讲过了,不过在这里还是再提一下。首先,谭师傅再次祭出“谭氏标准”,说“switch后面括号内的‘表达式’,ANSI 标准允许它为为任何类型”。实际上,没有任何C的标准(包括ANSI之前的K&R C)可以允许这个表达式是“任何类型”。它只能是整数类型(即可以表达为整数的类型,包括诸如字符、枚举等)。其次,在105页上,谭师傅自己也说,一个 case执行完之后会执行下一个case而不会自动跳出switch,但同时又说,“各个case和default出现的次序不影响执行结果”。这怎么可 能呢?必需还要加上一个条件:每个case和default后面都有break,这才有可能成立。可这时谭师傅还没讲过break是什么东东呢。

在这一章最后一小节里,谭师傅又给了好几个程序的例子。粗看了一下,应该没有什么大错,不过称不上是范例。一些写法很有问题。不过这里就不多讲了。下次再说。

Apr 08

前面已经学完了第三章。由于google books不提供全文,每次只能看到一部分,所以估计以后都只能学习一些片段了。

今天要学习的是第四章,第4.5节。这节没看到开头部分,只能从79页看起。不过看起来是在讲标准I/O库里的函数。

1、谭师傅说,“f 格式符”是“用来输出实数(包括单、双精度)”。又忘了有long double这类型了。

2、谭师傅说,“单精度实数有效位数一般为7位“。这个以前已经学习过了,ANSI C规定这个有效位数最少6位,具体取决于编译器。类似地,双精度数有效位数最低为10位(谭师傅说“一般为16位”)。

3、 谭师傅似乎忘记了自己之前说过的,两个float作算术运算时会变成double。这个说法当然对于ANSI C不成立。但是,如果它成立的话(比如在旧版K&R C中),那么例4.6就是错的,因为在printf(”%f\n”, a+b)里面,虽然a和b都是float,但加了之后变double,而float和double都是用的%f来表示,因此printf会按double 的精度输出,怎么能看出有效数字只有7位呢?

4、谭师傅说,“%m.nf,指定输出数据共占m列…左端补空格”。其实,如果m是一个开头为0的数,补的就不是空格,是0。

5、 谭师傅说,用“%e”格式时,“有的C编译系统自动指定给出数字部分的小数位数为6位,…指数占3位”。本来谭师傅这里很有可能蒙对一个C标准的,十 分可惜。ANSI C规定,这种格式下默认小数位数就是6位,而不是什么“有的C编译器”。附带说一句,ANSI C规定的指数位数是至少两位。

6、 谭师傅在讲到“%g”格式时说,自动选择 f 或 e 格式的依据是“选择输出时占宽度较小的一种”。我们在这里看到,谭师傅又一次地想当然,自己制定“谭氏标准”了。课后思考题:若 a = 100000(10的5次方),则 printf(”f=%f,e=%e,g=%g\n”, a,a,a); 会输出什么?是选择宽度较小的一种吗?提示:ANSI C规定,当输出的数值太大(超过一个编译器决定的值),或太小(小于10的负4次方)时,用 e 格式,否则用 f 格式。

7、对于scanf谭师傅用心良苦地讲了“%d%d%d”的四种写法,哦,不是,是四种输入的方法。不过,在表4-3格式字符里,漏了一个 [ (左方括号)和一个 n 。在4-4附加格式说明字符里,又少了大写的L(谭师傅老是忘记还有long double这个型)。

8、谭师傅又教导我们说,scanf 的控制格式里也可以有一般字符,这是“为了使用户输入时添加必要的信息,使含义清楚,不易发生输入数据的错误”。看来谭师傅没有考虑到,同样的格式也用于 fscanf、sscanf,等等。就算是 scanf ,程序的输入还有可以是重定向(redirect)的或者是经过管道(pipe)的,大部时候不关“用户输入”什么事。

好了,第4.5节就学习完了。下次再继续

Apr 02

前面已经学习到第3.8节。现在继续学习第3.9节,《赋值运算符和赋值表达式》。

1、谭师傅开宗明义地说,“赋值符号‘=’就是赋值运算符”。由于中文里面的“是”字比较含糊,也没有单复数,学生学到这里有可能会以为编译器会对“=”和“+=”这两种运算符做不同处理。事实上,从后面也可以看到,谭师傅始终也没讲清楚这两种情况的相同和不同之处。

2、谭师傅在讲到类型转换的时候说,将浮点型数据转换成整型时,会“舍弃浮点数的小数部分”。这又是一个谭氏标准,因为ANSI C里规定既可以向上取整,也可以向下取整,由编译器自己决定。

3、 谭师傅还说,“将一个double型数据赋给一个float变量时,截取其前面7位有效数字”。float的有效数字不一定是7位,这点我们之前已经提到 过了。这里要说的是,标准C规定这种情况下既可以向上取最近的数值,也可以向下取最近的数值,与前面第2点类似,也是由编译器决定的。

4、谭师傅说,在进行以上转换时(即double到float),“数值范围不能溢出”,否则“就出现溢出的错误”。不知道所谓“溢出的错误”是指的什么?不过标准C里倒是有说法,说这种情况是undefined,也就是说任何事都有可能发生。

5、 谭师傅说,“将一个int, short, long型数据赋给一个char型变量时,只将其低8位原封不动地送到char型变量(即截断)”。同样,如果系统的char是有符号的,而且数据太大导 致溢出,标准C里也是undefined,任何事情都有可能发生。事实上,如果想把一个int数据(比方说“int i”)的最低8位拿出来放到一个字符型变量里,唯一可靠的方法是“unsigned char c = i & 0xff;”。后面讲long转换成int也类似。

6、谭师傅在总结类型转换时说,“赋值规则…归根到底就一条:按存储单元中的存储 形式直接传送”。看来以上谭师傅的种种高论基本上就归根到这一条了。其实谭师傅自己也讲过,从比较小的类型转换成比较大的类型时,标准C规定的是数值传 递,所以在用补码存储数据的系统中,负数前面会补1,所谓“符号扩展”是也。这会怎么就忘记了呢?更重要的是,标准C中规定,如果有符号的类型数据溢出, 其结果是undefined,也就是说无法预料。如果写程序时依赖于“存储形式直接传送”,其结果就算不出错,也必然依赖于特定系统、不可移植。

7、谭师傅在第3小节《复合的赋值运算符》中给出了几个复合运算符(如+=)的例子,并且教导我们说,a+=3等价于a=a+3,并在后面又重复了一次这 个“等价”关系。很可惜的是,对于a+=3和a=a+3的区别,谭师傅只字未提。其实说起来也很简单,就是a+=3时,a的值只会取一次,而在a=a+3 中,a的值会被取两次。如果a是一个变量,那么这两种情况确实没区别,但如果a里面有一个表达式呢?课后思考题:a[i++]+=3 和 a[i++] = a[i++] + 3等价吗?

8、谭师傅还教导我们说,使用这种复合运算符的目的之一,是为了“提高编译效率”,因为“这样的写法与‘逆波兰’式一致”。看到这句,我又被谭师傅雷到 了。我咋从来就不知道逆波兰式是这样的哩?让我想想,a+3的逆波兰式是a 3 +,如果把=也勉强算上,那就应该是a a 3 + =。啊,有个“+=”吔。

9、谭师傅在第4小节《赋值表达式》中说,“常变量也不能作为左值,因为常变量不能被赋值”。看来谭师傅不雷死人不算完。不知道一个不能被赋值的变量要来 有什么用?读者可能会说了,谭师傅可能只是一时笔误,其实他的“真实意思”是指常变量只能在声明的时候同时赋值,而之后就不能再被赋值了。嗯,这个解释可 以接受。不过且慢,谭师傅书写到这里还根本没讲过什么是常变量哩。而且,下面谭师傅给出的例子里就有一个“int a=3,b,c;” ,说明谭师傅也认为声明的同时赋值也算是赋值的。所以谭师傅的“真实意思”只可能是,声明并定义常变量时,那个语句不算赋值,其它都算。我的天!

10、谭师傅接着又花了很大的篇幅讲了这么一个道理:“赋值表达式中的‘表达式’,又可以是一个赋值表达式“。本来嘛,像“a=b=5”这样的式子在实际 编程时是应该尽量避免的。不过讲讲特殊情况也好,免得看同学(注:同样学谭师傅的书的同学)的代码看不懂。不过呢,这里谭师傅真的是雷死人不偿命,一口气 上来俩雷人表达式。第一个是 (a=3*5)=4*3。据说谭师傅的书第三版和第二版之间的区别之一就是第三版用了TC++ 3.0。我算是看出来了,这是把C++的句法当C来讲了。第二个雷人表达式是 a+=a-=a*a。本来我想谭师傅的意思是要讲讲运算符的运行顺序,不过一不小心就炸了雷了。在C语言里,如果在同一个语句中有多个表达式,其中有side-effect(如赋值),而且其结果取决于这些表达式的执行顺序,那么其结果将是unspecified,也就是实际结果取决于编译器。K&R第二版的2.12 节中还给出了几个类似的错误,如 a[i] = i++; 和 printf(”%d %d”, ++n, power(2,n)); 。

既然已经学习到了这里,就顺便学习一下第3.10节,《逗号运算符和逗号表达式》。这个逗号操作符有“最难懂的操作符”(most obscure operator)的称号。谭师傅讲了一个小节,总之就是教你怎么把程序写得更难懂。不过我还是认真学习了,免得看同学(注:同上)的代码看不懂。

好了,第三章就学习完了。至此,我能够从google books上看到的内容已经学习完毕。

“到这里,就到这里。休息,休息一下。”

休息好了再继续

Mar 31

前面已经学习过第三章3.5节《字符型数据》,现在学习3.6节《变量赋初值》。

1、谭师傅说,“C语言允许在定义变量的同时使变量初始化“。又把“声明”当了“定义”。

2、谭师傅还说,“也可以使被定义的变量的一部分赋初值”。如果不看后面的例子,估计谁也不明白谭师傅讲的“变量的一部分”是什么部分。原来,谭师傅的意思是,“int a, b, c=5”这样的语句只对变量 c 赋初值。原来这就叫做“变量的一部分”啊。

3、谭师傅又教导我们说,“静态储存变量和外部变量的初始化是在编译阶段完成的”。谭师傅的“谭氏编译器”真是牛到不行了,程序还没运行就能初始化变量了。

接下来是第3.7节《各类数值型数据间的混合运算》。

4、谭师傅在讲什么是“数值型数据”时讲了整型、浮点型、字符型,漏了枚举型。本来嘛,按K&R,所有这些都叫做“算术类型”,不就很清楚了吗?按“谭氏分类法”,连谭师傅自己也不免考虑不周。

5、 谭师傅教导我们说,“float型数据在运算时一律先转换成双精度型,以提高运算精度”。不幸的是,前半句只对ANSI C之前的K&R C成立。在标准C中,两个float做算术运算时已经不会再转换成double。而后半句则完全是错的。K&R C里做了这样的转换只不过是因为当时Unix运行的硬件上有内置的双精度型,所以用双精度型反而比单精度型要快。与此类似的是char和short在表达 式中自动转成int。都是因为提高速度而不是提高精度。

接下来是第3.8节《算术运算符和算术表达式》。

6、谭师傅把运算符分了很多类,看起来是按运算符的“功能”分的,着实下了一番工夫。不过,C运算符的真正重要之处,即优先级和结合性,谭师傅没有仔细讲,只教人去看附录。有几个学生读课本时会去仔细研究附录?

7、谭师傅把做为负号的-和作为减法的-当成了同一个运算符来讲,实在是不应该。

8、谭师傅说,“如果参加+、-、*、/运算的两个数中有一个实数或双精度数,则结果是double型,因为所有实数都按double型进行运算”。看了 这句话,让我以为谭师傅在讲Fortran还是什么的,反正不是C。因为C语言里没有“实数”这个类型,也没有什么“所有实数都按double型进行运算 ”的规则。如果说C的“实数”是指所有可做算术运算,又非整数的数,那么它就应该包括“long double”,其精度不会比double低,因此在运算时如果有一个是long double,结果也是long double。

9、谭师傅在谈到“强制类型转换”时说,“在强制类型转换时,得到一个所需类型的中间变量”。这个“中间变量”不知道是什么神奇的东西。课后思考题:中间变量是变量吗?

好了,今天就学到这里,下次再学

Mar 30

前面学到了第三章第3.4节。现在来学第3.5节,《字符型数据》。这一节谭师傅暴露出来的最大问题,就一句话,他不明白字符常量的类型是int。此节出现种种奇谈怪论,下面一一道来。

1、谭师傅教导我们说,“字符型变量用来存放字符常量“,并且举例说,“ char c1; c1=’a'; ”。事实上,’a'是一个int,应当用int c1 = ‘a’才是正确的做法。

2、谭师傅又一次给出了强悍的谭氏标准:“所有编译系统中都规定以一个字节来存放一个字符,或者说一个字符变量在内存中占一个字节“。事实上,ANSI C只规定了字符型的最大、最小值。

3、谭师傅又花了整整一小节来说明对字符也可以像对整数一样进行算术运算。如果谭师傅把字符型也归到整型一类,不早就万事大吉了吗?

4、谭师傅说,“应注意字符数据只占一个字节,它只能存放0~255范围的数据“。一句话两处错。字符数据的大小C标准只有最小值规定,在所有系统中都能保证的取值范围也只有0~127。

5、谭师傅给出一个用字符变量做算术运算的例子,把一个字符变量减去32后就从小写字母得到大写字母。可惜的是,标准C并不保证小写字母和大写字母之前相差32,甚至都不保证字母表是以连续的整数来表示。所以这又是一个不可移植的C程序的范例。

6、谭师傅说,char c = “a”这样的语句是”错误的“,因为“不能把一个字符串常量赋给一个字符变量“。我不知道师傅这里“错误的”是什么意思,因为这样的语句是可以编译通过的(尽管有警告),因此语法上是正确的。这个语句也没有把什么“字符串常量”赋给变量,而是把这个字符串的首个字符地址赋给变量 。

7、谭师傅语重心长地说,“有些人不能理解:’a'和”a”究竟有什么区别“。我看,这只怕是要归功于谭师傅自己。如果讲清楚了’a'是int,”a”是数组,那还有什么不好理解?

好了,第3.5节就学习到这里,下次继续

Mar 27

之前已经学习了第三章3.1节和3.2节。现在接着学习第三章。

第3.3节

1、谭师傅花了很大一段讲什么是补码。我认为讲得还算清楚。不过,很可惜的是,整型数据在内存中如何存放,这不是标准C的一部分,而是取决于具体的硬件。为了让C程序可移植,ANSI C标准中从来没有规定过数据在内存中如何存放,将来也不可能。

2、谭师傅一会说有三种整型,一会又说有六种。这也就罢了。在学习3.2节时,我们已经提到过,整型应该包括字符、枚举,因为C语言里并不把这几种类型区别对待。像谭师傅这样讲法,有可能让人以为字符和枚举是什么特殊类型。

3、同上面第一点,有符号整型的第一位是否是符号位同样取决于具体硬件。

4、谭师傅说,一个int变量取值范围是-32768 ~ 32767。很可惜,这也是错的。一个int变量取值范围随编译环境不同而不同。ANSI C规定编译器必须提供一个<limits.h>,其中包含各种整数类型的大小。ANSI C还规定,对于int类型,最小的取值范围是 -32767 ~ 32767。谭师傅还不厌其烦地列出了TC的各种整型数据取值范围。这些在TC上是成立的。如果换了一个系统,就只有自求多福了。

5、谭师傅还花了一个小节来讲整型数据溢出时会发生什么事。这里的这句“将变量b改成long型…“已经多为人诟病,就不再提了。但其实最关键的问题在于,当数据溢出时会发生什么事,实际上在ANSI C标准中是undefined,也就是说发生任何事都有可能,取决于具体的编译、运行环境。

第3.4节

6、对于浮点型,同样,标准C并不要求数据在内存中以何种形式、用多少空间来存放。

7、谭师傅教导我们说,“ANSI C并未具体规定每种类型数据的长度、精度和数值范围”。实际上,ANSI C要求编译器提供一个<float.h>,其中必须包括关于浮点类型的信息,并对最低精度作了规定。比如,标准C要求float类型最少有6位有效数字,最少能表达10的37次方大小的数字,能表达的最小正整数是10的负5次方,等等。

8、标准C并不要求double的精度一定比float高,long double精度一定比double高,而只规定了这三种类型后一种精度不能比前一种低。

9、谭师傅说,“用程序计算1.0/3.0*3的结果并不等于1“。很遗憾的是,这又一次证明了谭师傅喜欢想当然,不上机实验。TC运行结果,1.0/3.0*3确实等于1。虽然谭师傅讲的关于精度的问题确实值得注意,但用错误的例子并不能说明问题。

小结一下:把可移植的C变成了不可移植的C,谭师傅功不可没。

好了,第3.3和3.4节学习完了,下次接着学

Mar 26

之前已经学习了第二章,现在来看第三章,《数据类型、运算符与表达式》。

第3.1节:

1、谭师傅说,struct是C语言提供的数据结构。这里还得有请谭师傅“翻译”一下所谓的“数据结构”是个什么东西,不然此类“谭氏术语”看得多了之后,头都会大三圈的。

2、谭师傅还把C语言里的数据类型分成了几大类,可谓标新立异,用心良苦。现在让我们来欣赏一下“谭氏C语言类型“(如下图)。

tan-3.1

通常,我们都会把谭师傅所说的基本类型叫做算术类型,因为这些类型的数据可以进行加、减、乘、除的运算,而不是像谭师傅那样把指针当成一更”高级“的类型。另外,谭师傅所谓的“整型”,大概是包括int, short int 以及long int。在标准C里,这些类型和字符型、枚举型一并称为“Integral Type”(中文ANSI C标准译为“整型”,但容易和int类型搞混),因为所有这些类型都可以用整数来表示。谭师傅所谓“构造类型”中的结构体和共用体,我们一般称之为“复合类型”,因为它们可以将不同类型的数据组合到一起。最后,C语言里没有什么“空类型”。换句话说,C不允许没有类型的类型。当然,谭师傅很有可能指的是“void”。不过,void可不能单独使用。void*表示可以指向任何类型的指针,void函数表示函数不返回值,而函数形参用void则表示函数不带参数。不知道谭师傅所谓的“空类型”是哪一种?

第3.2节:

3、例3.1中,谭师傅先定义了PRICE,再#include<stdio.h>。这样也不能说不对,但养成了习惯就不太好。

4、谭师傅说,符号常量不能被赋值,不过没有讲为什么不能被赋值。作为努力寻求知识的学生,我很希望看到谭师傅讲清楚#define 实际上是做了替换,把PRICE换成了30,这样,不能写30=40这样的语句不就很清楚了吗?

5、谭师傅说,PRICE的“作用域”(姑且认为师傅讲的是scope)在例3.1中是主函数。我看了心里一阵嘀咕。符号常量有“作用域”吗?当preprocessor把PRICE换成了30后,它就是一个常数。常数有什么“作用域”?还是“主函数”?

6、谭师傅在讲到变量时教导我们说,“由编译系统给每一个变量分配对应的内存地址“。嗯,让我想想,如果有一函数f,它里面用到了一个局部变量a,但这个函数从来没被调用过,那么这个a有被分配内存地址吗?如果有被调用过呢?是“编译系统”给分配的吗?

7、我也很希望谭师傅能够在讲变量时加一句,尽可能不要使用下划线开头的变量名,以免和系统定义的标识符冲突。很遗憾,没有看到。

8、谭师傅还斩钉截铁地说,“ANSI C标准没有规定标识符的长度“。这一句话让我赶快把K&R又翻出来看了一下。还好,C还是那个可移植的C。C89规定了一般标识符最少要识别31个字符,并区分大小写(external的6个字符不分大小写)。谭师傅提到PC上的MS C不知道是什么年代的古董,只认8个字符。我劝谭师傅还是把那破玩艺砸了,把字节都掰成比特扔掉。“建议变量名的长度最好不要超过8个字符“这种“谭氏C标准”还是留给师傅自己用吧。

9、谭师傅还举了几个长变量名的例子,例如student-name和student-number。且慢,这两个不是是非法变量名么?

10、虽然谭师傅提倡用长变量名,要见名知义,不过呢,师傅“在一些简单的举例中,为了方便起见,仍用单字符的变量名“。还好我心细,看到了这句,不然还以为写这些例子的是个菜鸟。

11、谭师傅又一次把“声明”说成“定义”。咳,说什么好呢。

12、最后,有一处错误信息里的“Undefined“被拼成了”Undifeed“。还好大多数人也就只看前三个字母,无伤大雅,无伤大雅。

好了,第3.1节和3.2节就学习到这里。下次接着学

Mar 25

之前已经学习过第二章的总论,现在继续学习。由于第二章整个内容极简单,也和C语言本身没什么太大关系,这里把第二章所有小节一并学习。以下是学习心得。

1、在2.1节中,谭师傅强调了“数值运算算法”和“非数值运算算法”的区别,并说数值运算算法“研究比较深入”,“算法比较成熟”,而非数值运算算法则“难以规范化“,“往往需要…重新设计解决特定问题的专门算法“。我不知道谭师傅有没有哪怕是尝试过学习Java/J2EE、C#/.NET,以及PHP、Perl、Python、Ruby等等语言及其相应framework。不知道谭师傅对它们所包含的“非数值运算算法”是怎么看的。就C或C++而言,不知道谭师傅对于STL、Boost、MFC、WDK等等是个什么看法。不过,考虑到谭师傅用的是TC,我们也不好勉为其难是不是?

2、在2.2节中,谭师傅给出了几个简单算法的例子。例2.1后面的解释中,谭师傅说“S1,S2代表步骤1,步骤2“,那么,前面出现的“步骤S3“是不是要理解成”步骤步骤3“?

3、在利用例2.1讲了循环之后,谭师傅教导我们说,“由于计算机是高速进行运算的自动机器,实现循环是轻而易举的,所有计算机高级语言中都有实现循环的语句,因此,上述算法不仅是正确的,而且是计算机能实现的较好的算法”。其实,算法要写成循环形式,恰恰是因为这样让程序员容易懂,而不是让机器容易懂。这才能解释为什么比较抽象的高级语言可以写出各种各样的循环语句而低级的汇编语言则只有有限几种。再进一步说,把循环语句改写成不循环的语句(loop unrolling)正是程序优化的一个重要手段。这就说明了循环语句绝不总是“计算机能实现的较好的算法”。

4、例2.3要求判定2000到2500年每一年是否闰年,并输出结果。这里谭师傅给出了如下算法:

S1: 2000=>y
S2: 若y不能被4整除,则输出y“不是闰年”。然后转到S6
S3: 若y能被4整除,不能被100整除,则输出y“是闰年”。然后转到S6
S4: 若y能被100整除,又能被400整除,输出y“是闰年”,然后转到S6
S5: 输出y“不是闰年“
S6: y+1=>y
S7: 当y<=2500时,转S2继续执行,否则算法停止。

这里有两个问题。一个是S2和S3中间的句号应该是逗号,否则“然后转到S6”就变成了无条件地转。按谭师傅在2.3节中算法应没有歧义的要求,这个算法是不及格的。第二个问题在于,S3和S4中的第一个条件判断是多余的。如果在程序中写上这样多余的判断,只会让程序更难懂。

5、从例2.1到例2.3,谭师傅一直都是用的“x=>y”来表示把x的值赋于y。到了例2.4,谭师傅突然又用了“x=y”的形式。乘法也直接就用星号(*)来表示。例2.5是更是“=>”和“=”混用。真是让人无所适从。虽然这是讲C语言的书,但谭师傅也要事先交代一下这里的“=”和“*”是什么意思,是不是?

6、在2.3节中,谭师傅说算法应该包含“有限的操作步骤”,然后又进一步说,“如果让计算机执行一个历时1000年才结束的算法,这虽然是有穷的,但超过了合理的限度,人们也不把它视为有效的算法“。我猜谭师傅从来没有研究过什么是复杂度,否则不会这样混淆算法的定义(或算法的有效性)和算法的效率和可行性。

7、谭师傅说,算法应“有零个或多个输入”。通常中文里说“多个”都是指“两个或以上”。那谭师傅的意思是仅有一个输入的不是算法?从后文来看,显然不是这样的。所以我只能理解为谭师傅想重造中文语法。而且,由于不可能有“负”个数的输入,这个所谓的“特性”就是废话一句。

8、第2.4节大部分内容实践当中用不着,就不多说了。只是其中又出现了几种不同的缩进风格(如例2.21)要留意。在该节最后,谭师傅又教导我们说,“应当强调说明的是,写出了C程序,仍然只是描述了算法,并未实现算法。只有运行程序才是实现算法”。如此精辟,不能不单独拿出来欣赏。我开始有点明白谭师傅在第一章里说C语言是一种“系统描述语言”是什么意思了。果然是博大精深。

9、在2.5节中谈到程序设计时,谭师傅很明显对于自下而上的方法不屑一顾,认为那是错的。他说,“有些人胸有全局…叫做自顶向下,逐步细化…另有些人写文章时不写提纲,如同写信一样提起笔就写,想到哪里就写到哪里,直到他认为把想写的内容都写出来了为止。这种方法叫做至下而上,逐步积累“,又说,“用第一种方法考虑周全,结构清晰,层次分明…这就是用工程的方法设计程序“。这么一段话,可以说是真正暴露了谭师傅对于稍大规模开发没有经验,也对工程设计没有概念,以为上面能设计出来的,下面就一定能实现。

好了,第二章就学习到这里了,下次接着学