在C中,如果B是易失性的,那么表达式(void)(B = 1)应该读取B.

Ned*_*Ned 13 c volatile c99 variable-assignment

我在几个嵌入式平台的编译器上工作.用户最近抱怨我们的一个编译器出现以下行为.给出这样的代码:

extern volatile int MY_REGISTER;

void Test(void)
{
    (void) (MY_REGISTER = 1);
}
Run Code Online (Sandbox Code Playgroud)

编译器生成它(在伪汇编程序中):

Test:
    move regA, 1
    store regA, MY_REGISTER
    load regB, MY_REGISER
Run Code Online (Sandbox Code Playgroud)

也就是说,它不仅写入MY_REGISTER,而且之后将其读回.由于性能原因,额外负载使他心烦意乱.我解释说这是因为根据标准"赋值表达式在赋值后具有左操作数的值,[...]".

奇怪的是,删除cast-to-void会改变行为:负载消失.用户很高兴,但我只是感到困惑.

所以我也在几个版本的GCC(3.3和4.4)中检查了这一点.在那里,编译器永远不会生成负载,即使明确使用该值,例如

int TestTwo(void)
{
    return (MY_REGISTER = 1);
}
Run Code Online (Sandbox Code Playgroud)

变成

TestTwo:
    move regA, 1
    store regA, MY_REGISTER
    move returnValue, 1
    return
Run Code Online (Sandbox Code Playgroud)

有没有人对该标准的正确解释有何看法?回读是否应该发生?如果使用该值或将其转换为void,则添加只读是正确还是有用?

Jen*_*edt 7

标准中的相关段落是这样的

赋值运算符将值存储在左操作数指定的对象中.赋值表达式在赋值后具有左操作数的值,但不是左值.赋值表达式的类型是左操作数的类型,除非左操作数具有限定类型,在这种情况下,它是左操作数类型的非限定版本.更新左操作数的存储值的副作用应发生在前一个和下一个序列点之间.

所以这清楚地区分了"左操作数的值"和存储值的更新.另请注意,返回值不是左值(因此在表达式的返回中没有对变量的引用)并且所有限定符都将丢失.

所以我读到这是因为当gcc返回它知道必须存储的值时正在做正确的事情.

编辑:

即将推出的标准计划通过添加脚注来澄清:

允许实现读取对象以确定值,但不需要,即使对象具有volatile限定类型.

编辑2:

实际上还有另一段关于表达式的陈述可能会对此有所阐述:

表达式语句中的表达式被计算为其副作用的void表达式.\ footnote {例如赋值和具有副作用的函数调用}

由于这意味着对于这样的语句不需要返回值的效果,这强烈暗示如果使用该值,则只能从变量加载该值.

总而言之,当客户看到变量已加载时,他的确非常不满.如果你扩展它的解释,这种行为可能符合标准,但它显然是在可接受的边界线上.

  • 那么我对 Jim 的回答的评论中的示例 `volatile` 变量怎么样?为什么左操作数的值是分配的值,而不是通过读取它总是得到的值?我并不是说*应该*有负载,只是我认为根本不知道*不应该*有负载。 (2认同)

APr*_*mer 6

回读似乎更接近标准(特别是考虑到读取一个volatile变量可能导致与写入的值不同的值),但我很确定它不是大多数使用volatile的代码所期望的,尤其是读取或写入volatile变量的上下文会触发其他一些影响.

一般来说,volatile的定义不是很明确 - "构成对具有volatile限定类型的对象的访问是由实现定义的."

编辑:如果我不得不编写一个编译器,我想我不会读回变量,如果没有使用它并重新读取它,如果是,但有一个警告.然后应该使用无效的演员?

(void) v;
Run Code Online (Sandbox Code Playgroud)

肯定应该是一个,考虑到这一点,我没有任何理由

(void) v = exp;
Run Code Online (Sandbox Code Playgroud)

不是.但无论如何,我会发出警告,解释如何获得其他效果.

顺便说一句,如果您在编译器上工作,您可能有人与C委员会联系,填写正式的缺陷报告将为您带来一个具有约束力的解释(好吧,存在DR被归类为"非缺陷"而没有任何缺陷的风险提示他们想要什么...)