说我有一个这样的数组:
int val[10];
我故意用从负值到任何高于9的值来索引它,但没有以任何方式使用结果值。这可能是出于性能原因(也许在进行数组访问后检查输入索引会更有效。)
我的问题是:
答案 0 :(得分:17)
这是未定义的行为。根据定义,undefined意味着“任何事情都可能发生”。你的代码可能崩溃,它可以完美地工作,它可以带来所有人的和平与和谐。我不打赌第二次或最后一次。
答案 1 :(得分:9)
这是未定义的行为,您实际上可能会与优化器发生冲突。
想象一下这个简单的代码示例:
int select(int i) {
int values[10] = { .... };
int const result = values[i];
if (i < 0 or i > 9) throw std::out_of_range("out!");
return result;
}
现在从优化器的角度来看待它:
int values[10] = { ... };
:有效索引位于[0, 9]
。
values[i]
:i
是一个索引,因此i
位于[0, 9]
。
if (i < 0 or i > 9) throw std::out_of_range("out!");
:i
位于[0, 9]
,永不服用
因此优化器重写的函数:
int select(int i) {
int values[10] = { ... };
return values[i];
}
有关开发人员未采取任何禁止行为的事实,有关假设向前和向后传播的更多有趣故事,请参阅What every C programmer should know about Undefined Behavior: Part 2。
修改强>
可能的解决方法:如果您知道可以从-M
访问+N
,则可以:
int values[M + 10 + N]
values[M + i]
答案 2 :(得分:5)
作为verbose said,这会产生未定义的行为。接下来会更加精确。
5.2.1 / 1说
[......]表达式E1 [E2]与*((E1)+(E2))相同(按照定义)
因此,val[i]
相当于*((val)+i))
。由于val
是一个数组,因此在执行加法之前会发生数组到指针的转换(4.2 / 1)。因此,val[i]
相当于*(ptr + i)
,其中ptr
是int*
设置为&val[0]
。
然后,5.7 / 2解释了ptr + i
指向的内容。它还说(重点是我的):
[...]如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素的元素,评估不得产生溢出; 否则,行为未定义。
在ptr + i
的情况下,ptr是指针操作数,结果是ptr + i
。根据上面的引用,两者都应指向数组的元素或指向最后一个元素的元素。也就是说,在OP的情况下ptr + i
是一个明确定义的表达式,适用于所有i = 0, ..., 10
。最后,*(ptr + i)
已为0 <= i < 10
定义明确,但i = 10
未定义。
修改强>:
我很困惑val[10]
(或等价地,*(ptr + 10)
)是否会产生未定义的行为(我正在考虑C ++而不是C)。在某些情况下这是真的(例如int x = val[10];
是未定义的行为)但在其他情况下这不是那么清楚。例如,
int* p = &val[10];
正如我们所看到的,这相当于int* p = &*(ptr + 10);
,它可能是未定义的行为(因为它解除引用指向一个超过val
的最后一个元素的指针)或者与明确定义的int* p = ptr + 10;
相同。
我发现这两个参考文献显示了这个问题有多模糊:
May I take the address of the one-past-the-end element of an array?
答案 3 :(得分:3)
如果将它放在带有一些填充整数的结构中,它应该是安全的(因为指针实际上指向“已知”目标)。 但最好避免它。
struct SafeOutOfBoundsAccess
{
int paddingBefore[6];
int val[10];
int paddingAfter[6];
};
void foo()
{
SafeOutOfBoundsAccess a;
bool maybeTrue1 = a.val[-1] == a.paddingBefore[5];
bool maybeTrue2 = a.val[10] == a.paddingAfter[0];
}