假设我有一个这样的数组:
int val[10];
我故意用从负值到高于 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];
}
有关基于开发人员没有做任何禁止的事情这一事实的假设的正向和反向传播的更多有趣故事,请参阅每个 C 程序员都应该了解的未定义行为:第 2 部分。
编辑:
可能的解决方法:如果您知道您将访问-M
您+N
可以:
int values[M + 10 + N]
values[M + i]
5.2.1/1 说
[...] 表达式 E1[E2] 与 *((E1)+(E2)) 相同(根据定义)
因此,val[i]
等价于*((val)+i))
。由于val
是一个数组,因此数组到指针的转换(4.2/1)发生在执行加法之前。因此,val[i]
等价于*(ptr + i)
where ptr
is a int*
set to &val[0]
。
然后,5.7/2 解释了ptr + i
指向什么。它还说(重点是我的):
[...] 如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则评估不应产生溢出;否则,行为是 undefined。
在 的情况下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);
which 可能是未定义的行为(因为它取消引用指向 的最后一个元素的指针)或与定义良好val
的相同。int* p = ptr + 10;
我发现这两个参考资料表明这个问题是多么模糊:
如果你把它放在一个带有一些填充整数的结构中,它应该是安全的(因为指针实际上指向“已知”的目的地)。但最好避免它。
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];
}