我可以将2D数组视为连续的1D数组吗?

时间:2011-09-01 10:35:15

标签: c++ arrays pointers multidimensional-array undefined-behavior

请考虑以下代码:

int a[25][80];
a[0][1234] = 56;
int* p = &a[0][0];
p[1234] = 56;

第二行是否会调用未定义的行为?第四行怎么样?

7 个答案:

答案 0 :(得分:9)

是的,你可以(不,它不是UB),它是由标准间接保证的。方法如下:2D数组是一个数组数组。保证数组具有连续的内存,sizeof(array)是元素数量的sizeof(elem)倍。从中可以看出,你所要做的事情是完全合法的。

答案 1 :(得分:8)

这取决于解释。虽然数组的连续性要求在如何布局多维数组方面没有太大的想象力(之前已经指出过),但请注意,当你正在进行p[1234]时,你正在为第1234个索引编制索引。第0行的元素只有80列。有些人将唯一有效的指数解释为0..79(&p[80]是特例)。

来自C FAQ的信息,这是Usenet在与C相关的问题上的智慧(我不认为C和C ++在这个问题上有所不同,而且这非常相关。)

答案 2 :(得分:3)

两行执行会导致未定义的行为。

订阅被解释为指针添加,后跟间接,即a[0][1234] / p[1234]等同于*(a[0] + 1234) / *(p + 1234)。根据{{​​3}}(这里我引用最新的草案,虽然OP提议的时候,你可以参考[expr.add]/4,结论是相同的):

  

如果表达式P指向具有n个元素的数组对象x的元素x [i],则表达式P + JJ + P(其中J具有值j)如果0≤i+j≤n,则指向(可能是假设的)元素x [i + j];否则,行为未定义。

因为a[0](衰减为指向a[0][0])/ p的指针指向a[0](作为数组)的元素,而a[0]仅指向constexpr int f(const int (&a)[2][3]) { auto p = &a[0][0]; return p[3]; } int main() { constexpr int a[2][3] = { 1, 2, 3, 4, 5, 6, }; constexpr int i = f(a); } 大小为80,行为未定义。

作为语言律师this comment,以下计划pointed out in the comment

Cashier(cashier_id*, cashier_name)
Category(category_id*, category_name)
Product(product_id*, product_name, price, category_id**)
Purchase(product_id**, cashier_id**, amount)

* primary key
** foreign key

当编译器出现在常量表达式中时,编译器会检测到这种未定义的行为。

答案 3 :(得分:0)

使用编写标准所描述的语言,调用类似这样的函数不会有问题:

void print_array(double *d, int rows, int cols)
{
  int r,c;
  for (r = 0; r < rows; r++)
  {
    printf("%4d: ", r);
    for (c = 0; c < cols; c++)
      printf("%10.4f ", d[r*cols+c]);
    printf("\n");
  }
}
如果数组中元素的总数少于double[10][4],则在double[50][40]rows*cols或任何其他大小上

。的确,除其他事项外,还设计了保证T[R][C]的行跨度等于C * sizeof (T)的保证,从而使得可以编写适用于任意大小的多维数组的代码。

另一方面,该标准的作者认识到,当实现被赋予类似以下内容时:

double d[10][10];
double test(int i)
{
  d[1][0] = 1.0;
  d[0][i] = 2.0;
  return d[1][0]; 
}

允许他们生成假设d[1][0]执行时return仍保持1.0的代码,或者允许他们生成i大于10时将捕获的代码,与要求2.0进行静默返回i==10相比,它们将更适合于某些目的。

标准在这些情况之间没有任何区别。尽管标准可能包含了一些规则,这些规则说第二个示例在i >= 10的情况下调用UB而不影响第一个示例(例如,将[N]应用于数组不会导致它衰减为一个指针,但产生第N个元素,该元素必须存在于该数组中),而标准依赖于这样一个事实,即即使不需要这样做,也允许实现以有用的方式进行操作,并且编译器编写者应大概能够识别出第一个示例那样的情况,这将使他们的客户受益。

由于该标准从未试图完全定义程序员需要对数组进行的所有操作,因此不应寻求有关质量实现应支持的构造的指导。

答案 4 :(得分:-1)

你可以随心所欲地重新诠释记忆。只要倍数不超过线性记忆。您甚至可以将a移动到12,40并使用负索引。

答案 5 :(得分:-1)

由于下标超出范围(第2行)和无法比较的类型(第3行),您的编译器将抛出一堆警告/错误,但只要实际变量(在本例中为int)是内在基础之一-types这是在C和C ++中保存的。 (如果变量是一个类/结构,它可能仍然在C中工作,但在C ++中,所有的注意都是关闭的。)

你为什么要这样做.... 对于第一个变体:如果你的代码依赖于这种混乱,它将容易出错并且长期难以维护。

当性能优化通过在数据空间上运行的1D指针替换它们时,我可以看到第二个变体的一些用途,但优秀的优化编译器通常会自行完成。 如果循环的主体太大/太复杂,编译器就无法通过它自己的1D运行来优化/替换循环,手动执行它的性能增益很可能也不会很大。

答案 6 :(得分:-1)

a引用的内存既是int[25][80]又是int[2000]。标准说,3.8p2:

  

[注意:一旦获得具有适当大小和对齐的存储,数组对象的生命周期就开始,并且当数组占用的存储被重用或释放时,其生命周期结束。 12.6.2描述了基础和成员子对象的生命周期。 - 结束说明]

a有一个特定的类型,它是int[25][80]类型的左值。但p只是int*。它并非int*指向int[80]&#34;或类似的东西。事实上,int指向的是int[25][80]名为a的元素,也是int[2000]占据相同空间的元素。

由于pp+1234都是同一个int[2000]对象的元素,因此指针算法是明确定义的。由于p[1234]表示*(p+1234),因此它也是明确定义的。

此规则对数组生命周期的影响是您可以自由地使用指针算法在整个对象中移动。


自评论中提及std::array以来:

如果有人std::array<std::array<int, 80>, 25> a;,则不存在 a std::array<int, 2000>。确实存在int[2000]。我正在寻找需要sizeof (std::array<T,N>) == sizeof (T[N])(和== N * sizeof (T))的任何内容。如果不这样做,你必须假设可能存在间隙,这会导致嵌套std::array的遍历变得混乱。