C语言作为一种主流程序设计语言,许多编程语言如Java、C++、C#都借鉴了它的语法。C语言也是一种很适当的程序设计入门的教学语言,国内大专院校的许多专业都开设了这门课程并且大多将其作为第一门程序设计语言课程,同时C语言也是全国计算机等级考试二级的内容。因此,C语言的教学质量对学生的后续学习和取得等级证书很重要,尤其是对学生在实际工作中更好地了解和使用计算机有非常重要的意义。
目前,国内绝大多数教材在介绍C语言表达式时,都没有介绍C语言的一个重要概念——序列点[1]。序列点概念的缺失使得读者只能记忆而不是遵循求值规则分析有序列点表达式的求值顺序。以简单的逗号表达式a = 3, ++a为例,设变量a为整型变量(下面表达式中出现的变量也默认为整型)。“逗号表达式自左向右依次求值”,故先对子表达式a = 3求值,再对子表达式++a求值。在实际教学中善于质疑的学生往往会问:表达式中自增操作符++的优先级最高,为何要先求子表达式a=3的值?表达式求值时不是先算高优先级的操作符吗?表达式究竟有没有求值原则呢?这不仅使得学生对C语言知识的认知残缺不全,而且也影响了学生自主学习的积极性,不利于创新型人才的培养。
一、序列点的定义及分析
根据C语言标准[2],序列点就是执行序列中的一些特定点,在这些点上,前面求值的副效应(side effect)应彻底完成且其后求值的副效应均未发生。在教材中照搬标准让初学者学习理解序列点是不明智的,应直接向初学者指出序列点在表达式求值中起的作用。含有序列点的表达式求值时要保证有序列点的操作符左边的由子表达式构成的操作数先于其右边的操作数求值[3]。C语言表达式求值的原则为:先考虑序列点,再根据操作符的优先级和结合性求值。
操作数是指操作符的操作对象,如表达式3+2中加法操作符左边的操作数为3,右边的操作数为2。在复杂的表达式中,需结合操作符的优先级和结合性来确定某操作符的操作数。对于表达式3+2*5,加法操作符左边的操作数为3,但其右边的操作数不为2,进行加法运算时3显然不可能和2相加。因为与加法操作符右边相邻的操作符为乘法操作符,其优先级较高,所以加法操作符右边的操作数为2*5(的积)。而乘法操作符左边的操作数为2,右边的操作数为5。对于表达式3+2-5,加法操作符右边相邻的操作符为减法操作符,两者优先级相同,但结合性为左结合,故它右边的操作数为2。
表达式a = 0 && ++a中逻辑与左边的操作数为0,右边的操作数为子表达式++a,整个表达式为赋值表达式;而表达式(a = 0) && ++a中逻辑与左边的操作数为子表达式(a = 0),右边的操作数为子表达式++a,整个表达式为逻辑表达式。逻辑与操作符有序列点,因此表达式(a = 0) && ++a求值时,虽然自增操作符的优先级最高,但求值时首先考虑序列点,逻辑与操作符左边的操作数(a = 0)需先于其右边的操作数++a求值。
二、C语言中部分操作符需要序列点的原因
逗号操作符(,)有序列点。逗号操作符多用于把多条语句变成一条语句,如a = 2;和++a;为两条语句,而a = 2, ++a;是一条语句。语句a = 2, ++a;执行时,如果逗号操作符没有序列点,子表达式++a就会先执行,即这条语句的执行顺序与上面两条语句的并不相同。基于逗号操作符的作用,逗号操作符只能优先级最低,且含有序列点。
逻辑与操作符(&&)有序列点。C语言中逻辑与操作符实行“短路计算”,即当其左边的操作数值为0即假时,不对右边的操作数求值而直接把0(假)作为求值的最终结果。如果逻辑与操作符没有序列点,表达式3>5 && ++a求值时,自增操作符的优先级最高,子表达式++a应先求值。逻辑与操作符左边的操作数3>5求值的结果为0,即假,根据短路计算,作为逻辑与右边的操作数,子表达式++a不会被求值。显然两者矛盾。为了短路计算,逻辑与操作符&&需要序列点。有了序列点之后,求值时序列点左边的操作数将先于其右边的操作数求值,即子表达式3>5先求值,由于短路计算,整个表达式的值为0,即假,且右操作数子表达式++a不会被求值。
逻辑或操作符(||)有序列点。C语言中逻辑或操作符也实行“短路计算”,因此,其有序列点的必要性与逻辑与操作符的相同。
条件操作符?:的问号处?有序列点。条件操作符常用于改写简单的if-else选择结构,如下面的语句
if(a > b)
++a;
else
++b;
可用条件操作符改写为a > b ? ++a : ++b;。
如果条件操作符没有序列点,语句a > b ? ++a : ++b;执行时,++a和++b会先于子表达式a > b执行,这样的执行顺序显然与if-else选择结构的不同,因此,条件操作符?:的问号?处有序列点。语句a > b ? ++a : ++b;执行时,问号处?左边的操作数a > b先执行,值为真时,对++a求值,不对++b求值;值为假时,反之。
三、含有序列点的表达式的求值分析
设整型变量a的值为0。对于表达式'a' || (a = 1) && (a += 2),逻辑或||左边的操作数为'a',右边的操作数为(a = 1) && (a += 2);逻辑与&&左边的操作数为(a = 1),右边的操作数为(a += 2)。求值时首先考虑序列点,逻辑或左边的操作数'a'的值非“0”为真,执行短路计算,其右边的操作数不会被求值,因此,整个表达式的值为1,即真。在表达式求值的过程中,变量a的值没有改变。
对于表达式(a = 0) && (a = 5) || (a += 1),逻辑与&&左边的操作数为(a = 0),右边的操作数为(a = 5);逻辑或||左边的操作数为(a = 0) && (a = 5),右边的操作数为 (a += 1)。求值时首先考虑序列点,逻辑与&&左边的操作数(a = 0)先求值,求值时一方面变量a的值会变成0,另一方面这个子表达式的值为0即假,执行短路计算,其右边的操作数(a = 5)不会被求值,因此,原表达式变为0 || (a += 1)。逻辑或||左边的操作数值为0,即假,不能短路计算,只能再求其右边操作数的值。子表达式(a += 1)求值时一方面变量a的值由0变成1,另一方面这个子表达式的值为1即真,因此,原表达式的值为1,即真。表达式的求值完成后,变量a的值变为1。
特别强调,用赋值表达式作为逻辑操作符的操作数仅仅为了清晰地表明在表达式求值过程中某个子表达式是否被求值,这类表达式的实用性和可读性非常差,实际编程时最好不要用赋值表达式做操作数。
序列点不仅可以使低优先级的操作符先于高优先级的操作符求值,而且也可以影响操作符的结合性。表达式a > b ? ++a : c > d ? ++c : ++d有两个条件操作符,条件操作符为右结合,因此,原表达式的求值顺序应为(a > b ? ++a : (c > d ? ++c : ++d))。左边条件操作符的3个操作数分别为子表达式a > b,子表达式++a和子表达式c > d ? ++c : ++d。按照结合性,子表达式(c > d ? ++c : ++d)应先求值,但是,由于左边条件操作符的问号处?有序列点,故子表达式a >b先求值,如果其为真,则只会对子表达式++a求值,而不会再对子表达式c > d ? ++c : ++d求值了。显然,此表达式中条件操作符的右结合只用于确认左边条件操作符的右操作数,而没有使得右边条件操作符先求值。
四、结束语
虽然C语言表达式种类繁多,但有着相对明确的表达式求值规则,根据求值规则正确分析出表达式的求值顺序有助于激发学生学习的主动性。在教材中照搬C语言标准中序列点的概念势必会增加学习的难度,给初学者提供一个准确且易于理解的概念,不仅不会造成C语言知识点的残缺,而且也为启发式教学提供了可能。