【问题标题】:Is it allowed to create objects at non-sizeof offsets from each other in an allocated memory region?是否允许在分配的内存区域中以非 sizeof 偏移量创建对象?
【发布时间】:2020-04-06 23:06:55
【问题描述】:

这最好通过例子来解释。

typedef struct s_ {
  int a, b;
} s;

int add(s* l, s* r) { return l->a + l->b + r->a + r->b; }

void init(s* v) {
    v->a = 1;
    v->b = 2;
}

int array_like() {
  // allocate enough space for s[2]
  char *p = malloc(2 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int array_skip() {
  // allocate enough space for s[3] and init only [0] and [2]
  char *p = malloc(3 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + 2 * sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int half_gap() {
  // allocate enough space for 2.5 s objects and lay them
  // out like [s1][gap of alignof(s) bytes][s2]
  char *p = malloc(2 * sizeof(s) + _Alignof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s) + _Alignof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

具体而言,考虑一个典型的平台,其中 sizeof(s) == 8alignof(s) == 4 - 尽管这个问题应该同样适用于具有不同值的平台。

最后三个函数,array_likearray_skiphalf_gap 都执行类似的功能:它们在由malloc。这三个都将第一个 s 对象放在存储的开头。它们的区别仅在于放置第二个对象的位置:

  • array_like 将其直接放在第一个对象之后,即偏移量 8 处,因此对于指向区域开头的指针 s* p,对象将位于 p[0]p[1]
  • array_skipsizeof(s) 放置在第一个对象末尾之后的字节,即偏移 16,因此对于指向区域开头的指针 s* p,对象将位于 p[0]p[2]
  • half_gap_Alignof(s) 放置在第一个对象之后的字节,或第一个对象之后的 4 个字节,即从存储开始的 12 个字节。请注意,这个对象仍然在 4 个字节的边界上正确对齐,但它没有从第一个偏移 integralsizeof(s) 个字节。您不能像前两种情况那样使用类似数组的符号来表示第二个对象的位置。

之后,每个函数写入两个对象的两个成员,然后从它们读取。

这些函数中哪些是合法的 C11,并且所有合法的函数都保证返回预期值 6?

【问题讨论】:

    标签: c pointers language-lawyer c11 pointer-arithmetic


    【解决方案1】:

    您问题中的代码是正确的。当且仅当它与被转换为的类型正确对齐时,才定义指针转换。

    (脚注 - 我看到人们争辩说 6.3.2.3/7 没有明确指定转换的结果指向内存中的相同字节 - 但是如果接受该参数,则 malloc 不可用,因为它不能保证将void * 转换为任何类型以供将来使用点到分配的块中。所以我认为该论点无效)。

    由于要求数组在元素之间没有填充,我们可以得出结论,sizeof(T) 必须是 _Alignof(T) 的倍数,因此所有整数表达式都是 _Alignof(T) 的倍数,因此所有的有问题的指针正确对齐。

    关于术语,object 在 C 中表示“存储区域”。因此 malloc 分配的整个空间都是一个对象,它的任何连续子集也是如此。

    使用赋值运算符修改一个对象,而不是创建它。当您在 malloc 的空间上使用赋值运算符时,它会设置写入的字节的 有效类型。 C11 6.5/6(又名严格别名规则)定义了“有效类型”的含义。


    还有一个细节。如果您的 init 函数如下所示:

    void init(s* v) {
        s a = { .a = 1, .b = 2 };
        *v = a;
    }
    

    那么这将是故事的结尾,s 类型的值被写入该位置。但标准并不清楚v->a = 1; 设置了哪种有效类型。该标准最常见的解释是v->a = 1 表示(*v).a = 1;,并且此赋值还具有为整个*v 对象设置有效类型的“副作用”。

    这种解释使 TBAA 能够在像 void f(s *ps, t* pt) 这样的函数(其中 s 是一个没有 t 类型的成员的结构)上假设 *ps*pt 是不相交的。

    我确信后一点已经有多个关于它的问题

    【讨论】:

    • 谢谢。不清楚的一件事是什么允许您在 malloc 区域内的任意偏移处创建对象。 7.22.3 可能让我有点失望,因为它说从 malloc 返回的指针可能用于访问分配的空间中的此类对象或此类对象的数组,但它没有不是说你可以像half_gap 那样以非数组方式使用它……但我想它可能被其他规则所暗示。
    • @BeeOnRope malloc 创建一个对象(我在“使用赋值运算符”开头的段落中解决了这个问题)
    • @BeeOnRope 对于您在评论中引用的文本,只是句子的一半,整个句子指定指针在开头的对象或此类对象数组中正确对齐空间 。这并不意味着空间后面不能有其他子对象
    猜你喜欢
    • 2020-08-19
    • 1970-01-01
    • 2012-01-08
    • 2021-02-22
    • 1970-01-01
    • 1970-01-01
    • 2023-03-18
    • 1970-01-01
    相关资源
    最近更新 更多