对于所有不完整的答案,我必须澄清一下。
首先,表达式从右到左求值:
list->head = (list->tail = NULL);
标准将behaviour of the assignment operator定义为:
赋值运算符将值存储在左操作数指定的对象中。赋值表达式在赋值后具有左操作数的值,111) 但不是左值。赋值表达式的类型是左操作数在左值转换后的类型。更新左操作数的存储值的副作用是在左操作数和右操作数的值计算之后排序的。操作数的计算是无序的。
现在的问题是,标准留下了两种方法如何获得赋值的值。**
// variant 1 (read the left-hand side after writing it)
list->tail = NULL;
list->head = list->tail;
也就是说,list->tail 在获得它的值后被读取,并将该值分配给list->head。
// variant 2 (use temporary storage)
typeof(list->tail) temp = NULL; // get the value/type of the RHS of the inner assignment
list->tail = temp;
list->head = temp;
(RHS:右侧,运算符右侧的术语)。感谢@chux,这两个任务也可以互换。
见footnote 111:
允许实现读取对象以确定值,但不是必需的,即使对象具有 volatile 限定类型。
变体在抽象机器方面表现相同 - 除非list->tail 是合格的volatile(更一般地说:除了最左边的对象之外的任何对象)。简而言之,volatile 告诉编译器对对象的访问有副作用。它通常用于外围硬件寄存器,例如USART。虽然它很少(如果经常被错误地)用于桌面应用程序,但通常用于操作系统内核驱动程序和裸机嵌入式系统。
对此类寄存器的写入和读取通常会将写入(外部)硬件的数据分别传递。读取会产生这种硬件寄存器的值。更糟糕的是,这些寄存器可以是只写的或只读的。对于只写,读取会产生与写入内容无关的不确定值。与此相关的是,对这些对象的访问必须非常小心地控制和排序。不会有意外的读取或写入。
因此,请考虑上面的代码。然后变体将生成对硬件的不同访问:
- 变体 1 将导致先写入,然后再进行读取。如果寄存器是
只写,读取产生不相关的数据。这可能不是我们想要的。
- 变体 2 只会写入,但会重用为第二个赋值写入的值。
- 变体 2 的写入执行顺序不确定,这是
volatile 左值的另一个问题。
请注意,使用哪种变体不是由用户决定的,而是由编译器决定的。它甚至可能因代码中的不同表达式而有所不同。
因此,一旦涉及到volatile,链式分配就不再适用。