8

C语言基本功修炼秘籍第2节,算符优先级只是“部分”优先?序列点能够带来哪些好处?为何...

 3 years ago
source link: https://blog.popkx.com/c%E8%AF%AD%E8%A8%80%E5%9F%BA%E6%9C%AC%E5%8A%9F%E4%BF%AE%E7%82%BC%E7%A7%98%E7%B1%8D%E7%AC%AC2%E8%8A%82-%E7%AE%97%E7%AC%A6%E4%BC%98%E5%85%88%E7%BA%A7%E5%8F%AA%E6%98%AF%E9%83%A8%E5%88%86/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一节以一段简单的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;
}

c26cd2df05fd30463ceba5edd93c4a68.png
这段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节。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK