以下示例来自维基百科。
int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // undefined behavior
如果我从不取消引用 p,那么为什么 arr + 5 单独是未定义的行为?我希望指针表现得像整数 - 除了取消引用时指针的值被视为内存地址。
以下示例来自维基百科。
int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // undefined behavior
如果我从不取消引用 p,那么为什么 arr + 5 单独是未定义的行为?我希望指针表现得像整数 - 除了取消引用时指针的值被视为内存地址。
那是因为指针的行为不像整数。这是未定义的行为,因为标准是这样说的。
然而,在大多数平台上(如果不是全部),如果您不取消引用数组,您将不会崩溃或遇到可疑行为。但是,如果您不取消引用它,那么添加的意义何在?
也就是说,请注意,在数组末尾加一个的表达式在技术上是 100%“正确”的,并且保证不会按照 C++11 规范的§5.7 ¶5 崩溃。但是,该表达式的结果是未指定的(只是保证不会溢出);而超过数组边界的任何其他表达式都是明确未定义的行为。
注意:这并不意味着从一倍偏移量读取和写入是安全的。您可能会编辑不属于该数组的数据,并且会导致状态/内存损坏。你只是不会导致溢出异常。
我的猜测是这样的,因为它不仅取消引用是错误的。还有指针算术、比较指针等。所以说不要这样做比列举可能危险的情况更容易。
原始的 x86 可能会出现此类语句的问题。在 16 位代码上,指针是 16+16 位。如果向低 16 位添加偏移量,则可能需要处理溢出并更改高 16 位。这是一个缓慢的操作,最好避免。
在这些系统array_base+offset
上,如果偏移量在范围内(<=数组大小),保证不会溢出。但array+5
如果数组仅包含 3 个元素,则会溢出。
溢出的结果是你得到了一个指针,它没有指向数组后面,而是在前面。这甚至可能不是 RAM,而是内存映射硬件。如果您构造指向随机硬件组件的指针,C++ 标准不会尝试限制发生的情况,即它是真实系统上的未定义行为。
如果arr
恰好在机器内存空间的末尾,那么arr+5
可能在该内存空间之外,因此指针类型可能无法表示该值,即它可能会溢出,并且溢出是未定义的。
“未定义的行为”并不意味着它必须在该代码行上崩溃,但它确实意味着您无法对结果做出任何保证。例如:
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
我敢肯定,您还可以在这里举出许多其他示例。
一些系统,非常罕见的系统,我无法命名,当你像这样增加边界时会导致陷阱。此外,它允许存在提供边界保护的实现......虽然我想不出一个。
本质上,您不应该这样做,因此没有理由指定您这样做时会发生什么。指定会发生什么会给实施提供者带来不必要的负担。
您看到的这个结果是因为 x86 的基于段的内存保护。我发现这种保护是合理的,因为当您增加指针地址并存储时,这意味着在您的代码中的未来时间点,您将取消引用指针并使用该值。因此编译器希望避免这种情况,您最终会更改其他人的内存位置或删除代码中其他人拥有的内存。为了避免这种情况的编译器已经设置了限制。
除了硬件问题,另一个因素是尝试捕获各种编程错误的实现的出现。尽管如果配置为捕获程序已知不使用的构造,许多这样的实现可能是最有用的,即使它们是由 C 标准定义的,标准的作者不想定义构造的行为,这些行为会 - -在许多编程领域——出现错误。
在许多情况下,捕获使用指针算法来计算非预期对象地址的操作要比以某种方式记录指针不能用于访问它们所标识的存储的事实要容易得多,但可以对其进行修改以便它们可以访问其他存储。除了较大(二维)数组中的数组之外,允许实现保留“刚刚过去”每个对象末尾的空间。给定类似的东西doSomethingWithItem(someArray+i);
,一个实现可以捕获任何试图传递任何不指向数组元素或刚刚超过最后一个元素的空间的地址的尝试。someArray
如果为额外的未使用元素分配保留空间,并且doSomethingWithItem()
仅访问它接收到的指针的项目,该实现可以相对便宜地确保上述代码的任何非陷阱执行可以 - 在最坏的情况下 - 访问其他未使用的存储。
计算“刚刚过去”地址的能力使得边界检查比其他情况更加困难(最常见的错误情况是将doSomethingWithItem()
指针传递到数组末尾,但除非doSomethingWithItem
尝试取消引用,否则行为将被定义那个指针——调用者可能无法证明的东西)。因为标准允许编译器在大多数情况下保留刚刚超过数组的空间,但是,这种允许将允许实现限制未捕获的错误造成的损害——如果允许更通用的指针算术,这可能是不切实际的。