22

我想我们都同意通过以一维方式取消引用指向其第一个元素的(可能是偏移的)指针来访问真正的多维数组被认为是惯用的 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. 标准是否保证编译器不会在 egmtx[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];
    

那么 C 标准的哪一部分明确允许这样做呢?(为了讨论,我们假设

编辑

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

4

4 回答 4

15

所有数组(包括多维数组)都是无填充的。即使从未明确提及,也可以从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] + 3andmtx[1] + 0保证比较相等(参见第 6.5.9 节,第 6 节),但它们在语义上是不同的。例如,前者不能被取消引用,因此指向mtx[1].

于 2011-06-09T10:46:26.297 回答
10

您想要执行的访问类型的唯一障碍是类型的对象int [5][3]并且int [15]不允许相互别名。因此,如果编译器知道类型指针指向前者int *的数组之一,它可能会施加数组边界限制,从而阻止访问该数组之外的任何内容。int [3]int [3]

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

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

于 2011-06-09T13:15:42.087 回答
1
  1. 确保数组元素之间没有填充。

  2. 提供了以小于完整地址空间的大小进行地址计算的规定。例如,这可以在 8086 的巨大模式中使用,这样如果编译器知道您无法跨越段边界,则段部分不会总是更新。(我要提醒我使用的编译器是否从中受益已经太久了)。

使用我的内部模型——我不确定它是否与标准模型完全一样,而且检查起来太痛苦了,信息散布在各处——

  • 你在做什么clearBottomRightElement是有效的。

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

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

  • int *p = &row[7];不编译(gcc 同意我的观点)

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

于 2011-06-09T10:12:55.597 回答
-2

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

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

数组下标

连续的下标运算符指定多维数组对象的一个​​元素。如果 E 是维度为 i × j × 的 n 维数组 (n ≥ 2)。. . × k,则 E(用作左值以外的值)被转换为指向 (n − 1) 维数组的指针,维数为 j × 。. . × ķ。如果一元 * 运算符显式地或作为下标的结果隐式地应用于此指针,则结果是指向的 (n - 1) 维数组,如果用作左值以外的数组,它本身将转换为指针. 由此得出,数组以行优先顺序存储(最后一个下标变化最快)。

数组类型

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

于 2011-06-09T10:19:12.070 回答