考虑以下代码:
int a[25][80];
a[0][1234] = 56;
int* p = &a[0][0];
p[1234] = 56;
第二行调用未定义行为吗?第四行呢?
两行都导致未定义行为。
下标解释为指针加后加间接,即a[0][1234]
/p[1234]
等价于*(a[0] + 1234)
/*(p + 1234)
。根据…add]/4(这里引用的是最新的草稿,而OP被提出的时间,你可以参考这个评论,结论是一样的):
如果表达式
P
指向有n个元素的数组对象x的元素x[i],则表达式P + J
和J + P
(其中J
的值为j)如果0≤i+j≤n,则指向(可能是假设的)元素x[i+j];否则,行为是未定义的。
由于a[0]
(衰减为指向a[0][0]
的指针)/p
指向a[0]
的元素(作为数组),而a[0]
的大小只有80,因此该行为是未定义的。
正如Language Lawyer在评论中指出的,下面的程序无法编译。
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);
}
当出现在常量表达式中时,编译器会检测到此类未定义行为。
这取决于解释。虽然数组的连续性要求在如何布局多维数组方面没有留下太多的想象空间(这在前面已经指出),但请注意,当您执行p[1234]
时,您索引的是只有80列的第0行的第1234个元素。有些将唯一有效的索引解释为0..79(&p[80]
是一个特例)。
来自C常见问题解答的信息,这是Usenet关于C相关问题的智慧(我不认为C和c++在这方面有什么不同,这是非常相关的。)
在编写标准所描述的语言中,调用这样的函数是没有问题的:
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];
}
允许他们生成代码,假设return
执行时d[1][0]
仍然保持1.0,或者允许他们生成代码,如果i
大于10,则会捕获,这将使他们更适合于某些目的,而不是要求他们在使用i==10
调用时静默返回2.0
。
标准中没有对这些场景进行任何区分。虽然是可能的标准包括规则,会说第二个示例调用乌兰巴托如果i >= 10
而不影响第一个例子(例如说应用[N]
数组不会引起它衰变指针,而是收益率第n个元素,它必须存在于数组),标准而不是依赖于这样一个事实,即实现允许行为以有用的方式,即使不需要这样做,编译器编写者应该能够识别像第一个例子这样的情况,当这样做会使他们的客户受益时。
由于标准从来没有试图完全定义程序员使用数组所需要做的一切,因此不应该指望它作为指导,以确定质量实现应该支持哪些构造。
由于下标超出范围(第2行)和不兼容的类型(第3行),编译器将抛出一系列警告/错误,但只要实际变量(本例中为int)是内在基本类型之一,这在C和c++中是可以避免的。(如果变量是一个类/结构,它可能在C中仍然可以工作,但在c++中就不一样了。)
为什么要这样做....对于第一个变体:如果您的代码依赖于这种混乱,那么从长远来看,它将容易出错并且难以维护。
我可以看到第二种变体的一些用途,当性能优化循环在2D数组上通过替换它们在数据空间上运行的1D指针,但一个好的优化编译器通常会自己做。如果循环体太大/太复杂,编译器无法通过自己的一维运行来优化/替换循环,那么手动执行的性能增益很可能也不会显著。
您可以自由地以任何您喜欢的方式重新解释内存。只要倍数不超过线性存储器。您甚至可以将a移动到12,40并使用负索引。
a
引用的内存同时是int[25][80]
和int[2000]
。标准规定,3.8p2:
[注:数组对象的生命周期从获得合适大小和对齐的存储空间开始,到该数组占用的存储空间被重用或释放时结束。12.6.2描述了基对象和成员子对象的生命周期。
a
有一个特定的类型,它是int[25][80]
类型的左值。但是p
就是int*
。它不是"int*
指向int[80]
"或类似的东西。因此,实际上,int
所指向的是int[25][80]
的一个元素,名为a
,同时也是int[2000]
的一个元素,占用了相同的空间。
由于p
和p+1234
都是同一个int[2000]
对象的元素,因此指针运算是定义良好的。由于p[1234]
的意思是*(p+1234)
,它也是定义良好的。
这个规则对数组生命周期的影响是,你可以自由地使用指针算术来遍历一个完整的对象。
既然std::array
在评论中被提到了:
如果有std::array<std::array<int, 80>, 25> a;
,那么不存在 std::array<int, 2000>
。确实存在一个int[2000]
。我正在寻找任何需要sizeof (std::array<T,N>) == sizeof (T[N])
(和== N * sizeof (T)
)。如果没有这个,您必须假设可能存在间隙,这会使嵌套的std::array
的遍历变得混乱。