我刚刚在嵌入式 c (dsPIC33) 中遇到了语句
sample1 = sample2 = 0;
这是否意味着
sample1 = 0;
sample2 = 0;
他们为什么这样打字?这是好还是坏的编码?
请记住,赋值是从右到左进行的,并且它们是正常的表达式。所以从编译器的角度来看,这条线
sample1 = sample2 = 0;
是相同的
sample1 = (sample2 = 0);
这与
sample2 = 0;
sample1 = sample2;
即,sample2
赋值为零,然后sample1
赋值为sample2
。在实践中,这与您猜测的将两者都分配为零相同。
形式上,对于两个变量t
和u
类型T
和U
分别
T t;
U u;
那作业
t = u = X;
(其中X
一些值)被解释为
t = (u = X);
并且等价于一对独立的赋值
u = X;
t = (U) X;
请注意, 的值X
应该t
“就好像”它首先通过变量一样到达变量u
,但实际上并不要求它以这种方式发生。X
只需u
在分配给t
. 该值不必先分配给u
,然后从复制u
到t
。上面的两个赋值实际上是没有顺序的,可以按任何顺序发生,这意味着
t = (U) X;
u = X;
也是此表达式的有效执行计划。(请注意,这种排序自由度特定于 C 语言,其中右值赋值的结果。在 C++ 中,赋值计算为左值,这需要对“链式”赋值进行排序。)
如果没有更多的上下文,就无法说这是一种好的或坏的编程实践。如果这两个变量紧密相关(就像一个点x
的y
坐标),使用“链式”赋值将它们设置为某个共同值实际上是非常好的做法(我什至会说“推荐的做法”)。但是当变量完全不相关时,将它们混合在一个“链式”赋值中绝对不是一个好主意。特别是如果这些变量具有不同的类型,这可能会导致意想不到的后果。
我认为没有实际汇编列表的 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实际上是在链接所有的东西。
结果是一样的。如果它们都具有相同的值,则有些人更喜欢链接分配。这种方法没有任何问题。就个人而言,如果变量具有密切相关的含义,我认为这更可取。
您可以自己决定这种编码方式的好坏。
只需在 IDE 中查看以下行的汇编代码。
然后将代码更改为两个单独的分配,并查看差异。
除此之外,您还可以尝试在编译器中关闭/打开优化(大小和速度优化),看看它如何影响汇编代码。
如前所述,
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
value ( sample = UDR;
)。
您可以看到,嵌入式系统的行为可能比代码编写者预期的要复杂得多。在对 MCU 进行编程时,请谨慎使用此符号。
sample1 = sample2 = 0;
确实意味着
sample1 = 0;
sample2 = 0;
当且仅当sample2
是较早声明的。
你不能这样做:
int sample1 = sample2 = 0; //sample1 must be declared before assigning 0 to it
关于编码风格和各种编码建议,请参见此处: 可读性 a=b=c 或 a=c;b=c;?
我相信通过使用
sample1 = sample2 = 0;
与 2 个分配相比,某些编译器生成程序集的速度会稍快一些:
sample1 = 0;
sample2 = 0;
特别是如果您正在初始化为非零值。因为,多重分配转化为:
sample2 = 0;
sample1 = sample2;
因此,您只需进行一份和一份副本,而不是 2 次初始化。加速(如果有的话)将很小,但在嵌入式情况下,每一点都很重要!
正如其他人所说,执行的顺序是确定性的。= 运算符的运算符优先级保证这是从右到左执行的。换句话说,它保证 sample2 在 sample1 之前被赋予一个值。
但是,在一行上进行多个分配是不好的做法,并且被许多编码标准 (*) 禁止。首先,它不是特别可读(或者你不会问这个问题)。二是危险。如果我们有例如
sample1 = func() + (sample2 = func());
then 运算符优先级保证与之前相同的执行顺序(+ 的优先级高于 =,因此是括号)。sample2 将在 sample1 之前获得一个值。但与运算符优先级不同,运算符的评估顺序不是确定性的,它是未指定的行为。我们不知道最右边的函数调用在最左边的函数调用之前被评估。
编译器可以自由地将上述内容翻译成机器代码,如下所示:
int tmp1 = func();
int tmp2 = func();
sample2 = tmp2;
sample1 = tmp1 + tmp2;
如果代码依赖于 func() 以特定顺序执行,那么我们创建了一个讨厌的错误。它可能在程序的一个地方工作正常,但在同一程序的另一部分中断,即使代码相同。因为编译器可以自由地以它喜欢的任何顺序评估子表达式。
(*) MISRA-C:2004 12.2、MISRA-C:2012 13.4、CERT-C EXP10-C。
照顾这种特殊情况......假设 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;