别名结构和数组是否合法?

时间:2018-01-08 09:29:17

标签: c arrays struct language-lawyer

结构中相同类型的连续成员之间的指针算术过去常常是一种常见做法,而指针算术仅在数组内有效。在C ++中,它将是明确的Undefined Behavior,因为数组只能由声明或新表达式创建。但是C语言将一个数组定义为一个连续分配的非空对象集合 特定的成员对象类型,称为元素类型。(C11的n1570草案,6.2.5类型§20)。因此,如果我们可以确保成员是连续的(意味着它们之间没有填充),那么将它看作数组是合法的。

这是一个简化的示例,它在没有警告的情况下编译,并在运行时给出预期的结果:

#include <stdio.h>
#include <stddef.h>
#include <assert.h>

struct quad {
    int x;
    int y;
    int z;
    int t;
};

int main() {
    // ensure members are consecutive (note 1)
    static_assert(offsetof(struct quad, t) == 3 * sizeof(int),
        "unexpected padding in quad struct");
    struct quad q;
    int *ix = &q.x;
    for(int i=0; i<4; i++) {
        ix[i] = i;
    }
    printf("Quad: %d %d %d %d\n", q.x, q.y, q.z, q.t);
    return 0;
}

这里没有任何意义,但我已经看到了一个真实世界的例子,在结构成员之间进行迭代可以提供更简单的代码而且风险更低。

问题:

在上面的例子中,static_assert是否足以使结构的别名与数组合法化?

(注1)由于struct 描述了一组顺序分配的非空成员对象,后面的成员必须具有增加的地址。只需编译器可以在它们之间包含填充。因此,如果3次t加上之前的总填充,则最后一个成员(此处为sizeof(int))的偏移量。如果偏移正好是3 * sizeof(int),则struct

中没有填充

作为副本提出的问题包含一个接受的答案,让我们认为它将是UB,而+1 answer让我们认为它可能是合法的,因为我可以确保不存在填充

5 个答案:

答案 0 :(得分:2)

这里的问题是你对连续分配的定义:&#34;我们可以确保成员是连续的(意味着它们之间没有填充)&#34;。

虽然这是连续分配的必然结果,但它没有定义属性。

您的结构成员是具有自动存储持续时间的单独变量,具有或不具有填充的特定顺序,具体取决于您如何控制编译器,这些都是。因此,您不能使用指针算法来获取给定另一个成员的地址,并且这样做的行为是未定义的。

答案 1 :(得分:2)

不,像这样对struct和数组进行别名是不合法的,它违反了严格的别名。解决方法是将结构包装在一个联合中,该联合包含一个数组和各个成员:

union something {
  struct quad {
    int x;
    int y;
    int z;
    int t;
  };

  int array [4];
};

这会避免严格的别名冲突,但您仍可能有填充字节。您可以使用静态断言检测到它。

另一个问题仍然存在,那就是你不能在指向结构的第一个成员的int*上使用指针算法,因为在添加运算符的指定行为中概述了各种模糊的原因 - 它们要求指针指向数组类型。

避开所有这些的最好方法是简单地使用上面联合的数组成员。这与静态断言一起产生定义明确,坚固耐用且可移植的代码。

(理论上,您也可以使用指向字符类型的指针来遍历结构 - 与int*不同,这可以按照6.3.2.3/7进行。但如果你有这个问题,这是一个更麻烦的解决方案对单个字节没兴趣。)

答案 2 :(得分:1)

我要和UB争辩。首先,来自 6.5.6 Additive operators 的强制引用:

  

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   指针操作数指向数组对象的元素和数组   足够大,结果指向一个偏离的元素   原始元素使得下标的差异   结果和原始数组元素等于整数表达式。   换句话说,如果表达式P指向一个的第i个元素   数组对象,表达式(P)+ N(等效地,N +(P))和(P)-N   (其中N具有值n)分别指向第i + n和第i   数组对象的第i个元素,只要它们存在即可。而且,如果   表达式P指向数组对象的最后一个元素,即   表达式(P)+1点超过数组对象的最后一个元素,   如果表达式Q指向一个数组的最后一个元素   对象,表达式(Q)-1指向数组的最后一个元素   宾语。如果指针操作数和结果都指向元素   相同的数组对象,或者超过数组的最后一个元素的一个   对象,评估不得产生溢出;否则,   行为未定义。如果结果指向最后一个元素   数组对象,不得用作一元*的操作数   被评估的运算符。

我强调了我认为问题的症结所在。当你说数组对象是&#34;是一个连续分配的非空对象集合,具有特定的成员对象类型时,你就是对的,称为元素类型&#34; 。但反过来又是真的吗?连续分配的对象集是否构成数组对象?

我要说不。需要明确创建对象。

因此,对于您的示例,没有数组对象。通常有两种方法可以在C中创建对象。使用自动,静态或线程本地持续时间声明它们。或者分配它们并为存储提供有效的类型。你既没有创建数组。这使得算术正式未定义。

答案 3 :(得分:1)

首先 -

引用C11,章§6.5.2.1p2

  

后缀表达式后跟方括号[]中的表达式是数组对象元素的下标名称。下标运算符[]的定义是E1[E2](*((E1)+(E2)))相同。 ...

这意味着ix[i]评估为*(ix + i)。这里的子表达式为ix + iix的类型为pointer to integer

现在,

引用C11,章§6.5.6p7

  

出于这些运算符的目的,指向不是数组元素的对象的指针与指向长度为1的数组的第一个元素的指针的行为相同,其中对象的类型为其元素类型。

因此我们知道ix指向一个大小为1的数组。甚至构造一个超出长度的指针(除了一个除外)是未定义的行为,更不用说解除引用它了。

这导致我解释这确实是不允许的。

答案 4 :(得分:1)

这将是UB。正如在that other question中建立的那样,static_assert可以以一致的方式测试可能的填充。所以是的,结构的4个成员确实是连续分配的。

但真正的问题是连续分配是必要的,但还不足以构成一个数组。即使我在C标准中找不到它的明确参考,对象在其生命周期内也不能重叠 - 这在C ++标准中更明确地说明了。它们可以是聚合(结构或数组)的成员,但不允许聚合重叠。这与1992年12月10日Antti Haapala在其answer中提到的C89引用的缺陷报告#017的回应一致。

即使C没有new语句,分配的存储也具有没有声明类型的特定属性。这允许在该存储中动态创建对象,但是当在其地址创建不同类型的对象时,已分配对象的生命周期结束。因此,即使在分配的内存中,我们也不能同时拥有数组和结构。

根据Lundin's answer,通过数组和结构之间的联合输入punning应该有效,因为(非规范性)注释说明

  

如果用于读取union对象内容的成员与上次使用的成员不同   在对象中存储一个值,该值的对象表示的相应部分被重新解释   作为新类型中的对象表示

并且两种类型都具有相同的表示:4个连续的整数

如果没有联合,迭代数组成员的方法是在字节级别,因为6.3.2.3转换/指针说:

  

7 ...当指向对象的指针转换为指向字符类型的指针时,   结果指向对象的最低寻址字节。连续增量   结果,直到对象的大小,产生指向对象剩余字节的指针。

char *p = q;
for (i=0; i<4; i++) {
    int *ix = (int *) (p + i * sizeof(int));  // Ok: points to the expected int member
    *ix = i;
}

但非字符类型上的指针算术迭代结构的成员是UB,因为结构的各个成员不能同时是数组的成员。