【问题标题】:Multidimensional array indexing using pointer to elements使用指向元素的指针进行多维数组索引
【发布时间】:2016-12-16 17:30:24
【问题描述】:

据我所知,栈上的多维数组会按行顺序占用连续的内存。根据 ISO C++ 标准使用指向元素的指针来索引多维数组是未定义的行为吗?例如:

#include <iostream>
#include <type_traits>
int main() {
  int a[5][4]{{1,2,3,4},{},{5,6,7,8}};
  constexpr auto sz = sizeof(a) / sizeof(std::remove_all_extents<decltype(a)>::type);
  int *p = &a[0][0];
  int i = p[11];  // <-- here
  p[19] = 20;  // <-- here
  for (int k = 0; k < sz; ++k)
    std::cout << p[k] << ' ';  // <-- and here
  return 0;
}

如果指针没有超出数组a 的边界,上述代码将正确编译并运行。但这是因为编译器定义的行为或语言标准而发生的吗?任何来自 ISO C++ 标准的参考都是最好的。

【问题讨论】:

  • 嗯,我找不到任何与 ISO C++ 标准直接相关的内容。但是,是的,自动分配的数组保证连续存储在内存中。当您在简单指针上使用索引运算符(即p[11])时,它等效于*(p+11),因此如果存在*p 类型的合法数据,则定义行为。
  • 我认为[expr.add]/5 可能会禁止这样做,但[dcl.array]/1 确实保证存储是连续的。
  • @Yakk 从指针增量和索引运算符的定义中不言而喻。从技术上讲,C++ 标准保证,您只需成为一名律师即可正确阅读。多维数组是数组的数组(数组等)。因此,增加数组的索引会导致增量等于数组元素的大小,即子数组元素乘以子数组的长度……以此类推
  • @Swift 不,超过任何数组末尾的访问都是不合法的。即使你知道那里有什么。
  • @Swift 有一个由 4 个 5 个整数组成的数组。没有int[20] 类型的变量:20 个整数的数组。

标签: c++ c++11


【解决方案1】:

这里的问题是 严格的别名规则,它存在于我的草案 n3337 for C++11 in 3.10 Lvalues and rvalues [basic.lval] § 10 中。这是一个详尽的列表,没有明确允许将多维数组别名为整个大小的一维数组。

因此,即使确实需要在内存中连续分配数组,这也证明了多维数组的大小,例如T arr[n][m] 是维度乘以元素大小的乘积:n * m *sizeof(T) .当转换为 char 指针时,您甚至可以对整个数组进行算术指针操作,因为任何指向对象的指针都可以转换为 char 指针,并且该 char 指针可用于访问对象的连续字节(*) .

但不幸的是,对于任何其他类型,标准只允许在一个数组内进行算术指针操作(并且根据定义取消引用数组元素与取消引用指针相同在指针算术之后:a[i] *(a + i))。因此,如果您同时遵守指针运算规则和严格的别名规则,则 C++11 标准未定义多维数组的全局索引,除非您通过 char 指针运算:

int a[3][4];
int *p = &a[0][0]; // perfectly defined
int b = p[3];      // ok you are in same row which means in same array
b = p[5];          // OUPS: you dereference past the declared array that builds first row

char *cq = (((char *) p) + 5 * sizeof(int)); // ok: char pointer arithmetics inside an object
int *q = (int *) cq; // ok because what lies there is an int object
b = *q;            // almost the same as p[5] but behaviour is defined

char 指针算法 以及对破坏大量现有代码的恐惧解释了为什么所有众所周知的编译器都默默地接受具有相同全局大小的一维数组的多维数组的别名(它导致相同的内部代码),但从技术上讲,全局指针算法仅对 char 指针有效。


(*) 标准在 1.7 中声明的 C++ 内存模型 [intro.memory] ​​

C++ 内存模型中的基本存储单元是字节... C++ 程序可用的内存由一个或多个连续字节序列组成。每一个 byte 有一个唯一的地址。

3.9 之后的类型 [basic.types] §2

对于普通可复制类型 T 的任何对象(基类子对象除外),无论该对象是否 持有类型 T 的有效值,构成对象的底层字节可以复制到数组中 字符或无符号字符。

要复制它们,您必须通过char *unsigned char * 访问它们

【讨论】:

  • 我认为技术上没有混叠。我们有一个指向int*p 中大小为5 的数组的第一个元素的指针。我想你是说如果指向T[a][b] 的指针被保证合法地别名为指向T[a*b] 的指针,那么操作将是隐式合法的?
  • 是什么将它限制为仅 char 指针算术?在您的示例中 (((char *) p) + 5 * sizeof(int)) 可能在任何平台上都是合法的,但是如果数学中存在错误,会将与 int 元素开头匹配的地址移出 increemt,则在严格的平台上数据对齐,例如SPARC,这将导致灾难性的情况,它是一个非法的操作码。标准应该追求更精简和通用的工作路径,为什么不能*(p+5)?
  • @Yakk:我的意思是a[i] 只有在aa+i 指向同一个数组时才是合法的。如果我们可以给T[a][b]T[a*b] 取别名,情况就会如此。但我同意你的观点,给指针起别名就足够了。
  • @Swift:我不想争论标准是否应该允许这个或那个。我只说 bytes 是一种特殊情况,它可以访问普通可复制对象的 representation...
  • 标准声明 C++ 内存模型中的基本存储单元是字节,这听起来确实很有趣,因为对于许多使用 C++ 作为编程语言的现有平台来说,这不是真的。并且 char 并不是所有的字节。在某些平台上,您无法访问与 CPU 字不对齐的地址。或者字长取决于所使用的类型。如果您在此类系统上获取 int 数组并将其向前移动一个字节,则数据将无法作为 int 值访问。
【解决方案2】:

我相信您示例中的行为在技术上是未定义的。

该标准没有多维数组的概念。您实际声明的是“由 5 个 4 个整数组成的数组的数组”。也就是说a[0]a[1]实际上是两个不同的4个int数组,都包含在数组a中。这意味着a[0][0]a[1][0] 不是同一个数组的元素。

[expr.add]/4 说以下(强调我的)

当一个具有整数类型的表达式被添加到指针或从指针中减去时,结果具有类型 的指针操作数。如果指针操作数指向数组对象的一个​​元素,并且数组是 足够大,结果指向与原始元素偏移的元素,使得 结果和原始数组元素的下标等于整数表达式。换句话说,如果 表达式 P 指向数组对象的第 i 个元素,表达式 (P)+N(等价于 N+(P)) 和 (P)-N(其中 N 的值为 n)分别指向数组的第 i + n 个和第 i - n 个元素 对象,只要它们存在。此外,如果表达式 P 指向数组对象的最后一个元素, 表达式 (P)+1 指向数组对象的最后一个元素,如果表达式 Q 指向 超过数组对象的最后一个元素,表达式 (Q)-1 指向数组的最后一个元素 目的。 如果指针操作数和结果都指向同一个数组对象的元素,或者一个过去 数组对象的最后一个元素,评估不应产生溢出;否则,行为是 未定义

因此,由于p[11] 扩展为*(p + 11),并且由于pp + 11 不是同一个数组的元素(一个是a[0] 的元素,另一个是结束后的多个元素a[0]),添加的行为是未定义的。

然而,我会非常惊讶地发现这样的添加会导致与您期望的结果不同的任何实现。

【讨论】:

  • 但是((p + 4) + 4) + 3 的定义不是很好吗,因为p + 4a[0] 的末尾,并且恰好是指向a[1][0] 的指针?我很确定对齐和填充规则保证sizeof(a[0]) == 4*sizeof(int)a[1]sizeof(a[0])a[0] 之后的字节。
  • 标准没有定义多维数组,因为它是递归定义的。把它想象成递归模板。但与类不同,数组保证以连续方式存储。如果在 int a[4][5] 数组中尝试访问 a[4][0] 元素,编译器将正确地发出警告...因为它与 *(a+20) 相同,在这个特定的case 超出数组边界。它将警告 a[0][6]。这种情况可能表明代码中有错误\拼写错误。如果索引给定运行时,编译器将不知道使用了什么值,并且通常的指针数学将起作用。
  • @DanielH 不要将实现与抽象混淆。抽象说明了需要定义的内容。指针和数组索引不是根据 C++ 中的线性内存模型定义的。相反,定义了各种操作。实现它们的一种方法是使用线性内存模型。但即使在那里,优化器也可以假定只会发生已定义的操作。一个经典的例子是 double 和 integer 的并集:修改一个然后读取另一个是 UB,即使这些位保证是重叠的。
  • @Yakk 无关示例。在联合结果的情况下,取决于平台使用的格式,值的位图。指针数学足够抽象。它不是描述如何实际上数组存储在内存中,它描述了我们可以使用指针来遍历数组元素的事实。毕竟,在许多平台上,指针是一个结构,而不是一个标量值,实现-明智的
  • @Daniel H 顺便说一下,填充不适用于数组,仅适用于 POD 结构和联合。这种情况下的填充规则是“没有填充”
【解决方案3】:

如果你声明

int  arr[3][4][5];

arr 的类型是int[3][4][5]arr[3] 的类型是int[4][5],等等。数组数组的数组,但不是指针数组。让我们看看如果我们增加第一个索引会发生什么?它会将指针向前移动数组元素的大小,但 arr 的数组元素是二维数组!相当于递增:arr + sizeof(int[4][5])/sizeof(int) 或 arr + 20。

以这种方式迭代我们会发现 arr[a][b][c] 等于 *(*(*(arr + a) + b) + c),前提是数组永远不会有任何填充(以符合 POD 类型与 C99 的强制兼容性):

*((int*)arr + 20*a +  5*b + c)

当具有整数类型的表达式被添加或减去时 从一个指针,结果具有指针操作数的类型。如果 指针操作数指向数组对象的一个​​元素,而数组 足够大,结果指向一个元素偏移量 原始元素使得下标的差异 结果和原始数组元素等于积分表达式

【讨论】:

  • @NathanOliver 抱歉,我修好了,自动更正在几乎每个带有 [ 的 int 之后添加 *
  • 问题是关于使用 int 指针,但不会将 arr 衰减为指针。
  • @Daniel H arr 衰减为指向与 &(arr 的第一个元素) 评估的指针相同的对象的指针
  • 对,但问题中的示例代码显示int *p = &amp;a[0][0](在您的int *ptr = &amp;arr[0][0][0] 答案中等效。这是一个int 指针。
  • arr[a][b][c]*(*(*(arr + a) + b) + c),不是你写的。 arr + 20 超出了arr 的范围。 arr + 3arr 的结尾。
猜你喜欢
  • 1970-01-01
  • 2022-01-02
  • 1970-01-01
  • 2013-05-12
  • 2012-05-06
  • 1970-01-01
  • 1970-01-01
  • 2015-01-17
相关资源
最近更新 更多