这是一个非常经典且容易出错的复合赋值表达式,因为它涉及到运算符的优先级、结合性以及副作用。
为了彻底理解它,我们把它拆解成几个步骤。
核心要点
- 运算符优先级:在C语言中,(乘法)的优先级高于(减法赋值)。
- 运算符结合性:赋值运算符(包括和)是从右向左结合的。
- 表达式的求值顺序:虽然我们知道哪个运算符先执行,但表达式中子表达式的求值顺序(比如
a的值何时被读取或修改)是由标准决定的,而不是由优先级决定的。
步骤分解
让我们假设变量 a 的初始值是某个数,int a = 2;,我们一步步来计算 a = a -= a * a;。
第1步:确定执行顺序
根据运算符优先级,a * a 会比 a -= ... 先计算。
根据运算符结合性,整个表达式是从右向左结合的,所以它实际上被解释为:
a = (a -= (a * a));
这个括号可以帮助我们理解,整个表达式由两个部分组成:
- 内部的子表达式:
a -= (a * a) - 外部的赋值:
a = (子表达式的结果)
第2步:计算内部的子表达式 a -= (a * a)
现在我们来计算 a -= (a * a),这个表达式等价于 a = a - (a * a)。
重要提示:所有 a 的引用都使用的是 a 的当前值,也就是我们初始设定的 2。
-
计算乘法部分:
a * a的结果是2 * 2 = 4。
-
执行减法赋值:
- 表达式变为
a = a - 4。 a - 4的结果是2 - 4 = -2。- 这个结果
-2会被赋值给变量a。
- 表达式变为
到此为止,变量 a 的值已经发生了改变,它现在是 -2。
第3步:计算外部的赋值 a = (子表达式的结果)
现在我们回到最外层的表达式:a = (子表达式的结果)。
- 这个“子表达式的结果”是什么?在C语言中,赋值表达式(如
x = y)的值,就是被赋值后x的值。 a -= (a * a)这个子表达式的值,a被赋值后的新值,也就是我们上一步计算出的 -2。
- 执行最终的赋值:
- 表达式变为
a = -2。 - 变量
a的值被再次赋值为-2。
- 表达式变为
总结与最终结果
经过以上三步,我们发现:
a的值从2变成了-2。- 最后一步又将
a的值赋为-2。
整个表达式执行完毕后,变量 a 的最终值是 -2。
示例代码验证
你可以运行下面的C代码来验证我们的分析。
#include <stdio.h>
int main() {
int a = 2;
printf("初始值: a = %d\n", a);
// 关键表达式
a = a -= a * a;
printf("执行后: a = %d\n", a);
return 0;
}
输出结果:
初始值: a = 2
执行后: a = -2
为什么容易出错?
人们通常会犯的错误是认为表达式的执行顺序是从左到右,
- 错误理解1:先执行
a = a,a的值不变。 - *再执行 `a -= a a
**,此时a还是2,计算2 - (2*2),得到-2`。 - 最终结果也是
-2。
在这个特定的例子中,因为 a 的初始值是 2,错误的从左到右计算恰好得到了正确的答案,但这纯属巧合! 这种理解是完全错误的,因为它违背了C语言运算符的优先级和结合性规则。
让我们换一个初始值来证明这种理解的错误性,假设 a 的初始值是 3。
正确的计算过程 (a=3):
a = a -= a * aa = a -= 3 * 3=>a = a -= 9a = 3 - 9=>a = -6(此时a的值变为-6)a = (-6)- 最终结果:a = -6
错误的从左到右计算过程 (a=3):
- 先执行
a = a,a仍然是3。 - 再执行
a -= a * a,即a = 3 - (3 * 3)=>a = 3 - 9=>a = -6。 - 最终结果:a = -6
嗯?这次结果又对了?看来这个例子也不够有说服力。
让我们再试一个,a 的初始值是 1。
正确的计算过程 (a=1):
a = a -= a * aa = a -= 1 * 1=>a = a -= 1a = 1 - 1=>a = 0(此时a的值变为0)a = (0)- 最终结果:a = 0
错误的从左到右计算过程 (a=1):
- 先执行
a = a,a仍然是1。 - 再执行
a -= a * a,即a = 1 - (1 * 1)=>a = 1 - 1=>a = 0。 - 最终结果:a = 0
看起来这个表达式很“宽容”?让我们试一个能让两个计算过程产生分歧的例子,关键在于 a 的值在计算过程中被修改,并且这个修改后的值在后续的“错误理解”逻辑中被使用。
让我们试试 a = 5。
正确的计算过程 (a=5):
a = a -= a * aa = a -= 5 * 5=>a = a -= 25a = 5 - 25=>a = -20(此时a的值变为-20)a = (-20)- 最终结果:a = -20
错误的从左到右计算过程 (a=5):
- 先执行
a = a,a仍然是5。 - 再执行
a -= a * a,即a = 5 - (5 * 5)=>a = 5 - 25=>a = -20。 - 最终结果:a = -20
好吧,这个表达式 a = a -= a * a 由于其结构,似乎很难找到让“错误理解”和“正确理解”产生不同结果的初始值,这会让很多人更加困惑。
真正的问题在于对“表达式求值顺序”的理解,而不仅仅是运算符优先级。
让我们换一个更经典的例子来阐明这个概念:i = i++。
对于 i = i++;
- 运算符优先级: 和 优先级相同,结合性从右到左,所以解释为
i = (i++)。 - 求值顺序:标准规定,对于 操作,先读取左操作数(
i的地址),再计算右操作数(i++)。 i++的副作用:i++会返回i的旧值,然后稍后(在“序列点”之后)再将i的值加 1。- 执行过程:
- 读取
i的地址(为了存储结果)。 - 计算
i++,得到i的旧值(5)。 - 将
5赋值给i(i的值是5)。 - 在序列点后,执行
i++的副作用,i的值变为6。
- 读取
- 最终结果:
i的值是6。
如果有人认为 i++ 先执行,再赋值,他会得到 i=5 的错误结论,这凸显了不能仅仅依赖“优先级”来推断所有细节,必须理解“求值顺序”和“序列点”。
回到 a = a -= a * a,虽然它看起来“对”,但它的正确执行路径是明确的:先乘,再减赋值,最后赋值,依赖这个路径而不是从左到右的直觉,才是编写可靠代码的关键。
