【问题标题】:Pointer arithmetics with two different buffers具有两个不同缓冲区的指针算法
【发布时间】:2019-06-21 10:07:55
【问题描述】:

考虑以下代码:

int* p1 = new int[100];
int* p2 = new int[100];
const ptrdiff_t ptrDiff = p1 - p2;

int* p1_42 = &(p1[42]);
int* p2_42 = p1_42 + ptrDiff;

现在,标准是否保证p2_42 指向p2[42]?如果不是,在 Windows、Linux 或 webassembly 堆上总是如此吗?

【问题讨论】:

  • 甚至不能保证 int 对象与 sizeof(int) 对齐(我知道的所有 ABI 都是这种情况,但几乎所有编程规则都有例外,所以一些 ABI 可能不是那样);如果不是这样,代码显然不能保证工作。
  • @curiousguy 除了性能之外,没有特别的理由不对齐英特尔的字节边界。如果我们在实践中使用struct i5 { int i[5]; }; 而不是intp1p2 将不会与sizeof(i5) 对齐。

标签: c++ pointers language-lawyer pointer-arithmetic


【解决方案1】:

添加标准报价:

expr.add#5

当两个指针表达式PQ相减时,结果的类型是实现定义的有符号整数类型;此类型应与 <cstddef> 标头 ([support.types]) 中定义为 std::ptrdiff_­t 的类型相同。

  • (5.1) 如果PQ 都计算为空指针值,则结果为0。

  • (5.2) 否则,如果PQ分别指向同一个数组对象x的元素x[i]x[j],则表达式P - Q的值为i−j

    李>
  • (5.3) 否则,行为未定义。 [ 注意:如果值 i−j 不在 std::ptrdiff_­t 类型的可表示值范围内,则行为未定义。 —— 尾注 ]

(5.1) 不适用,因为指针不是空指针。 (5.2) 不适用,因为指针不在同一个数组中。所以,我们剩下 (5.3) - UB。

【讨论】:

  • 5.2 如果你有一个特殊的分配器(我认为)可以适用
  • @sudorm-rfslash:危险区域。数组是对象,但分配器只创建存储而不是对象。这两个数组是两个不同的对象。在这两者之间,无论使用何种分配器,实现都可能为其自己的开销保留空间。通常,实现存储要销毁的元素的数量。 (关于数组如何正式地逐个元素增长存在一些标准争论,但这主要是std::vector 的事情。new[100] 是一次性操作)
  • @sudorm-rfslash 5.2 甚至不适用于多维数组的 2 个不同的子数组(一个完整对象的子对象)(例如 int a[2][3]; &a[1][0] - &a[0][2]; 是 UB),并且您希望它适用于 2在同一个缓冲区中创建完整的数组对象(例如unsigned char 的数组)...
  • @Joker_vD:这不保证有意义。 uintptr_t 有足够的位来保存指针值,就是这样。
  • @curiousguy 指针值来源仅在您进行整数运算以颠覆指针运算中关于 UB 的规则时才相关。如果您不尝试从整数转换回指针,则不存在 UB — 如果您在汇编中编写它,您只会得到积分结果。
【解决方案2】:
const ptrdiff_t ptrDiff = p1 - p2;

这是未定义的行为。仅当两个指针指向同一数组中的元素时,它们之间的减法才被明确定义。 ([expr.add] ¶5.3)。

当两个指针表达式PQ相减时,结果的类型是实现定义的有符号整数类型;此类型应与 <cstddef> 标头 ([support.types]) 中定义为 std::ptrdiff_­t 的类型相同。

  • 如果 PQ 都计算为空指针值,则结果为 0。
  • 否则,如果 P 和 Q 分别指向同一数组对象 x 的元素 x[i]x[j],则表达式 P - Q 的值是 i−j
  • 否则,行为未定义

即使有一些假设的方法可以合法地获得这个值,即使是求和也是非法的,因为即使是指针+整数求和也被限制在数组的边界内 ([expr.add] ¶4.2)

当一个整数类型的表达式J被添加到一个指针类型的表达式P或从中减去时,结果的类型为P

  • 如果 P 的计算结果为空指针值,而 J 的计算结果为 0,则结​​果为空指针值。
  • 否则,如果P 指向具有n 个元素的数组对象x 的元素x[i],则81 表达式P + JJ + P(其中J 具有值j) 指向(可能是假设的)元素x[i+j] 如果0≤i+j≤n 和表达式P - J 指向(可能是假设的)元素x[i−j] 如果0≤i−j≤n
  • 否则,行为未定义。

【讨论】:

  • 标准是否有理由让您创建一个指向数组末尾之后的元素的指针?
  • @Vaelus 这使得编写在每一步增加一个指针的循环变得更加容易。例如,否则for (char *x = xs; x < (xs + sizeof(xs)); x++) {...} 将是非法的,因为它在中止之前将 x 递增到其数组的末尾。
  • @amalloy 将是非法的,因为它在中止之前将 x 递增到其数组的末尾它会在第一次递增之前变为非法 - 在xs + sizeof(xs)
  • @LanguageLawyer 但那是explicitly allowed,还是我误读了?您可以指向假设的数组的最后一个元素(只要您不取消引用),因此允许 xs + sizeof(xs)x 等于该值。
  • @MaxLanghof:AFAICT LanguageLawyer 只是说,_if xs + sizeof(xs) 是非法的(但不是),即使在第一次评估条件时,你也会得到 UB,就在递增之前,如这是第一次评估xs + sizeof(xs) 子表达式。话虽如此,如上所示,明确允许创建指向“过去最后一个”元素的指针(只要您不取消引用它)并且是常见的习惯用法。
【解决方案3】:

第三行是未定义的行为,因此标准允许之后的任何内容。

只有两个指向(或之后)同一个数组的指针相减才是合法的。

Windows 或 Linux 并不真正相关;编译器,尤其是它们的优化器会破坏你的程序。例如,优化器可能会识别出p1p2 都指向int[100] 的开头,因此p1-p2 必须为0。

【讨论】:

  • 由于第三行是未定义的行为,标准也允许任何 before :(
【解决方案4】:

该标准允许在内存被划分为离散区域的平台上实现,这些区域不能使用指针算法相互访问。举个简单的例子,一些平台使用 24 位地址,其中包含一个 8 位存储库编号和一个存储库内的 16 位地址。向标识银行最后一个字节的地址添加一个将产生指向该相同银行的第一个字节的指针,而不是下一个银行的第一个字节。这种方法允许使用 16 位数学而不是 24 位数学计算地址算术和偏移量,但要求没有对象跨越存储体边界。这样的设计会给malloc 带来一些额外的复杂性,并且可能会导致比其他情况更多的内存碎片,但用户代码通常不需要关心将内存划分为银行。

许多平台没有这样的架构限制,并且一些为此类平台上的低级编程而设计的编译器将允许在任意指针之间执行地址运算。该标准指出,处理未定义行为的常用方法是“在翻译或程序执行期间以环境特征的记录方式表现”,并且在支持它的环境中支持广义指针算法将非常适合该类别。不幸的是,该标准未能提供任何方法来区分以这种有用方式表现的实现和那些没有表现的实现。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-10-22
    • 1970-01-01
    • 2010-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多