【问题标题】:Using pointed to content in assignment of a pointer在分配指针时使用指向的内容
【发布时间】:2018-07-23 02:30:31
【问题描述】:

我一直认为,在赋值中读取正确表达式后缺少序列点会导致如下示例产生未定义的行为:

void f(void)
{
   int *p;
   /*...*/
   p = (int [2]){*p};
   /*...*/
}
// p is assigned the address of the first element of an array of two ints, the
// first having the value previously pointed to by p and the second, zero. The
// expressions in this compound literal need not be constant. The unnamed object
// has automatic storage duration.

但是,这是 C11 标准委员会草案中“6.5.2.5 复合文字”下的示例 2,版本标识为 n1570,我理解为最终草案(我无法访问最终版本)。

那么,我的问题是:标准中有什么东西可以提供这种定义和指定的行为吗?

编辑

我想详细说明我认为的问题,以回应一些已经出现的讨论。

我们有两个条件可以明确声明一个赋值具有 未定义的行为,根据 dbush 给出的答案中引用的标准的 6.5p2:

1) 标量对象的副作用相对于另一侧是无序的 对同一个标量对象的影响。

2) 标量对象的副作用相对于值是无序的 使用相同标量对象的值进行计算。

第 1 项的示例是“i = ++i + 1”。在这种情况下,副作用 由于 ++i 将值 i+1 写入 i 相对于将 RHS 分配给 LHS 的副作用是无序的。在每一侧的值计算和将 RHS 分配给 LHS 之间存在一个序列点,如下面 Jens Gustedt 的回答中给出的 6.5.16.1 中所述。但是,由于 ++i 对 i 的修改不受该序列点的影响,否则行为将 被定义。

在我上面给出的例子中,我们有类似的情况。有一个值计算,它涉及创建一个数组并将该数组转换为指向其第一个元素的指针。将值写入该数组的一部分还有一个副作用,*p 写入第一个元素。

所以,我看不出我们在修改的标准中有什么保证 数组的其他未初始化的第一个元素将被排序 在将数组地址写入 p 之前。这个修改(写*p到第一个元素)和写的修改有什么不同呢 i+1 到 i?

换句话说,假设一个实现将示例中感兴趣的语句视为三个任务:第一,为复合文字对象分配空间;第二:将指向所述空间的指针分配给p;第 3 步:将 *p 写入新分配空间中的第一个元素。 RHS 和 LHS 的值计算将在赋值之前进行排序,因为计算 RHS 的值只需要地址。这种假设的实现在哪些方面不符合标准?

【问题讨论】:

  • 你为什么认为它会产生未定义的行为?例如,i++ 等价于定义明确的i = i + 1;
  • 没有副作用,所以不需要序列点。
  • 副作用是将值写入 p。我认为这与 i++ 示例不同,我在评论中描述了下面 dbush 给出的答案。
  • 赋值本身的副作用只有在计算出 RHS 之后才会发生。
  • @Barmar,是的,但是在写入数组内容之前,可以计算右手边(可以得到数组的地址)。

标签: c variable-assignment c11 compound-literals


【解决方案1】:

C standard 的第 6.5p2 节详细说明了为什么这是有效的:

如果标量对象的副作用相对于其中任一对象而言是未排序的 对同一标量对象或值的不同副作用 使用相同标量对象的值进行计算,行为是 不明确的。如果有多个允许的排序 表达式的子表达式,如果这样的表达式的行为是未定义的 未排序的副作用发生在任何排序中。 84)

脚注 84 指出:

84) 本段呈现未定义的语句表达式,例如

i = ++i + 1;
a[i++] = i; 

同时允许

i = i + 1;
a[i] = i;

6.5.2.5 发布的 sn-p 属于后者,因为没有副作用。

【讨论】:

  • 感谢您的回复。但是,我不确定这是否定义了行为。在您提供的脚注中的两个允许的示例中,必须读取 i 的值以获取分配的值(以及在后一种情况下写入分配的地址),因此在分配它之后没有命令读取 i 的选项。但是,在我给出的示例中,可以分配复合文字并生成分配给 p 的地址,在分配后仅将 *p 写入分配的部分空间。
【解决方案2】:

需要看6.5.16.1中赋值运算符的定义

更新左操作数的存储值的副作用是 在左右操作数的值计算之后排序。 操作数的计算是无序的。

所以在这里你清楚地看到,它首先以任意顺序甚至同时计算两边的表达式,然后然后将右边的值存储到左边指定的对象中。

此外,您应该知道,作业的 LHS 和 RHS 的评估方式不同。引用有点太长了,这里总结一下

  • 对于 LHS,评估留下“左值”,即对象,例如 p,原封不动。特别是它不看的内容 对象。

  • 对于 RHS,存在“左值转换”,即对于在那里找到的任何对象(例如 *p),该对象的 内容 都会被加载。

  • 如果 RHS 包含数组类型的左值,则此数组将转换为指向其第一个元素的指针。这就是您的复合文字所发生的事情。

编辑:您添加了另一个问题

这个修改(将 *p 写入第一个元素)怎么样? 与将 i+1 写成 i 的修改不同?

区别只是分配的 LHS 中的 i 必须更新。复合文字中的数组不在 LHS 中,因此与更新无关。

【讨论】:

  • 感谢您的回复。但是,我没有看到我们如何得到 *p 对数组元素的分配必须在右侧的值计算之前排序,这只是获取作为复合文字的数组的地址。跨度>
  • @Kyle,嗯,我不确定我是否看到了您的问题。整个复合文字,包括它的初始值设定项是 RHS,p 是赋值的 LHS。因此,文本指出 RHS 和 p 的指定是存储操作之前的序列。 p 是一个左值,在这里,它只被评估以确定对象p 本身而不是它的内容。
  • @Kyle:没有将*p 分配给数组的元素。 *p 是复合文字中的初始值设定项。提供初始值不是赋值;它是复合文字对象创建的一部分,也是右侧值计算的一部分。
  • @JensGustedt,我不确定我是否看到这会导致我给出的示例中定义行为的结论。我试图在编辑我的问题时更清楚地表达我的担忧,因为评论太长了。
  • @Kyle:复合文字在初始化完成之前可以通过什么方式访问?当它存在并被转换为指向它的第一个元素的指针时,它已经被初始化了。对象使用其初始值创建。初始化不会发生在单独的赋值中,也不是副作用。它是创建由复合文字定义的对象的一个​​不可分割的部分。不存在对象存在且没有初始值的时刻。
【解决方案3】:

(int [2]){*p} 中,*p 为复合文字提供了一个初始值。这不是一项任务,也不是副作用。创建对象时,初始值是对象的一部分。不存在数组存在且未初始化的时刻。

p = (int [2]){*p} 中,我们知道更新p 的副作用是在计算右侧之后排序的,因为C 2011 [N1570] 6.5.16 3 说“更新左侧存储值的副作用操作数在左右操作数的值计算之后排序。”

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-12-12
    • 2018-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-03
    • 1970-01-01
    相关资源
    最近更新 更多