读K&R之变量、求值顺序和副作用

Dennis M. Ritchie

直到不久前,我都不曾认真地了解过C。大学课堂上学习过C++,在那段不长的时间里,学到的东西一直支撑着我对C的认知。等工作五年后,我重新学习C时,发现自己好生浅薄。就在自己还在读《The C Programming Language》的第二章时,网络上传来了Dennis M. Ritchie过世的消息。大师已逝,他所留下的知识将会继续惠泽着后人。我把阅读《The C Programming Language》所作的笔记整理出来,希望更多的人能够学习到Dennis M. Ritchie所建立的知识,这也算是对大师的一种纪念。

关于变量

对变量的命名与符号常量的命名存在一些限制条件,名字是由字母和数字组成的序列,但其第一个字符必须是字母。下划线“_”被看作是字母,通常,变量名使用小写字母,符号常量名全部使用大写字母。

对于内部名而言,至少前31个字符是有效的。函数名和外部变量名包含的字符数目可能少于31,这是因为汇编程序和加载程序可能会使用这些外部名,而语言本身是无法控制加载程序和汇编程序。对于外部名,ANSI标准仅保证前6个字符的唯一性,并且不区分大小写。

任何变量的声明都可以使用const限定符限定。该限定符指定变量的值不能被修改。对于数组而言,const限定符指定数组所有元素的值都不能被修改。

默认情况下,外部变量和静态变量都被初始化为0。未经显式初始化的自动化变量的值为未定义值(即无效值)。

形容词external与internal是相对的,internal用于描述定义在函数内部的函数参数和变量,外部变量定义在函数之外,因此可以在许多函数中使用。函数本身是“外部的”。

外部变量的用途表现在它们与内部变量相比具有更大的作用域和更长的生存期。过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清,–外部变量的值可能被意外地或不经意地需该,而程序的修改又变得十分困难。

如果要在外部变量的定义之前使用该变量,或者外部变量的定义和变量的使用不在同一个源文件中,则必须在相应的变量声明中强制地使用关键字extern。

在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其他文件可以通过extern声明来使用它。外部变量的定义中必须指定数组的长度,但extern声明则不一定要指定数组的长度。外部变量的初始化只能出现在其定义中。

关于求值顺序

C语言没有指定同一运算符中多个操作数的计算顺序(&&、||、?:和,运算符除外),也没有指定函数各参数的求值顺序。有解释称是为了灵活,但我觉得这是一种混乱,不同编译系统对此作出不同的解读,导致了结果差异,这对移植性是一种破坏。

起初,我并没有意识到这个问题会成为“问题”,直到我看到CSDN里有人贴出《C测试,看看你的C语言过关了没》。对于那个问题,我依照Java的规则进行回答,和GCC实际结果正相反,这才让我重视起这个问题。

相较之下,The Java Language Specification 给出了清晰的定义:

The Java programming language guarantees that the operands of operators appear to be evaluated in a specific evaluation order, namely, from left to right.1

这条规则可以继续细化成四条子规则,由此可以清楚,在Java中同一运算符多个操作数,函数中多个参数,均遵照从左到右的计算顺序:

  1. Evaluate Left-Hand Operand First
  2. Evaluate Operands before Operation
  3. Evaluation Respects Parentheses and Precedence
  4. Argument Lists are Evaluated Left-to-Right

关于副作用

函数调用、嵌套赋值语句、自增与自减运算符都有可能产生“副作用”——在对表达式求值的同时,修改了某些变量的值。

ISO/IEC 9899:1999中可以看到如下描述:

Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluation shall be complete and no side effects of subsequent evaluations shall have taken place.2

K&R在第二章的末尾给出一个示例(如下),并且说ANSI C标准明确规定了所有对参数的副作用都必须在函数调用之前生效,但这对前面介绍的printf函数调用没有什么帮助

printf("%d, %dn", ++n, power(2, n)); /* 错 */

但是,我不理解这里为何对前面介绍的printf函数调用没有什么帮助。特意查了ISO/IEC 9899:1999对自增操作符副作用的解释,还是不明白。

The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point.

这个疑问就暂时留下吧,先记着 MISRA-C 对自增、自减运算符的要求吧:

The increment (++) and decrement (–) should not be mixed with other operators in an expression.3

关于计算顺序和副作用的话题,我分别看到了Java和C两个忠告,作为本篇博文的结尾:

It is recommended that code not rely crucially on this specification. Code is usually clearer when each expression contains at most one side effect, as its outermost operation, and when code does not depend on exactly which exception arises as a consequence of the left-to-right evaluation of expressions.1

在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。4


参考文献

  1. James Gosing, Bill Joy, Guy Steele, Gilad Bracha. The Java Language Specification (third edition)[M]. Addison-Wesley, 2005.5:414
  2. ISO/IEC 9899:2000 (E) — Programming Language — C, 13
  3. MISRA-C:2004 Guidelines for the use of the C language in critical systems, 2004.10:56
  4. Brian Kernighan, Dennis Ritchie. C语言程序设计(第2版)[M]. 徐宝文, 李志, 译. 北京:机械工业出版社, 2004.1:43

Leave a comment

Your comment