为什么越界指针算术未定义的行为?

时间:2012-05-06 19:32:26

标签: c++ undefined-behavior

以下示例为from Wikipedia

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5;  // undefined behavior

如果我从不取消引用p,那么为什么arr + 5单独的未定义行为?我希望指针表现为整数 - 除了取消引用时,指针的值被视为内存地址。

7 个答案:

答案 0 :(得分:21)

那是因为指针的行为不像整数。它是未定义的行为,因为标准是这样说的。

在大多数平台上(如果不是全部),如果不取消引用数组,则不会发生崩溃或遇到可疑行为。但是,如果你没有取消引用它,那么添加的重点是什么?

那就是说,请注意,在数组末尾的表达式在技术上是100%“正确”并且保证不会因C ++ 11规范的§5.7¶5而崩溃。但是,该表达式的结果是未指定(只保证不会溢出);而任何其他表达式超过数组边界超过一个是显式 undefined 行为。

注意:这并不意味着从一个一个偏移量读取和写入是安全的。您可能编辑不属于该阵列的数据,并且导致状态/内存损坏。你不会导致溢出异常。

我的猜测就是这样,因为它不仅解除引用错误。还有指针算术,比较指针等等。所以更容易说不要这样做而不是枚举它可能危险的情况。

答案 1 :(得分:15)

原始x86可能会出现此类声明的问题。在16位代码上,指针是16 + 16位。如果向低16位添加偏移量,则可能需要处理溢出并更改高16位。这是一个缓慢的操作,最好避免。

在这些系统上,如果偏移量在范围内(< =数组大小),则array_base+offset保证不会溢出。但是如果数组只包含3个元素,array+5会溢出。

这种溢出的结果是你有一个指针,它不会指向后面数组,但之前。这甚至可能不是RAM,而是内存映射硬件。 C ++标准不会试图限制在构造指向随机硬件组件的指针时发生的情况,即它是真实系统上的未定义行为。

答案 2 :(得分:4)

“未定义的行为”并不意味着它必须在该行代码上崩溃,但它确实意味着您无法保证结果。例如:

int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // I guess this is allowed to crash, but that would be a rather 
                  // unusual implementation choice on most machines.

*p; //may cause a crash, or it may read data out of some other data structure
assert(arr < p); // this statement may not be true
                 // (arr may be so close to the end of the address space that 
                 //  adding 5 overflowed the address space and wrapped around)
assert(p - arr == 5); //this statement may not be true
                      //the compiler may have assigned p some other value

我确信你可以在这里投入很多其他的例子。

答案 3 :(得分:3)

如果arr恰好位于机器内存空间的末尾,则arr+5可能位于该内存空间之外,因此指针类型可能无法表示该值,即它可能会溢出,溢出未定义。

答案 4 :(得分:2)

某些系统,非常罕见的系统,我不能命名一个,当你增加这样的边界时会引起陷阱。此外,它允许提供边界保护的实现......再次,虽然我想不出一个。

基本上,你不应该这样做,因此你没有理由说明当你这样做时会发生什么。指定发生的事情会给实施提供者带来不必要的负担。

答案 5 :(得分:0)

您看到此结果的原因是x86的基于段的内存保护。我发现这种保护是合理的,因为当您增加指针地址并进行存储时,这意味着在将来的代码中,您将取消引用指针并使用该值。因此,编译器希望避免这种情况,您最终将更改其他人的内存位置或删除代码中其他人拥有的内存。为了避免这种情况,编译器提出了限制。

答案 6 :(得分:0)

除硬件问题外,另一个因素是试图捕获各种编程错误的实现的出现。尽管如果配置成捕获已知不使用程序的构造,许多这样的实现可能是最有用的,即使它们是由C标准定义的,该标准的作者也不想定义将- -在许多编程领域 –都是错误的征兆。

在许多情况下,比起以某种方式记录指针不能用于访问它们标识的存储但可以对其进行修改这样的事实,诱使使用指针算术计算意外对象地址的操作要容易得多。他们可以访问其他存储。除较大(二维)数组中的数组外,将允许实现保留“恰好”超出每个对象末尾的空间。给定类似doSomethingWithItem(someArray+i);的东西,实现可以捕获任何尝试传递任何不指向数组元素或最后一个元素之后的空格的地址的尝试。如果为额外的未使用元素分配someArray保留空间,并且doSomethingWithItem()仅访问它接收到指针的项目,则该实现可以相对廉价地确保上述代码的任何非陷阱执行在最坏的情况下,可以访问原本未使用的存储。

计算“仅过去”地址的能力使边界检查比其他方式更加困难(最常见的错误情况是将doSomethingWithItem()指针传递到数组的末尾,但是行为除非doSomethingWithItem会尝试取消引用该指针(调用者可能无法证明的东西),否则它将被定义。但是,由于该标准允许编译器在大多数情况下保留超出数组的空间,因此,这种允许将允许实现限制未捕获错误所造成的破坏-如果允许使用更通用的指针算法,这可能不切实际。 / p>