ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C语言Python:表达式的求值顺序(evaluation order)的差异

C语言Python:表达式的求值顺序(evaluation order)的差异

相关阅读

Pythonhttps://blog.csdn.net/weixin_45791458/category_12403403.html?spm=1001.2014.3001.5482

C语言https://blog.csdn.net/weixin_45791458/category_12423166.html?spm=1001.2014.3001.5482


C语言中表达式的求值

C语言针对表达式的计算,设置了操作符的优先级和结合性这两个特性,优先级用于解析不同优先级的符号,结合性用于解析相同优先级的符号。但是这两个特性并不能完全确定表达式的求值顺序(至少对于大多数类型的操作符),这就给编译器留下了一定的优化的空间,下面举例说明这一点。假设有如下所示的简单表达式。

例1 1 + 2 + 3

C语言编译器在语法分析时会构建一个语法分析树,类似图1所示的二叉树结构。

图1 语法分析树

在图1中,1+2整体作为一个子表达式成为了+操作符的左操作数,这是操作符的结合性导致的结果。这个语法分析树保证了,3和1+2会在根节点"+"前求值,1和2会在"+"的左子节点"+"前求值。但是,这并没有保证3和1+2的求值顺序,也没有保证1和2的求值顺序,具体地来说编译器可能选择先对3求值,随后对1+2求值,在对1+2求值时,先对2求值,再对1求值;也可能选择先对1+2求值,在对1+2求值时,先对1求值,再对2求值,最后对3求值....还有其他情况。

这看似对最终表达式结果并没有什么影响(不管哪种求值顺序,结果都是6),但如果将简单的操作数换成函数调用,则会出现不同的情况,如下例所示。

例2 func1()+func2()+func3() int func1() { printf("This is func1.\n"); return 1; } int func2() { printf("This is func2.\n"); return 2; } int func3() { printf("This is func3.\n"); return 3; }

在这个例子中,三个函数的执行顺序是不确定的,可能是func1、func2、func3,可能是func2、func1、func3,可能是func3、func1、func2,可能是func3、func2、func1。这就导致了printf语句的执行也是不确定的。

下面再看一个更复杂的例子。

例3 1 + 2 * 3

在这个例子中,由于"*"的优先级大于"+", 2*3整体作为子表达式会成为"+"的右操作数。解析得到的语法分析树如下图2所示。

图2 语法分析树

在这个例子中,2与3的乘法毫无疑问是会在与1的加法前进行的,但是对1、2、3的求值顺序是不确定的,可能是先对1求值,随后对2*3求值,在对2*3求值时,先对2求值,再对3求值;可能是先对2*3求值,在对2*3求值时,先对3求值,再对2求值,最后对1求值...还有其他情况。

在这个简单的例子里,不同的求值顺序对结果没有影响,但如果将1、2、3换成func1、func2、func3,分析是类似的,即三个函数的执行顺序是不确定的(至少是不完全确定的)。

也有少部分操作符,是有规定求值顺序的,下面将对此进行说明。

一些有规定求值顺序的操作符

逻辑与、逻辑或运算符

逻辑与(&&)和逻辑或(||),它们的求值顺序是从左到右的,即先计算左操作数的值,后计算右操作数的值。对于&&操作符,如果其左操作数求值结果为0,则右侧的操作数不会被求值,最终结果为0;对于||操作符,如果其左操作数求值结果为1,则右侧的操作数不会被求值,最终结果为1,这也被称为短路逻辑,如例4所示。

例4 int a = 0; int b = 1; if (a && func1()) { // 这里func1不会执行,因为 a 为 0 } if (b && func1()) { // 这里func1不会执行,因为 b 为 1 }
三元(条件)运算符

在条件表达式中,首先计算条件部分,然后根据条件的值选择是求值 "真" 分支还是 "假" 分支,如下所示。

condition ? expr1 : expr2;

如果condition为真,则只求值expr1,否则只求值expr2,如例5所示。

例5 int x = 1; int a = (x > 0) ? func1) : func2()) // func2()不会执行,因为(x > 0)为真
逗号运算符

逗号运算符会从左到右的顺序求值,最后表达式的值为最右边操作数的值,如下所示。

expr1, expr2;

首先求值expr1,然后求值expr2,整个表达式的值为expr2的结果,如例6所示。

例6 int x = func1(), func2(); // func2()一定在func1()后执行
深入理解

其实如果深入了解这些特殊情况,会发现这些确定的求值顺序是由序列点所带来得,序列点(sequence point)是程序执行过程中一个特定的时刻,保证在该点之前的所有操作(包括表达式求值及其副作用)都已经完成并生效,而在该点之后的操作才开始。这意味着,在序列点之前发生的所有更改都必须在继续执行下一个操作之前完全确定。

对于除了上面三者的其他操作符,一个表达式的序列点一般仅存于表达式末尾的分号;,而对于逻辑与、逻辑或运算符,&&或||处存在序列点;对于三元(条件)运算符,?处存在序列点;对于逗号运算符,每个逗号处都是序列点。

这使得一些本来看起来像是出现了未定义行为的表达式,是确定的,如例7所示。

例7 // 未定义行为 int a = 1 int b = a + a++ ; // b值不确定,只保证了a的自增在表达式完成遇到;时发生 // 逻辑与、逻辑或运算符 int c = 0; int d = c++ || c; // d值必然为真,因为序列点保证了求值c时,自增已经发生 // 三元(条件)运算符 int e = 1 ; int f = e++ ? e : 0; // e值必然为2,因为序列点保证了求值e时,自增已经发生 // 逗号运算符 int g = 5; int h = (g++, g); // z值必然是6,因为序列点保证了求值逗号右边的g前,自增已经发生

即使这样,最好也不要使用这个特性,因为这样的代码是很容易使人产生疑惑和不解的。

Python中表达式的求值

Python中规定了表达式的求值顺序是从左到右的。就拿上面的例1举例,1+2子表达式一定在3之前求值,而1一定在2之前求值。用数据结构的语言来说,Python保证了在一个语法分析树中,表达式的求值是后序遍历的,即先求值左子节点,后求值右子节点,最后根据操作符求值整个表达式。下面来看一个例子。

例8 func1 + func2 * (func3 - func4)

根据运算符的优先级,这个表达式被解析为图3所示的语法分析树。

图3 语法分析树

根据后序遍历的定义,这四个函数的执行顺序为:func1、func2、func3、func4。详细说就是,"+"的左操作数func1一定会在右操作数func2 * (func3 - func4)前求值;"*"的左操作数func2一定会在右操作数(func3 - func4)前求值;"-"的左操作数func3一定会在右操作数func4前求值。

不止是针对表达式的求值,在Python中表达式列表的求值顺序也是确定的。表达式列表的定义如下所示,即为多个由","分隔的表达式,在函数调用、多变量赋值、函数返回中都有运用。

expression_list ::= expression ("," expression)* [","]

在如下例的函数调用中,求值的顺序是func1、func2、func3、func4、func5,其中func1也可以被求值,前提函数func1的返回值是一个函数名。

例9 func1(func2, func3, func4, func5)

在如下例的多变量赋值中,求值的顺序是func1、func2、func3。

a, b, c = func1(), func2(), func3()

在如下例的函数返回中,求值的顺序是func1、func2、func3.

return func1(), func2(), func3()

实际上,后两种情况下,表达式列表在被求值后会变成一个包含各表达式求值结果元组,在多变量赋值操作中,元组内的各个元素被赋值给对应的目标变量;在函数返回中,return语句返回一个元组。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进