在 C/C++ 中使用复合赋值的真正优势是什么(或者也可能适用于许多其他编程语言)?
#include <stdio.h>
int main()
{
int exp1=20;
int b=10;
// exp1=exp1+b;
exp1+=b;
return 0;
};
我查看了一些链接,例如 microsoft site、SO post1、SO Post2。 但优点是在复合语句的情况下 exp1 仅被评估一次。在第一种情况下 exp1 是如何真正被评估两次的?据我所知,首先读取 exp1 的当前值,然后添加新值。更新后的值被写回同一位置。在复合语句的情况下,这实际上是如何在较低级别发生的?我尝试比较两种情况的汇编代码,但我没有看到它们之间有任何区别。
对于涉及普通变量的简单表达式,之间的区别
a = a + b;
和
a += b;
仅是语法上的。 这两个表达式的行为完全相同,并且很可能生成相同的汇编代码。 (你是对的;在这种情况下,询问
a
是否被评估一次或两次都没有多大意义。)
有趣的是当赋值的左侧是涉及副作用的表达式时。 所以如果你有类似的东西
*p++ = *p++ + 1;
与
*p++ += 1;
它的作用更大! 前者尝试将
p
递增两次(因此未定义)。 但后者只对 p++
求值一次,并且定义明确。
正如其他人提到的,还有符号方便和可读性的优点。 如果你有
variable1->field2[variable1->field3] = variable1->field2[variable2->field3] + 2;
很难发现错误。 但如果你使用
variable1->field2[variable1->field3] += 2;
根本不可能有这个错误,后来的读者不必仔细检查条款来排除这种可能性。
一个小优点是它可以为您节省一对括号(或者如果您省略这些括号,则可以避免错误)。 考虑:
x *= i + 1; /* straightforward */
x = x * (i + 1); /* longwinded */
x = x * i + 1; /* buggy */
最后(感谢 Jens Gustedt 提醒我这一点),我们必须回过头来更仔细地思考一下当我们说“当赋值的左侧是一个表达式时”时我们的意思。涉及副作用。” 通常,我们认为修改是副作用,而访问是“免费”的。 但对于限定为
volatile
(或者在 C11 中为 _Atomic
)的变量,访问也算作一个有趣的副作用。 因此,如果变量 a
具有这些限定符之一,则 a = a + b
不是“涉及普通变量的简单表达式”,而且毕竟它可能与 a += b
不太相同。
使用复合赋值的优点
也有一个缺点。
考虑类型的影响。
long long exp1 = 20;
int b=INT_MAX;
// All additions use `long long` math
exp1 = exp1 + 10 + b;
10 + b
下面的添加将使用 int
数学和溢出(未定义的行为)
exp1 += 10 + b; // UB
// That is like the below,
exp1 = exp1 + (10 + b);
如果左侧不仅仅是一个简单的变量名,那么计算一次左侧可以为您节省很多。 例如:
int x[5] = { 1, 2, 3, 4, 5 };
x[some_long_running_function()] += 5;
在这种情况下,
some_long_running_function()
仅被调用一次。 这不同于:
x[some_long_running_function()] = x[some_long_running_function()] + 5;
这会调用该函数两次。
标准 6.5.16.2 是这么说的:
形式为 E1 op= E2 的复合赋值等效于简单赋值表达式 E1 = E1 op (E2),只不过仅计算左值 E1一次
所以“评估一次”就是区别。这在具有
volatile
限定符并且不想多次读取硬件寄存器的嵌入式系统中最为重要,因为这可能会导致不需要的副作用。
这实际上不可能在 SO 上重现,因此这里有一个人为示例来演示为什么多次评估可能会导致不同的程序行为:
#include <string.h>
#include <stdio.h>
typedef enum { SIMPLE, COMPOUND } assignment_t;
int index;
int get_index (void)
{
return index++;
}
void assignment (int arr[3], assignment_t type)
{
if(type == COMPOUND)
{
arr[get_index()] += 1;
}
else
{
arr[get_index()] = arr[get_index()] + 1;
}
}
int main (void)
{
int arr[3];
for(int i=0; i<3; i++) // init to 0 1 2
{
arr[i] = i;
}
index = 0;
assignment(arr, COMPOUND);
printf("%d %d %d\n", arr[0], arr[1], arr[2]); // 1 1 2
for(int i=0; i<3; i++) // init to 0 1 2
{
arr[i] = i;
}
index = 0;
assignment(arr, SIMPLE);
printf("%d %d %d\n", arr[0], arr[1], arr[2]); // 2 1 2 or 0 1 2
}
简单赋值版本不仅给出了不同的结果,还在代码中引入了未指定的行为,因此根据编译器的不同,可能会出现两种不同的结果。
不确定你在追求什么。复合赋值更短,因此比使用常规操作更简单(不太复杂)。
考虑一下:
player->geometry.origin.position.x += dt * player->speed;
对:
player->geometry.origin.position.x = player->geometry.origin.position.x + dt * player->speed;
哪一个更容易阅读、理解和验证?
对我来说,这是一个非常非常真实的优势,并且无论语义细节如何(例如某事物被评估多少次),它都一样真实。
像 C 这样的语言始终是底层机器操作码的抽象。在加法的情况下,编译器首先将左操作数移入累加器,然后将右操作数添加到其中。像这样的东西(伪汇编代码):
move 1,a
add 2,a
这就是
1+2
在汇编器中编译的内容。显然,这可能过于简单化了,但你明白了。
此外,编译器倾向于优化您的代码,因此
exp1=exp1+b
很可能会编译为与 exp1+=b
相同的操作码。
而且,正如@unwind 所说,复合语句的可读性要强得多。