对多维数组的一维访问:它是明确定义的行为吗?
Posted
技术标签:
【中文标题】对多维数组的一维访问:它是明确定义的行为吗?【英文标题】:One-dimensional access to a multidimensional array: is it well-defined behaviour? 【发布时间】:2011-09-11 13:38:03 【问题描述】:我想我们都同意,通过以一维方式取消引用指向其第一个元素的(可能是偏移的)指针来访问真正的多维数组被认为是惯用的 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!特别是:
标准是否保证编译器不会在中间添加填充,例如mtx[0][2]
和 mtx[1][0]
?
通常,从数组的末尾开始索引(除了末尾的一个以外)是未定义的(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 标准的哪一部分明确允许这样做呢? (为了讨论,我们假设c99。)
编辑
请注意,我毫不怀疑这在所有编译器中都可以正常工作。我要查询的是标准是否明确允许这样做。
【问题讨论】:
发表评论,因为我不确定。 AFAIK 数组保证在内存中是连续的,而结构可能在这些成员之间有填充。如果您查看数组访问的汇编代码,您应该能够看到对[][]
访问执行的操作与*(array + x * index + y)
相同。
我不是语言律师,所以我不会添加答案,但是,这正是光栅成像的工作原理。基本上你只有字节,你知道一行有多少字节。要转到下一行,您必须用行数*宽度偏移原始指针。所以在定义明确的数据的情况下,我会说这是非常好的编码。
@Wouter:哦,我毫不怀疑这很好!我每天都在使用这个原则,其他人也是如此。我纯粹是从语言律师学究的角度提问!
@Oli:好吧,律师是可怕的开发人员。在内存中,数组没有填充,因此,将多维数组索引为一维将始终有效。您的指针增量由基本数组指针确定,因此 arr[10] 必须是 arr + 10 * sizeof(arr) ,这在我确定的规范中。这意味着 arr[1][5] 的第二维总是 5 long 是:arr + 1 * 5 * sizeof(arrType) + 5 * sizeof (arrType)
...
我没有时间把它写出来,但 C99 6.5.2.1 第 3 和第 4 段似乎很好地定义了这一点
【参考方案1】:
所有数组(包括多维数组)都是无填充的。即使从未明确提及,也可以从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]
的第一个元素的指针(使第二个加法良好-defined) 不正确:
尽管 mtx[0] + 3
和 mtx[1] + 0
保证比较相等(参见第 6.5.9 节,第 6 节),但它们在语义上是不同的。例如,前者不能被取消引用,因此 not 指向 mtx[1]
的元素。
【讨论】:
我同意你所说的大部分内容。我不确定我是否同意(mtx[0] + 3) + 2)
有效的概念,因为所有越界指针添加都可以递归地表示为(((p+1)+1)+1)
等。如果以这种方式表达它们是明确定义的,那么 6.5.6/8 的意义何在?
@Oli:C 算术不是关联的 - (a+b)+c
不一定与 a+(b+c)
相同;问题的症结在于,在多维数组的情况下,一个指针可以同时“属于”两个数组,并且指针算法不会跟踪原始数组,因此您只需验证每个子表达式;据我所知,确实可以通过单步增量迭代多维数组
@Christoph:我同意你关于关联性的观点。我想剩下的唯一一点是使用指向前一个对象的最后一个元素的指针来别名对象是否有效。例如,在我的结构示例中,是否为保证row
和other
之间没有填充的实现明确定义了行为?
@Oli:在重新阅读第 6.5.6 节和进一步思考后,我改变了主意;)经过数组最后一个元素的指针是“特殊的”,不能在我最初描述的方式
是的,指针添加是潜在的问题。请注意,如果基类型是 char
类型,这不是问题,因为任何指向它的指针也是指向 representation 数组的指针,该数组是 unsigned char [sizeof whole_multi_dim_array]
类型的数组,并且因此所有的算术都是有效的。【参考方案2】:
您想要进行这种访问的唯一障碍是int [5][3]
和int [15]
类型的对象不允许相互别名。因此,如果编译器知道 int *
类型的指针指向前者的 int [3]
数组之一,它可能会施加数组边界限制,从而阻止访问 int [3]
数组之外的任何内容。
您可以通过将所有内容放入包含int [5][3]
数组和int [15]
数组的联合中来解决此问题,但我真的不清楚人们是否使用联合黑客来进行类型双关实际上是明确定义的。这种情况的问题可能会稍微少一些,因为您不会对单个单元格进行类型双关,只有数组逻辑,但我仍然不确定。
需要注意的一个特殊情况:如果您的类型是unsigned char
(或任何char
类型),则将多维数组作为一维数组进行访问将是完美定义的。这是因为与它重叠的unsigned char
的一维数组被标准明确定义为对象的“表示”,并且固有地允许给它起别名。
【讨论】:
通过联合进行类型双关语的定义并不比通过指针转换定义更多,但 GCC 的文档超出了前者的标准,并保证程序将按照程序员的期望进行。 “即使使用 -fstrict-aliasing,也允许使用类型双关语,前提是通过联合类型访问内存。” gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html @Pascal:C99 允许通过联合进行类型双关语 - 这在脚注 82(第 73 页)中明确提到,它是随 TC3 添加的 我希望有人会回答“不不!你错了!看看标准在这里明确允许它......”,但显然不是。所以我接受了这个答案,因为这个问题(关于别名)最简洁。 请注意,标准中的附录J.2明确列出了这种OOB多维数组访问作为UB的示例。 我同意这个答案,除了声称字符类型可以摆脱它的部分。特别是当一个字符指针递增指向另一个对象时,在这种情况下是外部数组的另一个元素。您能否详细说明或提供标准引文。 6.5 对 char 没有任何例外。谢谢。【参考方案3】:确定数组元素之间没有填充。
提供了以小于完整地址空间的大小进行地址计算的规定。例如,这可以在 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,这可能是这里的情况,或者我可能错过了一些东西)。
【讨论】:
你说得对,int *p = &row[7]
的语法错误。我将编辑我的问题。
我真正要寻找的是基于标准中措辞的论点...【参考方案4】:
我对@987654321@ 的理解是,没有要求多维数组必须在内存中以连续的顺序排列。遵循我在标准中找到的唯一相关信息(每个维度保证是连续的)。
如果你想使用 x[COLS*r + c] 访问,我建议你坚持一维数组。
数组下标
连续的下标运算符指定多维数组对象的一个元素。 如果 E 是维度为 i × j × 的 n 维数组 (n ≥ 2)。 . . × k,然后 E(用作 除了左值)被转换为指向(n - 1)维数组的指针 尺寸 j × . . . × ķ。如果一元 * 运算符显式应用于此指针,或者 作为下标的隐式结果,结果是指向的 (n − 1) 维数组, 如果用作左值以外的其他值,则它本身将转换为指针。由此而来 数组以行优先顺序存储(最后一个下标变化最快)。
数组类型
——数组类型描述了一个连续分配的非空对象集 特定的成员对象类型,称为元素类型。 36) 数组类型是 以它们的元素类型和数组中元素的数量为特征。一个 数组类型据说是从它的元素类型派生的,如果它的元素类型是 T ,则 数组类型有时称为“T 数组”。数组类型的构造从 元素类型称为“数组类型推导”。
【讨论】:
没错,所以如果你把一个连续的内存缓冲区当作多维数组,那很好,但反过来可能就不好了。这听起来对我来说是正确的。 @nimrodm:您对标准的解释与我的基本一致。 (我猜这让人放心!) 第一句话是公然错误的。内存中的布局完全由sizeof
的语义和指针算法决定。只是由于别名规则,这种用法是未定义的,因此仅适用于非字符类型。
我不同意不需要连续的多维。在具有 3 个元素的数组 (arr[3][3]) 中具有 3 个元素的数组必须是连续的以匹配描述,否则将不允许第二个“数组”(包含其他 3 个数组的那个)调用本身是一个数组,因为它的布局不会是连续的。 “内部”数组 (arr[3]) 包含 X 数组,而“外部”数组是 X[sizeof("inner array")] 数组。
是后者 - int[3]
对象的数组。以上是关于对多维数组的一维访问:它是明确定义的行为吗?的主要内容,如果未能解决你的问题,请参考以下文章