在C语言中,评估赋值运算符的左操作数有什么意义?

17
根据ISO C11 - 6.5.16.3规范,它表示:

  1. 赋值运算符将一个值存储在左操作数指定的对象中。赋值表达式具有赋值后左操作数的值,但不是lvalue。赋值表达式的类型是左操作数在lvalue转换后的类型。更新左操作数存储的值的副作用在左右操作数的值计算之后被排序。操作数的评估未排序。

所以我猜这意味着,例如,

int x = 10;
x = 5 + 10;
  1. 左操作数x被计算为10,右操作数被计算为15。
  2. 右操作数的值存储在由左操作数x指定的对象中。

但是,如果赋值的目的是存储右操作数的计算值(就像步骤2中一样),为什么需要评估左操作数? 评估左操作数的意义是什么?


左操作数在哪里被计算了? - phuclv
左侧值的计算完成后... - Jean-François Fabre
5个回答

27

当将x作为一个lvalue进行评估时,它不会被评估为10。 它被评估为一个lvalue,可以存储RHS的值。 如果LHS不被评估为一个lvalue,则该语句将是错误的。

来自C99标准(6.3.2.1/1):

lvalue是一个表达式(具有非void对象类型),它可能指定一个对象; 当对一个lvalue进行评估时,如果它没有指定一个对象,则行为未定义。

当你有一个简单的变量时,比如:

 x = 10;

然而,情况可能更加复杂。

 double array[10];
 int getIndex();   // Some function that can return an index based
                   // on other data and logic.

 array[getIndex()+1] = 10.0;

 // This looks like a function call that returns a value.
 // But, it still evaluates to a "storage area".
 int *getIndex2() { return(&array[0]); }
 *getIndex2()=123.45; // array[0]=123.45
如果getIndex()返回5,那么LHS评估为一个指定数组第7个元素的lvalue

你能详细解释一下什么是被评估为左值吗? - Jin
这是否意味着 array[getIndex()+1] 不会被评估为某个指针值,而是被评估为指向 int 的某个指针类型表达式? - Jin
哇,我从未听说过这样的概念,将其评估为一个对象。你知道关于这个主题的任何好参考资料吗? - Jin
任何一本关于 C 语言的书都应该在讨论数组时解释这一点。如果您可以访问 C99 标准,您可以在第 6.5.2.1/2 节中找到这个定义:后缀表达式后跟方括号 [] 中的表达式是数组对象的元素下标指示符。 - R Sahu
1
作为另一个例子,您可以有 *f() = 10;,这是在 LHS 上的函数调用,返回一个指针,然后对其进行解引用,产生要分配的对象。 - Paul J. Lucas

15

"左操作数"可以比你在例子中使用的简单的x复杂得多(尽管这确实不难评估):

*(((unsigned long*)target)++) = longValue;

左侧肯定需要进行一些评估。您引用的句子是指需要在赋值的左侧执行什么操作,以便找到适当的lvalue接收赋值。


4

只是为了说服自己(如果还没有这样做的话)从“叛徒”的角度来看,这证明我的帖子仅回答了您简单情况下的简单问题。

以下是一个小证明,显示在您的简单示例中,gcc 只是做了必要的事情,而不是更多:

代码:

int main()
{
int x = 10;
x = 5 + 10;

return x;
}

使用调试模式构建

K:\jff\data\python\stackoverflow\c>gcc -g -std=c11 -c assign.c

使用带有C/汇编代码的objdump

K:\jff\data\python\stackoverflow\c>objdump -d -S assign.o

assign.o:     file format pe-x86-64


Disassembly of section .text:

0000000000000000 <main>:
int main()
{
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 30             sub    $0x30,%rsp
   8:   e8 00 00 00 00          callq  d <main+0xd>
int x = 10;
   d:   c7 45 fc 0a 00 00 00    movl   $0xa,-0x4(%rbp)
x = 5 + 10;
  14:   c7 45 fc 0f 00 00 00    movl   $0xf,-0x4(%rbp)

return x;
  1b:   8b 45 fc                mov    -0x4(%rbp),%eax
}
  1e:   90                      nop
  1f:   48 83 c4 30             add    $0x30,%rsp
  23:   5d                      pop    %rbp
  24:   c3                      retq
  25:   90                      nop
  26:   90                      nop
  27:   90                      nop
  28:   90                      nop
  29:   90                      nop
  2a:   90                      nop
  2b:   90                      nop
  2c:   90                      nop
  2d:   90                      nop
  2e:   90                      nop
  2f:   90                      nop

正如其他(好的)答案所述,如果表达更复杂,则必须计算存储值的地址,因此需要进行某些形式的评估。

编辑:

有了一些稍微复杂一点的代码:

int main()
{
int x[3];
int i = 2;
x[i] = 5 + 10;

return x[i];
}

反汇编:

Disassembly of section .text:

0000000000000000 <main>:
int main()
{
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 30             sub    $0x30,%rsp
   8:   e8 00 00 00 00          callq  d <main+0xd>
int x[3];
int i = 2;
   d:   c7 45 fc 02 00 00 00    movl   $0x2,-0x4(%rbp)
x[i] = 5 + 10;
  14:   8b 45 fc                mov    -0x4(%rbp),%eax  <== hey, could be more optimized here: movl   $0x2,%eax covers line+above line :)
  17:   48 98                   cltq
  19:   c7 44 85 f0 0f 00 00    movl   $0xf,-0x10(%rbp,%rax,4)  <== this line holds the left-operand evaluation, in a way, %rax is used to offset the array address
  20:   00

return x[i];
  21:   8b 45 fc                mov    -0x4(%rbp),%eax
  24:   48 98                   cltq
  26:   8b 44 85 f0             mov    -0x10(%rbp,%rax,4),%eax
}
  2a:   90                      nop
  2b:   48 83 c4 30             add    $0x30,%rsp
  2f:   5d                      pop    %rbp
  30:   c3                      retq

你标记的行 x = 10 实际上并不符合 LHS 的评估 - 你所标记的是赋值 int x = 10。这里的 LHS 只是 -4(rbp),所以它非常容易评估,我们看不到评估过程。 - tofro
1
你可能是对的。为了结束争论,我改进了帖子,加入了直接混合 C/asm 输出。编译器是正确的 :) - Jean-François Fabre
你可以通过让赋值语句的左侧更复杂一些来加强你的回答,这样我们就能够更清楚地看到它的求值过程。 - tofro
感谢您的支持。尽管这只是展示了它的工作原理而不是其他答案所给出的真正解释,但我很荣幸能得到这些赞同。@OP:不要接受那个回答 :) - Jean-François Fabre
查看未经优化的反汇编实际上是一种毫无意义的练习。代码更难阅读,也不符合实际情况。 - Cody Gray

2

您在等号左侧有一些需要始终进行评估的非平凡表达式。以下是一些示例。

int array[5];
int *ptr = malloc(sizeof(int) * 5);

*ptr = 1;      // The lhs needs to evaluate an indirection expression
array[0] = 5;  // The lhs needs to evaluate an array subscript expression

for (int i = 0; i < 5; ++i) {
    *ptr++ = array[i];  // Both indirection and postincrement on the lhs!
}

// Here, we want to select which array element to assign to!
int test = (array[4] == 0);
(test ? array[0] : array[1]) = 5; // Both conditional and subscripting!

-1

还有什么其他方式呢?

int x, y, z;
x = y = z = 5;

工作?(赋值“z=5”必须将(r-)值赋给赋值“y= ...”,然后将值赋给赋值“x= ...”中的y。)

底层行为是:

  1. 将值5加载到一个寄存器中(在步骤7之前不要重复使用此寄存器)
  2. z的地址加载到一个寄存器中(当作左值时,"z"的意思就是这个)
  3. 将值5存储到z的地址中。现在5是"z"的右值。请记住,CPU处理的是值和地址,而不是"z"。变量标签"z"是一个人性化的指向包含值的内存地址的参考。根据它的用法,我们要么需要它的值(当我们获取z的值时),要么需要它的地址(当我们替换z的值时)。
  4. y的地址加载到一个寄存器中。
  5. z的值(5)存储到y的地址中。(可以优化并重复使用第一步中的"5")
  6. x的地址加载到一个寄存器中。
  7. y的值(5)存储到x的地址中。

1
赋值链的工作原理是因为在C语言中,赋值是表达式而不是语句,例如z = 5是一个表达式,它的值为5(同时也有将该值存储在z中的副作用)。但这实际上与OP的问题无关。 - Paul J. Lucas
@jamesdlin:恭喜你,现在你已经到达了我第五步中的括号注释。你现在想要换个角度,让人相信你已经阅读了你所批评的答案吗?你可能还需要思考一下,如果xyz的类型不同,需要进行提升,那么这将如何工作?在这种情况下,根据标准,是谁被评估了呢?(有用的提示:相关文本由OP引用。它是左成员。) - Eric Towers
我们并不是说左侧不会被评估。我们是说你所举的链式赋值示例并没有展示出那种评估方式。此外,如果涉及类型提升,它仍将应用于每个赋值的右侧。表达式的值被强制转换为其被分配到的类型;变量不会被强制转换为被分配表达式的类型。 - jamesdlin
也许这样更清楚:你的例子展示了左侧赋值的评估,就像 z = 5; y = z; x = y; 一样。 - jamesdlin
@jamesdlin:恭喜你。你现在已经证明了我的答案对OP的问题是响应的。zz=5的左侧,必须为y=z进行评估,yy=z的左侧,必须为x=y进行评估。然而你却说了矛盾的话:“如果涉及类型提升,它仍将应用于每个赋值的右侧”。在x=y=z中,将(y=z)转换为x是将子表达式的左侧y转换为x的类型,就像我7条评论之前写的那样。你时而自相矛盾,写着要将z转换为x的类型。 - Eric Towers
显示剩余10条评论

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