对多维数组的一维访问:它是明确定义的行为吗?

时间:2011-06-09 09:48:40

标签: c pointers multidimensional-array standards language-lawyer

我想我们都同意,通过以一维方式解引用(可能是偏移的)指向其第一个元素的指针来访问真正的多维数组被认为是惯用的C,例如:

void clearBottomRightElement(int *array, int M, int N)
{
    array[M*N-1] = 0;  // Pretend the array is one-dimensional
}


int mtx[5][3];
...
clearBottomRightElement(&mtx[0][0], 5, 3);

然而,我的语言律师需要说服这实际上是定义明确的C!特别是:

  1. 标准是否保证编译器不会在其间插入填充,例如mtx[0][2]mtx[1][0]

  2. 通常,索引关闭数组的末尾(除了结尾之外)是未定义的(C99,6.5.6 / 8)。所以以下内容显然是未定义的:

    struct {
        int row[3];           // The object in question is an int[3]
        int other[10];
    } foo;
    int *p = &foo.row[7];     // ERROR: A crude attempt to get &foo.other[4];
    

    因此,根据同样的规则,人们会期望以下内容未定义:

    int mtx[5][3];
    int (*row)[3] = &mtx[0];  // The object in question is still an int[3]
    int *p = &(*row)[7];      // Why is this any better?
    

    那为什么要定义呢?

    int mtx[5][3];
    int *p = &(&mtx[0][0])[7];
    
  3. 那么C标准的哪一部分明确允许这个? (为了讨论,我们假设。)

    修改

    请注意,我毫不怀疑这在所有编译器中都能正常工作。我要查询的是标准是否明确允许这样做。

4 个答案:

答案 0 :(得分:13)

所有数组(包括多维数组)都是无填充的。即使从未明确提及过,也可以从sizeof规则推断出来。

现在,数组预订是指针算术的一个特例,而C99第6.5.6节,第8节明确规定只有在指针操作数和结果指针位于同一个数组(或者过去的一个元素)时才会定义行为,这使得C语言的边界检查实现成为可能。

这意味着您的示例实际上是未定义的行为。但是,由于大多数C实现不检查边界,它将按预期工作 - 大多数编译器处理未定义的指针表达式,如

mtx[0] + 5 

与明确定义的对手相同,如

(int *)((char *)mtx + 5 * sizeof (int))

是明确定义的,因为任何对象(包括整个二维数组)总是可以被视为char类型的一维数组。


进一步冥想6.5.6节的措辞,将越界访问划分为看似明确定义的子表达式,如

(mtx[0] + 3) + 2

推断mtx[0] + 3是指向mtx[0]末尾的一个元素的指针(使第一个加法定义明确)以及指向mtx[1]的第一个元素的指针(使第二个添加定义明确)是不正确的:

即使mtx[0] + 3mtx[1] + 0保证比较相等(参见6.5.9节,§6),它们在语义上也是不同的。例如,前者不能被解除引用,因此指向mtx[1]的元素。

答案 1 :(得分:9)

您希望进行此类访问的唯一障碍是类型int [5][3]int [15]的对象不允许彼此别名。因此,如果编译器意识到类型为int *的指针指向前者的int [3]数组之一,则可能会施加数组边界限制,从而阻止访问int [3]数组之外的任何内容

您可以通过将所有内容放在包含int [5][3]数组和int [15]数组的联合内部来解决此问题,但我真的不清楚联盟是否会破坏人们的使用对于类型 - 双关语实际上是明确定义的。这种情况可能稍微有点问题,因为你不会打字单个单元格,只有数组逻辑,但我仍然不确定。

应该注意的一个特殊情况:如果您的类型是unsigned char(或任何char类型),那么将多维数组作为一维数组进行访问将非常明确。这是因为与它重叠的unsigned char的一维数组被标准明确定义为对象的“表示”,并且本质上允许对其进行别名。

答案 2 :(得分:2)

  1. 确定数组元素之间没有填充。

  2. 提供了比完整地址空间更小的地址计算。这可以用于例如8086的巨大模式中,以便如果编译器知道您不能跨越段边界,则段部分不会总是更新。 (我很久以前就提醒我,我使用的编译器是否从中受益)。

  3. 使用我的内部模型 - 我不确定它与标准模型完全相同,检查太痛苦,信息分布在各处 -

    • 您在clearBottomRightElement中所做的事情是有效的。

    • int *p = &foo.row[7];未定义

    • int i = mtx[0][5];未定义

    • int *p = &row[7];无法编译(gcc同意我)

    • int *p = &(&mtx[0][0])[7];处于灰色区域(上次我查看了类似的详细信息,我最后考虑了无效的C90和有效的C99,可能是这里的情况,或者我可能错过了一些东西)。

答案 3 :(得分:-1)

我对C99 standard的理解是, no 要求多维数组必须以连续的顺序排列在内存中。遵循我在标准中找到的唯一相关信息(每个维度 保证是连续的)。

如果你想使用x [COLS * r + c]访问,我建议你坚持使用单维数组。

数组下标

连续的下标运算符指定多维数组对象的元素。 如果E是n维阵列(n≥2),其尺寸为i×j×。 。 。 ×k,然后是E(用作 除了左值之外)转换为指向(n - 1)维数组的指针 尺寸j×。 。 。 ×k。如果unary *运算符明确地应用于此指针,或 隐式地,作为下标的结果,结果是指向的(n - 1)维数组, 如果用作左值以外,它本身被转换为指针。由此得出结论 数组以行主顺序存储(最后一个下标变化最快)。

数组类型

- 数组类型描述了一个连续分配的非空对象集 特定的成员对象类型,称为元素类型。 36) 数组类型是 以其元素类型和数组中元素的数量为特征。一个 数组类型据说是从它的元素类型派生的,如果它的元素类型是T,那么 数组类型有时被称为''T'数组'。从中构造数组类型 元素类型称为“数组类型派生”。