C99 是不是保证数组是连续的?

Posted

技术标签:

【中文标题】C99 是不是保证数组是连续的?【英文标题】:Does C99 guarantee that arrays are contiguous?C99 是否保证数组是连续的? 【发布时间】:2011-02-19 10:49:15 【问题描述】:

在另一个问题的热门评论线程之后,我开始讨论 C99 标准中关于 C 数组的定义和未定义的内容。

基本上,当我定义像 int a[5][5] 这样的二维数组时,标准 C99 是否保证它将是一个连续的整数块,我可以将它转换为 (int *)a 并确保我将拥有一个有效的一维数组25 个整数。

据我了解,上述属性隐含在 sizeof 定义和指针算术中,但其他人似乎不同意并说强制转换为 (int*) 上述结构会产生未定义的行为(即使他们同意 所有现有的实现实际上分配了连续的值)。

更具体地说,如果我们认为一种实现会检测数组以检查所有维度的数组边界并在访问一维数组时返回某种错误,或者不正确访问第一行以上的元素。这样的实现可以符合标准吗?在这种情况下,C99 标准的哪些部分是相关的。

【问题讨论】:

【参考方案1】:

我们应该首先检查一下 int a[5][5] 到底是什么。涉及的类型有:

int 整数数组[5] 数组[5] 个数组

不涉及整数数组[25]。

sizeof 语义暗示整个数组是连续的,这是正确的。 int 的数组[5] 必须有 5*sizeof(int),并且递归应用,a[5][5] 必须有 5*5*sizeof(int)。没有额外的填充空间。

此外,当使用 sizeof 给 memset、memmove 或 memcpy 时,数组作为一个整体必须工作。还必须可以使用 (char *) 遍历整个数组。所以一个有效的迭代是:

int  a[5][5], i, *pi;
char *pc;

pc = (char *)(&a[0][0]);
for (i = 0; i < 25; i++)

    pi = (int *)pc;
    DoSomething(pi);
    pc += sizeof(int);

对 (int *) 执行相同操作将是未定义的行为,因为如前所述,不涉及 int 数组 [25]。在克里斯托夫的回答中使用联合也应该是有效的。但还有一点使这更复杂,相等运算符:

6.5.9.6 两个指针比较相等当且仅当两者都是空指针,都是指向同一个对象(包括指向对象的指针和在其开头的子对象)或函数的指针,两者都是指向同一数组最后一个元素的指针对象,或者一个是指向一个数组对象末尾之后的指针,另一个是指向另一个数组对象的开头的指针,该数组对象恰好紧跟在地址空间中的第一个数组对象之后。 91)

91) 两个对象在内存中可能是相邻的,因为它们是较大数组的相邻元素或结构的相邻成员,它们之间没有填充,或者因为实现选择这样放置它们,即使它们不相关。如果先前的无效指针操作(例如访问数组边界外)产生未定义的行为,则后续比较也会产生未定义的行为。

这意味着:

int a[5][5], *i1, *i2;

i1 = &a[0][0] + 5;
i2 = &a[1][0];

i1 与 i2 比较。但是当使用 (int *) 遍历数组时,它仍然是未定义的行为,因为它最初是从第一个子数组派生的。它不会神奇地转换为指向第二个子数组的指针。

即使这样做

char *c = (char *)(&a[0][0]) + 5*sizeof(int);
int  *i3 = (int *)c;

不会有帮助。它比较等于 i1 和 i2,但它不是从任何子数组派生的;它最多是指向单个 int 或 int 数组 [1] 的指针。

我不认为这是标准中的错误。反之亦然:允许这样做会引入一种违反数组类型系统或指针算术规则或两者兼而有之的特殊情况。它可能被认为是缺少定义,但不是错误。

因此,即使 a[5][5] 的内存布局与 a[25] 的布局相同,并且使用 a (char *) 的相同循环可用于对两者进行迭代,实现是如果一个被用作另一个,则允许爆炸。我不知道它为什么应该或知道任何实现,也许标准中有一个事实直到现在还没有提到,这使它成为定义明确的行为。在那之前,我会认为它是未定义的并保持安全。

【讨论】:

@Secure:我相信这个定义背后的基本原理与cellperformance.beyond3d.com/articles/2006/06/…有关。读完这篇文章后,我相信标准选择了一个比必要更大的未定义行为,并且声明 concurrent accesses both through original pointer and casted one has undefined behavior 就足够了,但它们是安全的。 @Secure: 你同意吗,如果数组中使用的原始整数类型是char(或unsigned char?)而不是int,那么a[0][6]之类的东西会是有效且定义明确? @R..:不,这被明确列为未定义的行为。 J.2:“数组下标超出范围,即使对象显然可以使用给定的下标访问(如在左值表达式 a[1][7] 中给出声明 int a[4][5])( 6.5.6)。” @R..:但它不是字符的重叠数组,您仍然可以将其作为数组 [5][5] 访问。这是一个不同的问题。超出范围 UB 的数组下标不会对任何类型产生异常,例如 J.2 中的这样:“陷阱表示由不具有字符类型 (6.2.6.1) 的左值表达式读取。”因此,它始终是未定义的行为。 &amp;array[0][0]*(unsigned char (*)[25])&amp;array(unsigned char *)arrayarray[0] 都计算为指向 unsigned char 的相同指针。据我所知,它们必须相等(与== 比较)。访问 unsigned char [25] 类型的覆盖数组与一些但不是其他的有效 - 使用哪些有效? J.2 提供了丰富的信息,并且在它给出的示例中可能是正确的,但这并不意味着它可以扩展到其他表面上看起来相似的示例。【参考方案2】:

我在original discussion 中添加了更多的 cmets。

sizeof 语义暗示 int a[5][5] 是连续的,但是通过增加像 int *p = *a 这样的指针来访问所有 25 个整数是未定义的行为:指针算术仅在所涉及的所有指针位于(或过去一个元素)内时才被定义相同数组的最后一个元素,例如 &amp;a[2][1]&amp;a[3][1] 不这样做(请参阅 C99 第 6.5.6 节)。

原则上,您可以通过将&amp;a - 类型为int (*)[5][5] - 转换为int (*)[25] 来解决此问题。根据 6.3.2.3 §7,这是合法的,因为它不违反任何对齐要求。问题是通过这个新指针访问整数是非法的,因为它违反了 6.5 §7 中的别名规则。您可以通过使用union 进行类型双关来解决此问题(参见 TC3 中的脚注 82):

int *p = ((union  int multi[5][5]; int flat[25];  *)&a)->flat;

据我所知,这是符合标准的 C99。

【讨论】:

他可以合法地将 int(*)[25] 传递给另一个函数,对吧? (只要他不在与原始数组相同的范围内取消引用它)。 @Daniel:这确实是典型的用法(并且与调用 memset 或 memcpy 的权利一致)。但是从阅读 C99 开始,我并没有真正成功地思考这个问题。现在我可能会接受@Secure 的回答,因为我完全按照他的解释理解了连续的部分。 为此使用联合是未定义的行为。使用工会,您只能阅读最近写过的成员。 @R.. 仅当您正在写入的字节数比最近写入的字节数多时,它才会具有未指定的值。否则,在 C99 方面,没关系。另一方面,第二维的顺序是否有保证?即 &multi[1][4] == &flat[9] ? @syockit:gcc 和 clang 都太原始或太迟钝(我不知道是哪个),无法可靠地识别获取联合成员地址、使用该指针并放弃它的操作,所有都没有以任何其他方式访问联合,应该共同表现为对联合对象的访问。尽管即使在非常简单的情况下,该标准也没有明确要求这种承认,但我认为这种省略的原因是为了避免陈述显而易见的事情,而不是为了让编译器故意对这种可能性视而不见,这是不可信的。 【参考方案3】:

如果数组是静态的,比如你的 int a[5][5] 数组,它保证是连续的。

【讨论】:

研究 C 中“静态”一词的含义可能是个好主意。

以上是关于C99 是不是保证数组是连续的?的主要内容,如果未能解决你的问题,请参考以下文章

std::vector 元素是不是保证是连续的?

堆栈中的动态数组?

数组,指针,函数

C99 在运行时如何计算可变长度数组的大小?

C99 标准是不是保证 unsigned int 的二进制表示?

变长数组 - 转