Programming

Programming Competitions are Considered Harmful

今天在reddit上看到关于编程竞赛的讨论,有个人的观点很有意思,他(她)认为:

Programming is like music, as soon as you compete with others, you lose everything.
仔细想想这句话其实很对。我心目中供奉的那些大牛们,像Linus TorvaldsRichard StallmanBill JoyDonald KnuthDennis RitchieKen ThompsonRob Pike等等,没一个是因为什么编程比赛而出名,甚至都没听说过有一个曾参加过什么国际编程竞赛。Knuth参加过一个编程比赛,我承认,但那是人家CalTech自家的比赛。

不像我们天朝,为了参加某编程比赛争得头破血流,每年都乐不疲此。一年又一年,天朝拿到的金银铜铁牌已经无数了,可是怏怏大国到现在还没有一个图灵奖得主!噢,别提这个,有人会不满意,比如公子同学。反正我们近几十年内是没希望拿上,提也没用。

那天朝为什么至今仍没有自己的CPU和操作系统呢?喔,我说错了,我们有龙芯!不过这正好印证了我的观点。想一想,我们至今没有自己的操作系统,却有了自己的CPU,这又是为什么呢?那是因为ASIC领域没有类似ACM,*OI这样的比赛!

我不否认参加编程比赛的也有高手,但是如果为了比赛而去编程,那还有什么意思呢?现实中有很多很多问题需要解决,何必放此不管而解决那些人为制造的玩具问题呢?

那段C代码

上次提到了一些C语言的题目,留了两个没解决。其中第8个已经被“木天”解释了,我就不赘述了,有兴趣的看那篇文章下面的评论。还有一个,第26个,今天仔细看了看,其实也很简单。

[再链接一次原题:http://www.gowrikumar.com/c/]

具体原理如下:

首先,这个程序的作用是,把从第一个命令行参数传递进来的字符串(必须只含字母)用“点阵”打印出来。它必须至少带一个参数,否则就会崩溃。这大概是这个程序的一个瑕疵吧。

它是怎么做到的呢?很简单,既然是“点阵”,它就肯定要把“字体”存放好,然后读出。“字体”就存放在那个数组里面! 对比输出,和循环中的代码,我们不难发现,这个点阵是6x6的,因为屏幕输出的原因,必须是从上到下先逐行打印,每行必须是从左到右。而每行有多少个点,在哪个位置有点,这些信息都存放在一个数的比特位中!

再来看那个数组,一个数字表示一行,6个数就能存在一个字符的字体。所以这个数组也可以6个一组划分开。需要说明的是,A是从下标6开始的。至于前面的那6个0,主要是起保护作用,防止输入字符超出字母的范围时数组越界。

也是不错的一段代码。Enjoy it!

解答 C Puzzles

今天看到某人列出了几十个C语言迷题,很有意思。顺手做了一下,发现其实大多数都是来自C Faq,Expert C Programming,和网上出现的一些C语言技巧,一些很好,连我自己都被难住了,一些却又狠简单狠基础。除了第8个和第26个需要进一步理解,剩下的我全都作了简单的解答,不明白的请留言提问。

题目在这里:http://www.gowrikumar.com/c/

我的解答如下:

1. 见ECP第29页,一模一样的例子。

2. OS_HP-UX_print这不是一个有效的名字。//blush 一开始我也没注意到!

3. do-while的常识,你应该知道continue到哪里。如果不知道,看看C99第6.8.6.2节。

4. 常见问题,因为stdout是带缓冲的,如果不换行的话,是会有问题的!

5. h会被展开,而g不会。见C-FAQ,Question 11.17。

6. case后面的是字符,不是整数。

7. IA-64是RISC,不能访问未对齐的地址。

8. 感觉没错,但还未发掘出其原理。。。

9. 直接比较浮点数被认为是有害的,说过多次了。

10. 常识。如果很喜欢在初始化时用逗号运算符来标新立异,请加括号!

11. 当然合法!谁还以为printf是void类型?!它返回的是成功输出字符的个数!

12. duff’s device,是用Tom Duff的名字来命名的。很有名的一个东西,用来优化拷贝的,据说和Rob Pike此牛还有点儿关系~!不过注意,原始的duff’s device中的to可是不变的,因为它指向一个映射到内存的寄存器。

13. 正确,逻辑很简单,每次都保留除最低位置的1之外的1,减去1,依次测试。

14. 说过N^2次了,int foo()和int foo(void)是不一样滴!

15. 第二个是把a的内存比特位模式用int来解释。
第一个比较有意思,因为类型不匹配,而且float在被压入压出的过程中会被扩展精度,因为IA-32的浮点数寄存器都是80bits的。所以,float会被扩展成double后再当int处理!!

16. 声明的问题,指针不是数组。

17. switch嘛,当然会跳过第一个case/default之前的东西,所以初始化就被跳过了。

18. 切,形参写得再好数组也得退化啊!

19. format string overflow?!

20. C99 第7.19.6.2节,第5条。

21. buffer overflow

22. sizeof是操作符,编译时决定的,而非运行时。

23. ECP,第25页。

24. 等号优先级高于逗号。

25. 减法和除法错误,减数和被减数反了,除法同理。

26. 蛮牛的一段代码,还没看出其中的奥妙。以后看懂会另行说明。;-)

27. 以0开头是8进制,而最后一个是十进制。

28. scanf返回成功输入的个数。

29. /和*会被当成注释处理,所以那一行实际上是y=y;

30. -也要被输入。

31. &n

32. 加号优先级高于左移。

33. 当然不合法,return不能出现在表达式中。

34. 把for括号里的最后一个i换成n。暂时就想到这一个解法。

35. 显然ptr2不是指针。

36. 错误!因为会被0除!

37. 很明显是8。

38. free的地址不对。

39. 很简单,就是把a[n]换成了n[a],本质上一样。

40. 字符a前面的所有,见C99 7.19.6.2.12。

41. 一个是定义,其余是声明。

42. 取某个成员在其结构体中的偏移。

43. 就是宏的常见错误,比如:SWAP(a++, ++b)。

44. 某个表达式及其对应的值。

45. 参见Python源代码,Objects/intobject.c::int_add。

46. n/x向上圆整!

47. c会被自加两次。

48. 不能一个参数都没有就使用“…”。

49. 用?:三元操作符。;-)

50. 把输出字符的个数存入某个变量中,见过。

51. -_-b

52. %%

53. 后一个指针只读,前一个指针指的东西只读。

54. 前者不能重叠,后者可以。

55. %lf, %f, %g。就记起来这仨。

56. 写过N遍了。。。

57. 厄,看我这个:

include <stdio.h>

int main(void)
{while(printf(“Hello World!”)<0){}}

splice(2) considered useful?

无意中发现Linux还有splice(2)这么个玩意儿,而且Linus本人对其很是赞赏,还一门心思地给它做广告……那我们就来看看它到底是何方神圣。

splice(2)绝对比我想象中要难以理解,一开始以为它的功能和dup2(2)差不多,然后觉得不对,似乎和sendfile(2)差不多,后来发现也不对!(否则还加这么一个系统调用干嘛?!)它和sendfile(2)的差别还算是比较大的,后面我们会细谈。

splice(2)这东西,其实它的名字就已经告诉我们它是做什么的了,可因为它牵扯到了pipe,所以就变得不好理解了。我们知道,Unix里的pipe有两端,一个写另一个读。我们通常都用pipe来实现进程间的fifo通信。而splice却用pipe来连接一些东西,所以,它名字就可以告诉我们,它是用来把其它fd和pipe的某一端拼接到一起的!也就是说,splice(2)的两个fd参数中必须有一个是pipe的一端,否则就会产生EINVAL。而且,当某个fd是pipe一端时,其对应的offset pointer必须是NULL。而且而且,splice(2)不会block,读不到数据时它会返回0。还有一点格外重要,一定要注意数据流动的方向,splice(2)对这个方向是很敏感的。

我们可以看出,splice(2)和dup2(2)语意差别显著,并且,当调用两次splice(2)把pipe两端连接到两个其它文件时,其功能就等价于sendfile(2),都是zero copy。进一步思考,其实splice(2)控制的是某个不可见的buffer,而dup2(2)和sendfile(2)控制的都是文件本身。

wikipedia上也有对splice(2)的解释),不怎么详细,里面给的例子还可以,我对着它做了改进,算是用splice(2)实现了sendfile(2)的功能,代码见下。

注意1:LWN上的这篇文章过时了,不用看了。

注意2:man pages里对splice(2)原型的描述是错的!那两个表示offset的参数的类型都应该是loff_t,而不是off_t!我已经做了一个补丁提交给man pages的维护者。

补丁:http://wangcong.org/down/man-2-splice-loff_t.diff

代码:http://wangcong.org/down/splice_copy.c

被vim骗了

今天董溥同学在写一个脚本,遇到一个很奇怪的问题。为什么用awk提取出来的字符串总是带一个’r’?

首先被处理的文本没问题,因为我们在vim里看了。那就有可能是他写的脚本有问题。分析来分析去,发现最有可能是awk的print出问题。可是我们在shell交互模式下无法重现这个问题!无奈之下,把awk改成等价的head+tail+cut,可问题仍然存在!!

实在是无语了,思考了一段时间后,决定无论我们看到什么也要确定是文件本身有问题!可是究竟怎么才能直接证明文件有问题呢? vim看得好好的,没有证据啊!正在山穷水尽之际,董溥同学突然看到了vim在刚打开一个文件时闪现的几个字符!如下图所示(下图是我为写本文人工制造的):

这样就什么问题都弄清楚了!!原来vim把那些每行都以’r’结尾的文件直接识别成dos格式!直接用dos2unix一转换就可以了!

看起来挺多时候眼见不一定就为实。。。

这不是gcc的bug

一些人说,gcc可以安静地编译下面的程序:


void foo(void *p)
{
++p;
}

所以这是gcc的一个bug。很不幸,这一个feature,而不是bug。gcc手册中明显地提到:

In GNU C, addition and subtraction operations are supported on pointers
to void' and on pointers to functions. This is done by treating the size of avoid’ or of a function as 1.
如果你对此不满,那就打开-Wpointer-arith。(不过话说回来了,-Wall -W居然都不会打开这个选项~~)而Intel C编译器就可以给出警告:


foo.c(3): warning #1338: arithmetic on pointer to void or function type
++p;
^

当然了,标准肯定是不允许对void*进行算术操作,原因很简单,因为它指向的对象大小不知道。参见C99 6.5.6。

一个shell技巧

有时可能要碰到分解$PATH,那就得想法处理它的“:”分隔符。

首先想到的应该是用gawk,如下:

$ echo $PATH | gawk -F: -v OFS=”n” -v num=0 ‘{NF-=num; $1=$1; print}’

恩,这样其实挺麻烦的。有没有更好的技巧呢?当然有,看下面这个:

$ OLDIFS=$IFS; IFS=: ; printf “%sn” $PATH; IFS=$OLDIFS;

这个技巧在ksh下还可以这样用:

$ IFS=”:”; set -A array $PATH; for eachone in ${array[*]}; do echo $eachone; done

八皇后问题的非递归解法

用C++实现了八皇后问题的非递归算法。原理很简单,看代码就是了,无须多说。

BTW: C++的algorithm就是好用啊!

include

include

include

include

using namespace std;
const int MAX = 8;

vector board(MAX);

void show_result()
{
for(size_t i = 0; i < board.size(); i++)
cout<<"("<<i<<","<<board[i]<<")";
cout<<endl;
}

int check_cross()
{
for(size_t i = 0; i < board.size()-1; i++)
{
for(size_t j = i+1; j < board.size(); j++)
{
if((j-i) == (size_t)abs(board[i]-board[j]))
return 1;
}
}
return 0;
}

void put_chess()
{
while(next_permutation(board.begin(), board.end()))
{
if(!check_cross())
{
show_result();
}
}
}

int main()
{
for(size_t i =0; i < board.size(); i++)
board[i] = i;
put_chess();
return 0;
}

TCP Closure

今天在看TCP的释放时遇到几个问题,查了一下《TCP/IP详解 卷一》和RFC793,也请教了一下Herbert Xu,基本上都解决了。在这里总结一下。

TCP的释放过程如下图所示(取自《TCP/IP详解 卷一》):

问题1:既然TCP的CLOSE是单工的,那么,一方在收到FIN的ACK之后可能会过很长一段时间才收到对方的FIN。那么如果这个时间里这方已经关闭该进程,谁又来ACK对方的FIN呢?

Herbert解释到:

For you particular scenario, the key is that the TCP socket is
maintained by the OS, not the process.

So even after your process has called close(2) and exited, the
OS will continue to respond in ways required by RFC793 until
such a time when the socket has been CLOSEed in the sense of the
protocol.
问题2:两方同时发送FIN又如何呢?

没想到这个问题连RFC里都有,里面这样解释:

A simultaneous CLOSE by users at both ends of a connection causes
FIN segments to be exchanged. When all segments preceding the FINs
have been processed and acknowledged, each TCP can ACK the FIN it
has received. Both will, upon receiving these ACKs, delete the
connection.
也就是像下图所示(也是取自TCP/IP详解):

问题3:关于MSL。

MSL是maximum segment lifetime的缩写。TCP状态机里有个TIME_WAIT状态,设置它是为了防止最后一个ACK丢失,这个时间一般就是2MSL。而且,在这个2MSL的时间中,这个socket是不能被重用的,除非我们setsockopt(…,SO_REUSEADDR,…)。

shebang

在Unix中,shebang其实就是指“#!”,它取自#(SHArp)和!(bang)。

它是很多脚本文件中第一行的前两个字符,用来告诉Unix系统要用shebang后面指定的解释器
来解释该脚本。所以,在很多脚本中,第一行往往都是这么写的:

! /abs/path/to/interpreter

根据wikipedia上的解释),shebang最初由
Dennis Ritchie引入的,时间大概是在Version 7和Version 8之间。也正是因为shebang是以#
开头,所以很多Unix上的脚本都是用#作为注释的开始。常见的有:

!/bin/sh

!/usr/bin/perl -w

!/bin/awk -f

!/usr/bin/env python

!/usr/bin/ruby

shebang没有你想象得这么简单。首先,它后面的解释器路径一般来说必须是绝对路径。(当然了,
有人也说像#!python这样的也能执行。)如果不是,可能就会出现类似的错误:


bash: ./foo.py: python: bad interpreter: No such file or directory

因为shell会直接用execve去执行这个文件,出错马上就退出。而execve会通过shebang辨认出这个一
个脚本文件,然后尝试用后面的解释器去执行,但它并没有在PATH中寻找解释器,而是完全依靠
你给出的路径。如果你不想指定绝对路径或者出于可移植的原因不好指定,那么你应该试试用env(1)
,就上面的python的shebang一样,它会帮你在PATH中搜索。python之所以更倾向于这个还有个原因,
就是env一般固定在/usr/bin目录下,而python的安装位置就相对不那么固定。

用env时你应该注意这么一个事实:传递给解释器的argv和你想象得并不一样。下面这个就是不对的:

!/usr/bin/env perl -w

shell会提示:/usr/bin/env: perl -w: No such file or directory
错误的根源就是perl -w被当成了整体传递给env。

用下面的程序来看一下参数传递过程:


/test.c/

include <stdio.h>

int main(int argc, char** argv) {
int i;
for (i=0; i<argc; i++)
fprintf(stdout, “argv[%d]: “%s”n”, i, argv[i]);
return 0;
}
然后把编译出的test当作解释器:


$ cat invoker.sh

!/home/wangcong/test -1 -2 -3

结果如下:


$ ./invoker.sh a b c
argv[0]: “/home/wangcong/test”
argv[1]: “-1 -2 -3”
argv[2]: “./invoker.sh”
argv[3]: “a”
argv[4]: “b”
argv[5]: “c”

当然了,并不是所有的Unix都是这样,但最起码Linux上的bash和zsh上就是如此。所以,要编写可移植
的脚本,你应该当心这一点!