最近读k&r c和其他C语言书籍的一些感想:

C是工程师在开发现场基于具体的开发目标而发明的语言。
它强大有力,赋予程序员非常大的权力和自由,但并不强调优美,统一,安全,健壮等在年轻的后进语言宣传语中经常听到的概念。

C的指针基本沿袭自B。在B语言中,指针和普通的整型没有本质区别,因而指针和整型的加减运算十分自然合理;然而在C语言中,引入了很多数据类型,情况变得复杂得多,指针的运算显得复杂晦涩,给初学者造成了许多障碍。
将C的指针简单地理解成内存地址并不十分合适,更恰当的理解应该是:指针的值为一个内存地址,而指针类型则可以表明指针所指向的内存块的大小,对指针+1操作,地址的增量并非1而取决于指针指向的引用类型的大小。

C语言的语法非常不统一。以声明语法为例,初学者往往会误以为其遵循变量类型 变量名;这样的形式,如int a_num;
但对指针类型的声明(形如int *p;)并未遵从此形式:int*不是变量类型,倘若它是,那么int* p1, p2;就不至于导致歧义。
对此我的理解是,声明中出现的操作符并不具有与表达式中等同的语义,仅仅用来构成一定形式以表示某种数据类型。
例如int arr[5];声明了一个包含5个元素的整型数组,而在表达式中arr[5]意为*(arr+5)(或者也可以表示为*(&arr[0]+5)),两者语义相差甚远。

滥用C语言提供了一些语言特性很可能降低程序的可读性,甚至引入一些难以调试的隐性 bug。
例如老生常谈的自增自减运算符问题:
对变量增1减1确实是个使用场合非常多的操作,但++--操作隐藏了赋值操作的副作用,如果表达式和语句中涉及对此变量的多处引用,则不适宜使用此类操作符。
为了追求短小紧凑,C程序经常试图将多个表达式/语句表示的语义压缩到一个表达式中,但C标准并没有严格定义表达式的求值顺序(只规定了操作符的求值顺序,以及副作用必须在某些语义结束点前发生,但语义结束点前副作用的顺序没有严格定义,这样编译器能获得更大的自由,从而在理论上可能进行更好的性能优化),因此i+++i++这类表达式的值属于未定义值,具体是多少有赖于编译器的实现。
k&r c中专门强调了这个问题:

The moral is that writing code that depends on order of evaluation is a bad programming practice in any language.

即使程序的语义不依赖某个未定义数值,依然不推荐这样的写法,因为会大大增加维护的困难。

C程序有时会鼓励一些略显hack的写法。
例如k&r c的例程:

1
2
3
4
5
6
7
/*
This loop prints n elements of an array, 10 per line,
with each column separated by one blank,
and with each line (including the last) terminated by a newline.
*/
for (i = 0; i < n; i++)
printf("%6d%c", a[i], (i%10==9 || i==n-1) ? '\n' : '');

如果不借助注释很难一眼看懂代码(可能是我太弱:))。
考虑到C语言诞生的年代:电脑性能还十分低下,内存尚且非常昂贵,这种做法并不足以为奇。在单行代码中使用不是那么一目了然的紧凑写法其实并不会导致额外的维护工作,但却需要程序员之间建立一些不言而喻的约定俗成。
这种哲学如果推及其他语言并不一定合乎时宜,但以性能和紧凑为追求的C语言有充分的理由坚持。

总的来说,由于历史原因,C语言是一门不怎么严格的语言,对程序员提出了比较高的要求。C语言许多能算得上问题的问题,经常是解决具体问题时选择的折中和权衡。诚然,你需要克服许多看起来不是很必要的困难,但你因此变得更强大:)。