一行中的多重赋值

39
我刚刚在嵌入式C(dsPIC33)中遇到了这个语句。
sample1 = sample2 = 0;

这是否意味着?
sample1 = 0;

sample2 = 0;

为什么他们这样输入呢?这是好的还是坏的编码方式?

可能是一行中的两个等号?的重复问题。 - Baum mit Augen
10个回答

62

请记住赋值操作是从右到左进行的,并且它们是正常表达式。因此,从编译器的角度来看,以下这行代码:

sample1 = sample2 = 0;

等同于

sample1 = (sample2 = 0);

就是说

sample2 = 0;
sample1 = sample2;

也就是说,sample2 被赋值为零,然后 sample1 被赋值为 sample2 的值。实际上,和你猜测的一样,这相当于同时将它们都赋值为零。


13
值得注意的是,这个表达式中没有顺序关系。这意味着假设首先分配给 sample2,并且分配给 sample1 的值实际上从 sample2 中读取,是不正确的。更正确的说法是,sample1 被赋值为转换为 sample2 类型的值 0。而且,sample1 的赋值实际上可以在 sample2 赋值之前发生。 - AnT stands with Russia
2
感谢回复,我会避免在可读性和排序问题上使用sample1=sample2=0,正如AndreyT所指出的那样。 - riscy
4
除了已经给出的内容,我要补充一点,即sample2=0返回一个rvalue,其值等于在sample2中分配的值。因此,这是一个赋值语句,并且除了分配的值之外还有一个返回值,该返回值等于所分配的值。这个返回值(rvalue)被分配给sample1 - Hazem

12

正式地,对于两个类型分别为TU的变量tu

T t;
U u;

任务分配
t = u = X;

(其中X是某个值)被解释为

t = (u = X);

它等同于一对独立的赋值操作。

u = X;
t = (U) X;

请注意,变量X的值应该在被赋给变量t之前“好像”已经通过变量u传递过去了,但并不一定要确实以这种方式发生。 X只需要在赋值给t之前被转换为u的类型即可。这个值不必先赋给u,然后从u复制到t。上面的两个赋值实际上没有顺序,并且可以以任何顺序发生。
t = (U) X;
u = X;

对于这个表达式,is also a valid execution schedule。请注意,这种顺序自由是特定于C语言的,在C++中,赋值的结果为lvalue,需要对“链接”赋值进行排序。

没有办法在没有看到更多上下文的情况下判断它是好还是坏的编程实践。在两个变量紧密相关的情况下(例如点的xy坐标),使用“链接”赋值将它们设置为某些公共值实际上是完全良好的做法(我甚至会说是“推荐的做法”)。但是,当变量完全不相关时,混合它们在一个单一的“链接”赋值中绝对不是一个好主意。特别是如果这些变量具有不同的类型,这可能会导致意想不到的后果。


1
我相信标准规定的效果并不像变量首先被写入那样,如果涉及到volatile变量,这种区别可能非常重要。 - supercat

10
我认为没有实际的汇编清单,就无法给出关于C语言的好答案 :)
因此,对于一个简单的程序:
int main() {
        int a, b, c, d;
        a = b = c = d = 0;
        return a;
}

我有这个汇编代码(Kubuntu,gcc 4.8.2,x86_64),当然使用了-O0选项;)

main:
        pushq   %rbp
        movq    %rsp, %rbp

        movl    $0, -16(%rbp)       ; d = 0
        movl    -16(%rbp), %eax     ; 

        movl    %eax, -12(%rbp)     ; c = d
        movl    -12(%rbp), %eax     ;

        movl    %eax, -8(%rbp)      ; b = c
        movl    -8(%rbp), %eax      ;

        movl    %eax, -4(%rbp)      ; a = b
        movl    -4(%rbp), %eax      ;

        popq    %rbp

        ret                         ; return %eax, ie. a

所以gcc实际上将所有的东西都链接在一起。


7
编译器可以进行任何不改变可观察行为的代码转换,因此汇编通常不能证明什么。唯一可靠的是依赖于C++标准规范(如果编译器承诺遵守该规范)。 - Markus

4
如早先所注意到的,

sample1 = sample2 = 0;

等于

sample2 = 0;
sample1 = sample2;

问题在于riscy问到了关于嵌入式C的问题,这种语言常用于直接驱动寄存器。许多微控制器的寄存器在读写操作时具有不同的目的。因此,在一般情况下,它并不相同,如下所示:
sample2 = 0;
sample1 = 0;

例如,假设UDR是一个UART数据寄存器。从UDR读取意味着从输入缓冲区获取接收到的值,而向UDR写入意味着将所需的值放入传输缓冲区并进行通信。在这种情况下,
sample = UDR = 0;

以下是意思:a)使用UART传输零值(UDR = 0;),b)读取输入缓冲区并将数据放入sample值中(sample = UDR;)。
你可以看到,嵌入式系统的行为可能比代码编写者预期的要复杂得多。在编程MCU时要小心使用此符号。

1
sample1 = sample2 = 0;

does mean

sample1 = 0;
sample2 = 0;  

如果且仅如果sample2被先声明,那么你才能这样做。你不能这样做:
int sample1 = sample2 = 0; //sample1 must be declared before assigning 0 to it

1

正如其他人所说,执行顺序是确定的。= 运算符的优先级保证了从右到左执行。换句话说,它保证了在 sample1 之前给 sample2 赋值。

然而,一行上的多个赋值是不良实践,并被许多编码标准禁止使用(*)。首先,它不太可读(否则你就不会问这个问题了)。其次,它是危险的。例如,如果我们有

sample1 = func() + (sample2 = func());

然后运算符优先级保证了与之前相同的执行顺序(+ 的优先级高于 =,因此需要括号)。sample2 将在 sample1 之前被赋值。但是,与运算符优先级不同,运算符的评估顺序是不确定的,这是未指定的行为。我们无法知道最右边的函数调用是否在最左边的函数调用之前被评估。

编译器可以自由地将上述代码转换为机器码,如下所示:

int tmp1 = func();
int tmp2 = func();
sample2 = tmp2;
sample1 = tmp1 + tmp2;

如果代码依赖于func()按特定顺序执行,那么我们就会产生一个讨厌的bug。它可能在程序的某个地方正常工作,但在同一程序的另一个部分中却出现错误,尽管代码是相同的。因为编译器可以自由地按任意顺序评估子表达式。
(*) MISRA-C:2004 12.2,MISRA-C:2012 13.4,CERT-C EXP10-C。

函数调用只是可能导致问题的副作用之一。例如硬件寄存器等易变变量也会有与此处描述相同的问题。 - Lundin
1
编译器无法为 sample1 = func() + (sample2 = func()); 生成三个对 func() 的调用。 - D Krueger
@DKrueger 谢谢,那其实是个打错字。已经修复了。 - Lundin

1
结果是相同的。如果它们都指向相同的值,有些人更喜欢链接赋值。这种方法没有问题。就个人而言,如果变量具有密切相关的含义,我认为这种方法更可取。

1
你可以自行决定这种编码方式是好还是坏。
  1. 只需在IDE中查看以下行的汇编代码。
  2. 然后将代码更改为两个单独的赋值,并查看差异。
此外,您还可以尝试关闭/打开编译器中的优化(大小和速度优化),以查看其如何影响汇编代码。

0

处理这个特殊情况... 假设b是一个形式为结构体数组的结构体

{
    int foo;
}

假设i是b中的一个偏移量。考虑函数realloc_b(),它返回一个int类型的值并执行数组b的重新分配。考虑以下多重赋值:

a = (b + i)->foo = realloc_b();

根据我的经验,首先解决(b + i),假设它在RAM中为b_i;然后执行realloc_b()。由于realloc_b()会改变RAM中的b,因此导致b_i不再被分配。

变量a被正确赋值,但(b + i)->foo没有被赋值,因为b已经被最左边的赋值项realloc_b()更改了。

这可能会导致段错误,因为b_i可能位于未分配的RAM位置。

为了无错运行,并且使(b + i)->foo等于a,这个单行赋值必须拆分成两个赋值:

a = reallocB();
(b + i)->foo = a;

-1
关于编码风格和各种编码建议,请参见此处: 可读性 a=b=c 还是 a=c; b=c;? 我相信通过使用

可以更好地组织代码并提高代码的可读性。
sample1 = sample2 = 0;

有些编译器会生成汇编代码,与两个赋值语句相比稍微快一些:

sample1 = 0;
sample2 = 0;

特别是当您初始化为非零值时。因为多重赋值会被翻译为:

sample2 = 0; 
sample1 = sample2;

因此,您只需进行一次初始化和一次复制,而不是两次初始化。速度提升(如果有的话)将非常微小,但在嵌入式情况下,每一点都很重要!


2
我怀疑是否存在一种编译器,可以为 sample2=0; sample1=sample2;sample1=sample2=0; 生成不同的机器码。 - Lundin
@Lundin 我正在比较多重赋值 sampl1=sample2=0; 和两个单独的赋值:sample1=0; sample2=0; 然后,我认为它们之间有区别,你觉得呢? - P. B. M.
1
我怀疑不会有差别。如果有的话,那么 sample1=0; sample2=0; 是更快的版本(大约快 1 个 CPU tick...)。 - Lundin
@lundin 我不会说“从未存在过”(我在这些年里使用过一些相当糟糕的编译器),但是是的,任何具备能力的现代编译器,在开启优化的情况下,很可能会生成完全相同的汇编代码。 - Russ Schultz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接