Mar 18

谭师傅的书流传之广,令人叹为观止。于是决定好好把谭师傅的书好好研读一下。能够完整下载的,只找到了一个扫描版的《C语言程序设计》第二版。Google Books倒是有第三版部分章节。以后若不加说明,都是指Google Books里能够看到的内容。

以下是我对第一章1.1节的读书心得。

心得1:谭师傅年份拎不清。例如:

1、B语言应该在1969年在PDP-7实现,1970年在PDP-11上实现,而不是谭师傅说的1970和1971年。
2、第一个ANSI C标准于1989才正式出版(即C89),而不是谭师傅所说的1983年(1983年刚刚成立X3J11委员会)。

心得2:谭师傅事实也拎不清。例如:

3、谭师傅说ISO接受“87 ANSI C”作为ISO C标准(即C90)。事实上,不存在什么“87 ANSI C”。C90和C89区别非常小。
4、Thompson从来没有在PDP-7上用B语言实现过Unix。事实上,PDP-7上B语言编译器不直接产生机器代码,而且Thompson也从来没有过这样的打算,因为PDP-7的硬件限制太多。

心得3:谭师傅喜欢想当然。例如:

5、谭师傅说B语言的B是取其前身BCPL的第一个字母,C语言的C是取其第二个字母。事实上这两种语言的命名历来都有两种说法,就连Dennis Ritchie都没有像谭师傅说得那样斩钉截铁。

心得4:谭师傅语言表达很成问题。例如:

6、谭师傅说C语言是一种“系统描述语言“。这玩艺儿到底是啥?
7、谭师傅说Brian Kernighan和Dennis Ritchie“合称K&R”。实际上他们合著的那本经典书才被称为K&R。

1.1节学习完毕,以后再接着读

Mar 16

很多人认为C语言中的数组和常量指针是一回事。毕竟,如果我们有这样的代码:int a[10]; int * const p=a;,那么这两个变量a和p可以完全等价的使用。也就是说a[i]和p[i]都是访问的同一段内存空间,而且p和a一样都是只读的。那么,数组和常量指针在所有情况下都等价吗?在你回答“当然是”之前,请先看下面的代码:

有两个文件,文件1中有如下定义:

int array[10];

文件2中有如下声明和使用:

extern int *array;
array[0] = 1;

编译没有问题。运行起来,你猜怎么着?结果是“Segmentation fault”。为什么呢?原因就在于,数组和指针虽然有时可以混用,但从根本上来说是不同的类型。文件2相当于告诉编译器,说我有一个整数的指针叫做array,但它是在别的文件中定义的。而文件1中,我们只定义了一个叫做array的数组,所以造成错误。不过,为什么是“Segmentation fault”而不是其它呢?这就要看看数组和指针的区别到底在哪里。

在C程序中,每一个数组名在编译时,在Symbol Table里都对应着一个内存地址,这个地址就是该数组首元素的地址。相反,每一个指针p(无论是否常量)在Symbol Table里所对应的,是变量p的地址,而在我们为这个指针赋值后(int * const p=a;),这个变量p所在的内存空间就储存了另一个地址a,也就是真正数组首元素的地址。在访问数组时,如果我们说 a[1],则编译器会从Symbol Table里把a的地址找到,然后再加上a中一个元素的大小,得到一个新地址,再从该地址取得一个值。反之,如果我们说 p[1],那么编译器要先从Symbol Table拿到p的地址,取出其中存储的值,再把这个值加上p所指的类型大小,得到另一个地址,再到这个地址把值取出来。最后结果虽然一样,过程还是不同的。这个区别在下汇编代码中就可以看得很清楚。

我们先回头来看看刚才的“Segmentation fault”。当我们在文件1中定义数组array时,array在Symbol Table里是一个地址,这个地址下存放的是这个数组的首元素。而在文件2中,我们告诉编译器,我们定义的是一个指针array,也就是说,Symbol Table里的array代表的地址下所存放的,是另一个地址。所以当我们说“array[0] = 1”时,程序实际从array所代表的地址中取得了一个数,把它解释成另一个地址,再试图对这第二个地址中内容进行写操作。在我们这个例子中,array在文件1中没有初始化,所有的元素都被编译器设成了0。所以在文件2中,当我们把array当成指针时,其中存放的值就是0。而对地址0(也就是NULL)进行写操作当然会出现“Segmentation fault”。

最后,我们再来看看从编译器产生的汇编代码,数组和指针的区别就更加一目了然。首先创建一个文件叫test.c,内容如下:

#include <stdio.h>

int a[10];
int * const p=a;

int main()
{
  a[0] = 1;
  p[0] = 2;
  return 0;
}

在Linux上经gcc -S编译后得到如下结果:

    .file    “test.c”
.globl p
    .section    .rodata
    .align 4
    .type    p, @object
    .size    p, 4
p:
    .long    a
    .text
.globl main
    .type    main, @function
main:
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl    -4(%ecx)
    pushl    %ebp
    movl    %esp, %ebp
    pushl    %ecx
    movl    $1, a
    movl    p, %eax
    movl    $2, (%eax)
    movl    $0, %eax
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
    .size    main, .-main
    .comm    a,40,32
    .ident    “GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2″
    .section    .note.GNU-stack,”",@progbits

我们可以清楚地看到,当赋值给a[0]时,只需要一个movl语句,即movl    $1, a,而赋值给p[0]时,则需要先拿p的内容(movl    p, %eax),再用间接寻址模式进行赋值(movl    $2, (%eax))。

 

Mar 13

昨天看了张云楼的《谭浩强大师的世界纪录》,也只是一笑了之。大陆的计算机教材,也就是那么回事。翻译的生硬、错误多,自己写的干巴巴、过时、错误多。就拿谭浩强的C语言教材来说,我只在近20年前读过,只记得味如嚼腊。现在唯一还有印象的,是谭师傅花了很大一段文字说明他为什么认为struct应该译作“结构体”,而不是“结构”。对中国人来说,想学好计算机,能阅读英文资料是一项必不可少的能力。

没想到今天还有一位“注意版本”同学不服气,一定要为谭师傅辩护,却不免底气不足。不过倒引起我的兴趣,把双方的文章又看了看,觉得有些问题不得不说。张指出了谭书三处问题(实际上是4个),总结如下:

1、谭书:TC中int 32767+1 = -32768,long 32767+1 = 32768。张质问:“敢问谭教授上机试过没有,我试过了,不行。”
2、谭书:变量可以作左值,…常变量不能作左值。张质问:“有常量、有变量,什么叫常变量。”
3、谭书:赋值表达式能够作为左值,例如(a=3*5)=4*3可得a为12。张质问:”真够经典的了,不知谭教授怎样创造出来的?“
4、谭书:ANSI 标准允许switch后面的“表达式”为任何类型。张质问:“不知谭教授真的看过ANSI标准?”

而“注意版本”同学则认为“张云楼老师说发现了大量低级错误,恰恰说明其见识太少”。并在扯了一阵什么project和makefile以显示其“见识”并不“太少”之后,作了如下回复(对于2没有回复):

1、“相信教材作者至少在某个版本上试验过,很可能后来版本更新了引起问题。”
3、“在有些版本的编译器确实是支持的。”
4、“除非你查遍所有版本都同样说法,才能指责谭浩强的教材。“

张云楼号称自己是“教了10几年的C语言“的“计算机教师“,暂且认为是中国计算机教师的一个样本吧。而见多识广的“注意版本”可算是(至少是曾经的)学生。一师一生,都是半吊子。不用说追英赶美了,和印度比比都觉得不好意思。对于张云楼提出的问题,我的看法如下。

1、谭书说明了是TC。我不知道张老师用什么“试过了”,遗憾的是,我刚刚试验的结果,TC2.01和TC++3.0结果都一样,就是谭师傅讲的是对的(这里要说明的是TC最高版本就是2.01,以后改为Borland C)。“注意版本”同学却连试一下都没有,就乱说什么“版本更新”的问题,估计是微软的东西用得太多了。

2、我来指导一下张老师。所谓“常变量”,就是constant variable,例如 “const int i = 20;”。这时 i 仍然是变量(variable),但其值不能被程序改变。这样的变量在第一次赋值后,就不能再作为左值出现。

3、不知道“注意版本”同学是在什么“版本”的编译器上试过?什么版本的C语言表态式可以作左值?不会是谭氏版吧?

4、我可以很明确地告诉不愿读书、不愿试验但见多识广的“注意版本”同学,不管你注意的是哪个版本的ANSI标准,switch都只能接受整数类型为参数。对于这一点,完全不需要看ANSI标准(很贵又很长),只需要看看C语言经典K&R(The C Programming Language)第二版就知道了。据我所知,这本书在国内已有不少影印本,网上也有的下载,查一查还是不难的。

没有想到的是,这么多年来,大陆的学生仍然用的是这种教材。以后有机会一定要把谭师傅的书好好研读一下。