3.5 语句的概念

3.5.1 关于语句的闲话

什么是“语句”(Statement)?C89、C99标准给出的定义都是“语句是对待执行的动作的具体说明(A statement specifies an action to be performed)”。这个说法对于初学者来说是相当抽象的也是非常含糊的。

所谓C语言的语句,具有下面一些含义和特征。

(1)语句是根据C语言语法对“单词”(见1.4.2)和“词组”(见3.2)进行的特定的连接,且有确切的含义,能完成一特定的动作。例如:

3.5 语句的概念 - 图1

这里3、2是int类型的常量,+是一个运算符,它们都是C语言里的单词,共同构成了一个表达式,这个表达式要求CPU计算3+2的值,这个动作应该在“;”之前完成。这就是语句“对待执行行为的具体说明”。当然,这里这个行为除了浪费一些CPU的时间、增加了程序的长度之外,对你的程序没有任何实质作用(足够聪明的编译器对这样的语句是不予理睬的,但你不可以对编译器的聪明程度做事先的假设,更不可以依赖于这种聪明。编译器的本分是忠诚,不是聪明)。

(2)所谓的一个行为(An Action)本质上是特定一组计算机(CPU)指令的集合:

3.5 语句的概念 - 图2

从CPU的层面看,这是两个赋值求值和一个加法求值动作,这些动作应该在“;”之前完成。

(3)在一定程度上,语句描述或规定了一系列计算机的动作的执行顺序,这种确定性体现在语句结束时(;)所有规定动作(包括副效应)必须完成。而一个作用(An Action)里各个动作的直正的次序往往是无法得知的,这是编译器的事情。所以在:

3.5 语句的概念 - 图3

这样的语句中,我们不可能知道计算机先计算的i=3还是j=2,先计算哪个表达式是编译器的自由。这给我们的启示就是,不要写和运算次序相关的代码,这是靠不住的。甚至完全可以说是一种错误的代码,因为它没有唯一确定的含义。

(4)在形式上,C语言的语句总是以“;”或“}”作为结束标志的,这种标志的本质意义在于,计算机执行至“;”或“}”时,前面所要求的CPU动作必须全部完成。

(5)还是老话,C语言一词多用。仅仅从结尾是否是“;”或“}”是无法判断出一个C代码的语法成分是否是语句的。

(6)变量定义不算是语句。虽然变量定义是以“;”作为结束标志的。

3.5 语句的概念 - 图4

说的是让编译器预备一块内存,这块内存被命名为i,代码中后面一定范围内出现的i就是指的这块内存中的值或这块内存本身,这块内存应该存储一个int类型的值,并且可以参加int类型被容许参加的运算,这就是这个变量定义的全部含义。由于这个事儿是和编译器说的,不是让CPU完成的动作,所以C语言不把这个东西归为语句。

(7)main()函数的那一对如影随形的{}不是语句,而是main()函数定义的一部分。所谓main()函数定义,无非是向编译器说明这个函数是什么样的该干什么而已。所以整个main()函数无非是向编译器所做的一个倾述。所有这种向编译器说明情况的述说,在C语言中都不属于语句而属于所谓的“说明”(Declaration)。尽管在main()中也包含语句,但在整体上main()就是一个关于main()函数的说明和描述。写程序就是写main()函数,代码其他旁支末节的部分,无非是对这个说明的一个补充说明而已(我就是这么看的)。

(8)C语言所有的语句都必须写在某个{}之内,不允许跑到{}之外的语句。

(9)“说明”可以写在{}之内,也可以写在{}之外。但我奉劝你不要草率地把“说明”写在{}以外。

(10)写在{}以内的“说明”仅在这个{}之内有效,但在这个{}之内的另一个{}之内可能有效,也可能无效。

(11)在{}之内,C99以前的规定是先把对编译器说的事儿说完,再说让CPU做的事儿。也就是说要先写声明,再写语句。但在支持C99的编译器中没有这个限制,你只要把语句中出现的标识符事先向编译器说清楚了就可以,这也就是为什么变量在使用之前先需要定义的原因。

(12)C语言的有些语句之中可以包含其他语句,这从另一个角度显示了C语言强大的表达思想的能力。

(13)有些语句是由一些特定的关键字单词组成的特定的“句型”,其中往往包含一系列的动作,在学习时要无论在语法形式上还是在执行次序的语意上都作到忠实准确地理解。这主要指后面即将学到的控制语句。

(14)在代码中除非特别说明,语句依照出现的次序一句一句地执行。

关于语句的闲话就说到这里,下面介绍几种简单的语句。

3.5.2 空语句有两种

最简单的语句是空语句(Null-statement),仅仅有一个语句结束标志“;”组成。麻雀虽小,五脏俱全。作为一个语句,空语句其实什么必要的成分也不缺。这里启示我们的是所有的语句绝对不可以缺少结束标志。

空语句只出现在语法上要求有一个语句但又要求这个语句什么CPU动作也不执行的地方。

实践中实际上还有另外一种以“}”作为结束标志的空语句——{}。C语言委员会的专家们没有把这个语句列为空语句而是把这个语句归为“块”(block)的一种,可能是因为这种空着的{}不仅可以作为空语句使用,还可以有其他的用途(main()后面的{}就可以是完全空着的,尽管这样做很不好)。

实际上这个空着的块可以和“;”起完全一样的作用,而且在实践中往往用的比“;”还可能多些。

3.5.3 表达式语句

一个完整的表达式在尾部加上“;”就构成了一个表达式语句。语法上就是:

表达式;

可以这么说,表达式规定了若干CPU应该完成的动作,而“;”规定了这些动作必须完成的最后期限。所以:

3.5 语句的概念 - 图5

是一个赋值表达式,而:

3.5 语句的概念 - 图6

则是一个赋值语句。

从表达式出发不能完全确定运算的次序,而语句的结束标志则只规定了一个动作完成最后的期限。这种规定最后期限的语法标志在C语言中叫做“序点”(sequence point)。尽管在C语言中有序点这种概念,但对所有的表达式或语句完全确定次序依然是不可能的(我看到过太多的初学者纠结在这个无意义的问题上了,他们像是在发明永动机一样孜孜不倦却身陷泥潭止步不前而毫无自知,全然忘记了C语言是为了解决问题而不是为了自寻烦恼而创造的)。

函数调用也是一个表达式:

printf(“函数调用(表达式)语句\n”);

就是一个表达式语句,俗称函数调用语句。

所以会写C语句的前提是知道什么是表达式,不了解什么是表达式就不可能写出C语句,至少写不出好的语句。这道理就和不会组词就不可能很好地造句一样。

3.5.4 顺序结构

迄今为止,程序代码中所用到的语句都是按照由上到下的次序依次执行的,这种程序结构叫做顺序结构。顺序结构的流程图如图3-2所示。

3.5 语句的概念 - 图7

图3-2 顺序结构的流程图

3.5.5 复合语句

1.“块”的概念

C语言中,被{}括起来的东西通常叫“块”(block)。这个东西有点像自然语言中的自然段,可以跟在许多“标题”后面。跟在main()后面的时候,人们更喜欢称之为“函数体”。块可以作为函数体。

块还有其他许多用法,其中的一种就是作为复合语句(Compound Statement)。这也就是说,不是所有的块都是复合语句。

2.复合语句

块作为复合语句的条件有两个。一是出现在容许语句出现的位置(必须在某个函数体之内);另外内部需要由可选的声明部分和可选的语句部分组成。在语法上{}是必须的。下面是复合语句的一个例子,尽管它也可以作为一个不太合格的main()函数体。

3.5 语句的概念 - 图8

再次说明一下,这个复合语句中定义的变量i,仅仅在这个复合语句内部可以使用。在这个复合语句外部使用变量i,要么需要重新定义另一个i,要么是就是非法的。

语句块中的声明或定义和语句的顺序没有限制,但早期的规定是声明或定义应该在语句之前。所以如果编译器没有支持C99的话,应该在“{”后面先写定义或声明,然后再写语句。本书中的代码遵循先写声明和定义的方式,但这并不总是必须的。对于支持C99的编译器,只要在标识符在使用之前被定义或声明过就可以了,可以不一定在语句之前写。

复合语句在语法上本身已经具有语句的地位和意义,因此在复合语句的“}”后面不要画蛇添足地加上“;”,那样的写法,在编译器看来是两条语句,表示的是在复合语句后面还有一个空语句,这很重要,很多初学者容易忽视这一点。

复合语句最频繁最主要的用法是在后面的控制语句中,但在目下可以用来把代码划分成若干“逻辑模块”。尽管这种用法在语法不那么正规(后面会有更好的办法),但对于编程素养的养成是有益的。所以,尽量使用复合语句,哪怕这有点小米加步枪的味道。