推推C语言新书《狂人C-程序员入门必备》

http://product.dangdang.com/product.aspx?product_id=20974009

http://www.china-pub.com/54010

《狂人C》在CU上的意见贴:http://bbs.chinaunix.net /thread-1821644-1-1.html

今天上午收到快递,收到键盘农夫的大作《狂人C》。首先要感谢国家,也要感谢键盘农夫给我这个机会拜读他的大作。

这本书拿在手里很厚实,翻了翻目录以及章节内容,不是那种教科书死板的风格,用词摘句很平实,感觉键盘农夫应该是有实际编程的经验才能写出这样的书。我在前面推荐的C语言教学,里面只包含了《Linux C编程》这本是国人著作。想不到现在还有人愿意静下心,写一本有价值的C语言专著,非常佩服!

草草看了一下内容,感觉值得改进的地方有这样几点(希望农夫不要见怪哦):

一是英文字体选的不好,看上去很不舒服。另外代码字体与正文中的英文字体差别太大。字体的选择对于程序员来说其实不是小事。另外中文字体的间距感觉也不是很合适,有些别扭。还有一点是边界留的太窄,很难做笔记(当然,留的宽了也会增加页数成本,唉)。

第二点是变量名的选择,键盘农夫应该是针对这个问题有所考虑,选择了拼音首字母作为变量名或者函数名,但是我个人感觉这其实不是一个很好的选择,尤其是qiufqsm这种,很难直观的看出来。我个人觉得程序员必须要学英语用英语,文件名、变量名不要用中文或者拼音,其实读写简单的英文对于大多数程序员来说不是难题。

由于中文变量名的存在,后面tic-tac-toe的工程实例看的很费劲,因为非常不习惯这种风格。

第三点算是小问题,比如我倾向于使用“字符字面值”,“多维数组”这种翻译,而键盘农夫兄是另有选择,但是由于书中基本附带了英文原文,理解起来并不困难。还有就是指针这一章中,指针地址很多写的比较详细,但是感觉应该稍作说明(也许有,但是看的潦草没注意)这些地址仅仅是假设的。

第四点也不是大问题,就是练习题没答案,对于初学者来说,有答案对照,可能会帮助很大。当然也可以有一个网站提供答案,也是一个办法。

尽管吹毛求疵提了这些问题,但是要说的是,这本书写的还是很认真的,尤其是一些关键点,比如指针及数组的概念,用了大量篇幅来介绍,也体现了作者功力所在。建议各位C语言爱好者买一本读一读,一定会有所收获。我也会在将来把自己的读后感放上来分享。

C语言表达式计算顺序的一个小问题

浏览Q.yuhen的博客这篇文章 http://www.rainsts.com/article.asp?id=959 发现一个小问题,估计有类似想法的同学也有,所以记录一下。

问题在于这句话:

“很显然,依据 cdecl 规则,"printf(…, test(2), test(1))" 中的 printf 函数参数依次从右向左 "入栈"(暂且用这个说法)。因此 test(1) 被先调用,然后才是 test(2),上面的汇编代码也说明了这点。”

尽管事实是这样的,但这是一个有问题的说法。

cdecl的入栈顺序是没错的,这个入栈顺序是针对每个逗号分隔的表达式结果而言。也就是说对于每个结果一定是这样的顺序。但是表达式计算顺序(或者说每个逗号分割的函数调用)其实是没有规定的。这在K&R影印版第二版52页最末一段说的非常清楚,下面的f和g不一定谁先调用:

C, like most languages, does not specify the order in which the operands of an operator are evaluated. (exceptions are && || ?: and ",") for example x = f() + g();

另外在63页开始:

The commas that separate function arguments, variables in declarations, etc., are NOT comma operators, and do NOT guarantee left to right evaluation.

这也就是说逗号分隔的函数参数与逗号操作符是不一样的,不保证从左至右的计算顺序(当然也没有保证从右至左)。

注意这个evaluation,其实就是对test(2), test(1)的调用。如果这些调用有边界效应,在不同编译器、操作系统上可能会得到不同的结果。另外类似的问题有对在参数列表中,同一个变量调用++两次以上。

如果要保证求值顺序(注意不是求值结果的入栈顺序),只能用临时变量保存调用结果,或使用逗号操作,&& || :?这样可以保证求值顺序的操作才行。

其实我在前面这篇博客已经提到了这个问题,只是看到这个提法再重申一次。“C语言中的表达式求值问题http://sunxiunan.com/?p=1684

参考资料:

http://www.andromeda.com/people/ddyer/topten.html 参考第七条

C陷阱与缺陷 3.7求值顺序

能打印出自己的代码

任务:写一段C语言程序,打印出自己整个代码,不能差一分一毫。

这个任务在计算机编程中有个术语叫做:Quine,维基百科上有专门的条目介绍。

http://en.wikipedia.org/wiki/Quine_%28computing%29

quine代码好像没有太大作用,但是我们可以将其想象成一种可以自我繁殖的生物,每次运行就产生一个同样的实体,然后一个个这样繁殖下去,哇塞,这就是恐怖片了!

搜索了一下,基于C语言(使用VC2010编译必须设置language为C才可以)主要有以下几种比较简洁的实现:

main(a){a="main(a){a=%c%s%c;printf(a,34,a,34);}";printf(a,34,a,34);}

这算是第一种模式,关键在于%c%s%c这个打印格式,然后输入参数中多半有34或者0x22(也就是双引号)或者引用到字符串数组中双引号的位置。

另外一种是通过宏定义实现的:

#define T(a) main(){printf(a,#a);}
T("#define T(a) main(){printf(a,#a);}\nT(%s)")

宏定义的实现消除了对双引号的使用,格式更为灵活一些,不需要是abab这种模式了。

关于quine一个非常详细的论文,介绍了深层理论、如何写quine代码等等,可以看看。

http://www.madore.org/~david/computers/quine.html

另外可以参考:

http://www.c4swimmers.esmartguy.com/selfcodeprint.htm

http://www.c2.com/cgi/wiki?QuineProgram

C语言中的表达式求值问题

在细读《C programming Language 2nd》(K&R)到53页的时候,看到作者举了这样一个例子:

a[i] = i++;

如果你知道这个表达式有什么问题,就不需要继续看下去了,下面内容对你而言有些浅显。

如果你也像我一样,觉得这个很容易理解啊,i++这个表达式就是先取i的值返回,然后对i自加。a[i]就是i的值,然后i自加1。

这其实也是C语言陷阱之一,在K&R中反复强调(page52以及page202),函数参数也好、某个操作符中的操作数也罢,表达式求值次序是不一定的,每个特定机器、操作系统、编译器都不一样。(特例是&&,||,?:以及逗号操作符,它们会确定表达式求值顺序的)

还有类似的例子如下:

f() + (g() * h())

或者 int i = 7;  printf("%d\n", i++ * i++);

在我们一开始提出的问题中,a[i]取下标操作与i++自增的运算顺序是不一定的。这就是一种不确定性。

在第一个表达式中,可以确定的是这些内容:g和h函数的结果会先做乘法运算,然后与f函数的结果做加法运算,但是f,g,h谁先被调用,谁后被调用,这是不一定的,C语言标准没有对此作规定。

第二个表达式中,第一个自增操作和第二个自增操作以及乘法操作的顺序是不一定的,所以结果根本无法确定,即使我们给i++都包裹上括号 (i++) * (i++)也是一样的。括号并不会得到确定的操作数计算顺序,括号只能保证操作数的值(就是表达式或者函数求值的结果)相互计算的顺序。

K&R提供了几个建议,首先是函数调用嵌套赋值语句(或者可能改变参数值的操作)或者自增操作,都会有"side effect”,我们应该确保这种边际效应不会影响程序运行结果,如果某个表达式对同一个变量同时修改两次,那么一定要非常注意这是不是你想得到的结果。

如果不知道特定机器上实现如何,就不要依赖表达式计算顺序;即使知道了实现方式,这种依赖也不是一种好的编程方式。比如f、g、h函数计算,可以用赋值给临时变量来决定需要的顺序,对于print(i++)这个表达式也是如此,在printf之外先计算i的值。用K&R第一版的话来说就是if you don’t know how they are done on various machines, that innocence may help to protect you.

参考资料:

http://www.eskimo.com/~scs/cclass/notes/sx7c.html

《C Programming Language second edition》