考虑以下代码:
int a[25][80];
a[0][1234] = 56;
int* p = &a[0][0];
p[1234] = 56;
第二行代码是否会引发未定义行为?第四行呢?
考虑以下代码:
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)
。根据[expr.add]/4(这里我引用了最新的草案,在 OP 提出时可以参考这个评论,结论是一样的):
如果表达式
P
指向具有 n 个元素的数组对象 x 的元素 x[i],则表达式P + J
和J + P
(其中J
的值为 j)指向(可能是虚构的)元素 x[i+j],如果 0≤i+j≤n,则行为定义;否则,行为未定义。
由于 a[0]
(衰减为指向 a[0][0]
的指针)/p
指向 a[0]
(作为一个数组)的一个元素,而 a[0]
只有大小为 80,因此行为未定义。
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);
}
int f() { int i = 2; g(); return i; }
为 int f() { g(); return 2; }
?所有合理的实现都可以这样做,即使 g
可以猜测 i
的地址并访问它,因为_无论值是如何获得的_。但_无论值是如何获得的_并不是字面上的解释。这是一个不幸的措辞。 - Language Lawyer这取决于解释。虽然数组的连续性要求在多维数组的布局方面不容易想象(这已经被指出过了),但请注意,当您执行p[1234]
时,您正在索引仅有80列的第零行的第1234个元素。有些人认为只有0..79是有效的索引(&p[80]
是一个特殊情况)。
来自C FAQ的信息,它是与C相关的Usenet收集智慧。(我认为C和C++在这个问题上没有区别,这非常相关。)
*
操作符的操作数”。 因此,&a[0][0] + 80
在语义上与&a[1][0]
不同,因为前者不能通过*
解引用,但后者可以。C ++没有这个文本,并且(据我所知)似乎没有回答过程式何时可以解引用越界迭代器的问题。 - M.Mb [a_minus_b + i]
。实现可以(例如在像x86这样的分段内存机器上)为任何单个对象设置最大大小,使指针数学只能在seg:off地址的偏移部分上工作,并且无法从b
的段到达a
。有关UB的更多信息。 - Peter Cordes最初的回答
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,则会陷入困境,这些都比要求它们在调用i == 10
时无声地返回2.0
更适合某些目的。
标准中没有区分这些情况。虽然标准可以包括规则,如果i >= 10
,则第二个示例会调用UB而不影响第一个示例(例如说将[N]
应用于数组不会导致其衰减为指针,而是产生必须存在于该数组中的第N个元素),但标准依赖于实现即使不需要这样做也可以以有用的方式运行,并且编译器编写者应该能够识别像第一个示例这样的情况,当这样做将有利于他们的客户时。
由于标准从未试图完全定义程序员需要对数组执行的所有操作,因此不应将其视为指导优质实现应支持哪些构造的指南。
原始回答
int a[10][10]; ... a[i][j]=23;
,如果 j
超过了9,则允许编译器发出警报在某些情况下是有用的。这绝不意味着他们不同样地认识到,能够对像上面的第一个例子这样的代码进行有意义的处理也同样有用。从根本上说,他们认识到程序员(并通过其扩展认识到,尊重 C 精神“信任程序员”的编译器编写者)比委员会更适合知道每种方法何时最好帮助他们“做需要做的事情”。 - supercat由于下标越界(第2行)和类型不兼容(第3行),您的编译器将抛出一堆警告/错误,但只要实际变量(在这种情况下是int)是内置基本类型之一,这在C和C++中是安全的。 (如果变量是类/结构体,在C中可能仍然有效,但在C++中则无法确定。)
为什么要这样做... 对于第一个变量:如果您的代码依赖于这种摆弄,它将很容易出错,并且长期维护困难。
我可以看到第二个变量有一些用途,当性能优化2D数组循环时,通过用1D指针运行数据空间来替换它们,但是一个好的优化编译器通常也会自动完成此操作。 如果循环体非常大/复杂,编译器无法优化/替换循环以进行1D运行,则手动执行此操作带来的性能提升可能也不会显著。
reshape
函数非常接近。 - Christopher Creutzig你可以自由地重新解释内存。只要多个不超过线性内存即可。你甚至可以将a移动到12、40并使用负索引。
a
所引用的内存既是一个 int[25][80]
也是一个 int[2000]
。 标准中说:3.8p2:
【注:数组对象的生存期从获得适当大小和对齐方式的存储开始,到其占用的存储被重用或释放为止。 12.6.2 描述了基类和成员子对象的生存期。—注解结束】
a
具有特定类型,是类型为 int[25][80]
的 lvalue。 但是,p
只是一个 int*
。 它并不是指向 int[80]
或类似内容的 "int*
"。 因此,实际上指向的 int
是名为 a
的 int[25][80]
的元素,也是占用同一空间的 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
的遍历。
int[25][80]
的元素是int[80]
。不是 int[2000]
对象。我不确定你引用的文本与此有何关联;一个聚合及其子对象都同时分配和释放。 - M.Mint[2000]
对象呢?(显然,“获得了适当大小和对齐的存储空间”已经得到满足) - Ben Voigtp
的类型是 int*
,记得吗?而且一个 int
对象肯定占据完全相同的存储空间。第4点排除了基类子对象,但不排除成员子对象或数组元素。 - Ben Voigtint
子对象,因此不存在严格别名违规问题。 - M.M
int a[25][80]
的大小为80*25*sizeof(int)
字节,在大多数系统上可能是80*25*4
字节。 - Todd Lehmansizeof
的标准: “当应用于具有数组类型的操作数时,结果是数组中的总字节数。 当应用于具有结构或联合类型的操作数时,结果是该对象中的总字节数,包括内部和尾部填充。” - fredoverflow