C语言基本功修炼秘籍第2节,算符优先级只是“部分”优先?序列点能够带来哪些好处?为何说C语言程序员只有基本功扎实,才能写出紧凑简洁的代码?

上一节以一段简单的C语言程序为切入点,讨论了C语言中“序列点(sequence point)”的概念,在C语言程序开发中,如果不能明确这一点,很难写出可移植的C语言代码。

大多数情况下,C语言程序中的语句执行顺序都是确定的,各条语句产生的副作用的顺序也是确定的。相信读者应该明白,在C语言中,括号运算符“()”有时是能够改变表达式的计算顺序的,例如:

a = 1 + 2 * 3;
b = (1+2) * 3;

因为乘法运算的优先级比加法高,所以程序在处理 a 的值时,优先计算 2* 3,然后才会处理与 1 的和。对于 b,因为 1+2 被括号包围,所以会得到优先处理。

算符优先级只是“部分”优先

那么现在又有一个问题:假设一段C语言程序中,各条语句的副作用完成顺序已经确定(如果读者对这句话感到费解,可以先看看上一节),我能够使用括号运算符“()”改变这一顺序吗?

这个问题其实可以进一步延伸为:C语言程序中的各条语句的副作用完成顺序,能够通过运算的优先级改变吗?

只能说大部分情况如此,严格来说,运算优先级和括号运算符只能部分的影响表达式的处理顺序。请看下面这个表达式:

f() + g() * h()

我们都知道乘法运算会发生在加法运算之前,但是,我们不知道这三个函数 f(), g(), h() 哪一个会先被调用。换句话说,乘法运算相对于加法运算的优先级只是部分特定的计算顺序,这里强调“部分”这个词,是指优先级并不包括“操作数(f(), g(), h() 本身)”的计算。

这样的情况下,即使我们使用了括号运算符“()”改变上述C语言表达式的计算顺序,如下:

(f()+g()) * h()

但是括号运算符“()”也仅仅是告诉C语言编译器,优先处理哪些操作数与哪些运算符,至于操作数本身,括号运算符就管不到了,若考虑不同平台的编译器,f(), g(), h() 哪一个会先被调用依然是不能确定的。

再来看一个问题

了解了算符优先级只是“部分”优先这一概念后,解答下面这个问题就不难了,请看相关C语言代码:

#include <stdio.h>

int main()
{
    int i = 2;
    printf("%d\n", i++ * i++);

    return 0;
}


这段C语言代码虽然简单,但是却引发过争议。有人编译这段C语言代码并执行得到的结果是 4,有的则得到结果 6,这是怎么回事?是C语言不可靠吗?

其实不是的,我们把这里的两个 “i++”分别看作是上面分析的 f() 和 g()。相应的, 将 i++ * i++看作是 f() * g()。按照上面的讨论,若考虑不同平台C语言编译器,程序在处理 f() * g() 时,其实是不能确定 f() 和 g() 哪个先执行的,事实上,程序甚至都不能确定在执行 f() * g() 时, f() 和 g() 本身有没有被执行。

现在就明白了,对于C语言表达式 i++ * i++ ,不同的编译器处理结果是不同的。有的编译器会在处理乘法运算之前,先完成 i++表达式的副作用,有的编译器则不会。因此,编译并执行上面这段C语言程序,得到结果 4 和 6 都是允许出现的结果。

这时使用括号运算符:(i++) * (i++) 是完全没有任何意义的,因为 ++ 运算符本身优先级就已经高于乘法运算了。

因此,如果希望写出顺序确定的C语言代码,i++ * i++ 这种风格就不推荐了,应该使用显式的临时变量,或者单独的语句,例如下面这段C语言代码就不会再产生不同结果了:

a = i++;
b = i++;
c = a*b;

C语言程序中序列点的便捷之处

也许有读者认为C语言程序中的序列点让C语言编程变得难以捉摸,其实不是的,细究起来,几乎所有现代高级编程语言都会有“序列点”的概念,只不过可能名字不同而已。

另外,有经验的C语言程序员反而觉得序列点让C语言编程更加准确和便捷,事实上,如果能够确认序列点,写出精简的C语言代码就不难了,例如下面这段C语言代码:

if(d != 0 && n / d > 0){
    /* ... */
}

上一节提到C语言逻辑运算符 && 和 || 产生序列点,因此在上述 if() 条件表达式中,程序必定先处理 d!=0,n/d>0 只有在 d!=0 的情况才会执行,因此读者不必担心 n/d 会出现 0 做除数的情况。

读者可以思考一下,为什么 d 等于 0时,n/d>0 不会被执行。

如果 d 等于 0,则 n/d>0 就不会再被执行,这看起来很像物理电路里的“短路”,所以有程序员习惯称这种现象为C语言程序的“短路行为”。事实上,上述C语言代码和下面这段代码是等价的:

if(d!=0){
    if(n/d>0){
        /* ... */
    }
}

不过,前面那种写法显然要简洁和精简许多。类似的,再来看下面这段C语言代码:

if(p == NULL || *p == '\0'){
    statements
}

有使用C语言指针经验的读者应该都明白,对于废弃或者还未使用的指针,最好让其指向 NULL,这样在之后的使用中,可以通过其是否指向 NULL 判断指针是否有效。

就本例而言,要访问指针 p 指向的数据,首先应该判断其是否空指针,若是,就不应该在访问了,否则基本会引发段错误。上述C语言代码中,|| 运算符引入了序列点,因此程序会先处理 p==NULL,如果发现成立,那么 * p=='\0' 就不会再被执行了(读者自行思考原因)。这段C语言代码和下面是等价的:

if(p == NULL){
    statements
}
if(p!=NULL){
    if(*p == '\0'){
        statements
    }
}

不过这样写显然繁琐许多。

小结

本节通过几个实例,进一步讨论了C语言程序中语句的执行顺序。可见,要写出稳定可移植的C语言代码,没有扎实的基本功是不行的。本节在最后还讨论了基于逻辑运算符 && ,|| 结合序列点的编程技巧,可见,基本功扎实的C语言程序员总是能够写出紧凑简洁的代码。

本节是我的专栏《C语言程序开发基本功修炼秘籍》第2节。

阅读更多:   C语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif