【问题标题】:When exactly is a pointer difference defined?究竟何时定义了指针差异?
【发布时间】:2013-12-10 12:34:24
【问题描述】:

我有一个关于指针差异和结果类型的问题,ptrdiff_t

C99 §6.5.6 (9) 说:

当两个指针相减时,两个指针都指向同一个数组对象的元素,或者指向数组对象最后一个元素的元素;结果是两个数组元素的下标之差。结果的大小是实现定义的,其类型(有符号整数类型)是在标头中定义的ptrdiff_t。如果结果在该类型的对象中不可表示,则行为未定义。换句话说,如果表达式 P 和 Q 分别指向数组对象的第 i 个和第 j 个元素,则表达式 (P)-(Q) 的值是 i-j,前提是该值适合ptrdiff_t 类型的对象。

§7.18.3 (2) 要求 ptrdiff_t 的范围至少为 [−65535, +65535]

如果结果太大,我感兴趣的是未定义的行为。我在标准中找不到任何保证至少与size_t 或类似的签名版本相同的范围。所以,现在我的问题是:一个符合要求的实现是否可以使 ptrdiff_t 成为有符号的 16 位类型但 size_t 是 64 位? [编辑:正如Guntram Blohm 指出的那样,16 位签名最多为 32767,所以我的示例显然不符合要求] 据我所知,我不能在严格符合代码的元素中对超过 65535 个元素的数组进行任何指针减法即使实现支持比这大得多的对象。此外,程序甚至可能崩溃。

基本原理 (V5.10) § 6.5.6 说:

在处理同一数组中的指针时,为获得正确的代数顺序,对这种类型 [ptrdiff_t] 进行签名很重要。但是,指针差异的大小可以与可以声明的最大对象的大小一样大;由于这是无符号类型,因此两个指针之间的差异可能会导致某些实现溢出。

这可以解释为什么不需要定义指针的每个差异(指向同一数组的元素),但它没有解释为什么没有限制 PTRDIFF_MAX 至少为 SIZE_MAX/2 (带整数除法)。

为了说明,假设T 是任何对象类型,而nsize_t 的对象,在编译时是未知的。我想为Tn 对象分配空间,并且我想对分配范围内的地址进行指针减法。

size_t half = sizeof(T)>1 ? 1 : 2; // (*)
if( SIZE_MAX/half/sizeof(T)<n ) /* do some error handling */;
size_t size = n * sizeof(T);
T *foo = malloc(size);
if(!foo) /* ... */;

不会严格遵守,我必须这样做

if( SIZE_MAX/sizeof(T) < n || PTRDIFF_MAX < n )

相反。真的是这样吗?如果是这样,有人知道这样做的原因吗(即不需要PTRDIFF_MAX &gt;= SIZE_MAX/2 [编辑:将&gt; 更改为&gt;=] 或类似的东西)?

(*) 第一个版本中的一半是我在写这篇文章时认出来的,我有

if( SIZE_MAX/2/sizeof(T) < n )

首先,取SIZE_MAX的一半来解决理由中提到的问题;但后来我意识到如果sizeof(T) 为1,我们只需要一半SIZE_MAX。鉴于此代码,第二个版本(肯定是严格符合的)似乎一点也不差。但是,如果我是对的,我还是很感兴趣。

C11 保留 §6.5.6 (9) 的措辞,也欢迎对该主题的 C++ 相关答案。

【问题讨论】:

  • 标准中的数字似乎非常适合具有分段内存的 16 位系统:每个段为 64kB 大,并且数组必须适合一个段,因此最大差异为 +/- 64k。但是,远指针需要 32 位(段选择器加上偏移量)。

标签: c arrays pointers


【解决方案1】:

给你一个标题中问题的答案:指针差异本身不能用于确定两个指针的差异而不最终导致未定义的行为。正如您所注意到的,在PTRDIFF_MAX 远小于对象的可能大小的系统上,这将是一个严格的限制。但是这样的系统很少见(我不知道有什么),所以如果你的代码依赖于能够对大对象做不同的事情,你总是放一些类似

#if PTRDIFF_MAX < SIZE_MAX/2
# error "we need an architecture with sufficiently wide ptrdiff_t"
#endif

但即使在这种情况下(太窄ptrdiff_t),您也始终能够计算同一个较大对象的两个指针之间的差异。

  1. 确定两者中的哪一个(pq)更小。这总是 定义明确。
  2. p 是较小的那个,然后测试所有p + isize_t i1 开始直到到达qiSIZE_MAX
  3. 如果最终的iSIZE_MAX 并且您没有达到q,则无法表示差异。否则,i 加上最终符号就是您要查找的信息。

这不是很令人满意,但我无法弄清楚如何将线性算法改进为对数:为了避免 UB,我们不允许通过比较超过 q

而且,正如我所说,您只需要在一些真正具有异国情调的建筑的情况下使用它。

编辑:

使用 mafso 获取指针差异的最高有效位的技巧,这可以在 O(log(n)) 中完成,其中 n 是所需的距离。首先声明两个内部函数,假设p &lt; q

// computes the maximum value bit of the pointer difference
//
// assumes that p < q
inline
uintmax_t ptrdiff_maxbit(char const* p, char const* q) {
  uintmax_t len = 1;
  while (p+len <= q-len)
    len <<= 1;
  return len;
}

// compute the pointer difference
//
// assumes that p < q
// assumes that len2 is a power of two
// assumes that the difference is strictly less than 2*len2
inline
uintmax_t ptrdiff_bounded(char const* p, char const* q, uintmax_t len2) {
  if (len2 < 2) return len2;
  uintmax_t len = (len2 >> 1);
  p += len;
  q -= len;
  for (; len; len >>= 1)
    if (p + len <= q) {
      len2 |= len;
      p += len;
    }
  return len2;
}

然后定义计算字节差的函数并添加一个约定,以防intmax_t中无法表示该差:

inline
intmax_t ptrdiff_byte(void const* p0, void const* q0) {
  char const * p = p0;
  char const * q = q0;
  if (p == q) return 0;
  if (p < q) {
    uintmax_t ret = ptrdiff_bounded(p, q, ptrdiff_maxbit(p, q));
    if (ret > (-(INTMAX_MIN+1))+UINTMAX_C(1)) return INTMAX_MIN;
    else return -ret;
  } else {
    uintmax_t ret = ptrdiff_bounded(q, p, ptrdiff_maxbit(q, p));
    if (ret > INTMAX_MAX) return INTMAX_MAX;
    else return ret;
  }
}

最后,一个适合*p类型的宏。

#define ptrdiff(P, Q) (ptrdiff_byte((P), (Q))/(intmax_t)sizeof(*Q))

【讨论】:

  • 您不需要比覆盖一半地址空间的数组更奇特的东西。在 32 位系统上从 2.5 GB char 数组的末尾开始减去肯定会溢出 PTRDIFF_MAX。但是,大多数系统确实只为用户提供最多一半的空间,所以这有点异国情调。
  • 算法+1(虽然它主要是学术兴趣);我能够将其改进为 O((log(n))^2):说 pp==q 和p+1==q(如果*p 有效,则为p+1),然后在增加k 的同时检查p+(1&lt;&lt;k) &lt; q-(1&lt;&lt;k)。选择具有该属性的最大k。使用dist( p+(1&lt;&lt;k), q-(1&lt;&lt;k) ) + (1 &lt;&lt; k+1) 继续递归。不幸的是,这仍然不是 O(log n)…
  • @mafso,对。但我认为您可以将其转换为O(log n)。首先使用你的技巧找到k。然后不要在p+(1&lt;&lt;k)q-(1&lt;&lt;k) 之间进行递归搜索,而是在它们之间进行正常的二进制搜索。由于这两个地址距离pq 足够远,所以不会超出pq。 (以一些细节为模)
  • @Potatoswatter 我的 32 位系统(Windows)当然不能一次分配超过 SIZE_MAX/2。您确定大多数系统都是这样吗?
【解决方案2】:

我记得,在过去,一些 16 位 80x86 编译器有“大”或“巨大”数据模型,其中指针有 32 位,但整数仍然只有 16 位。这些编译器允许您创建大于 65536 字节的数组,但是,整数只有 16 位,访问不在前 64K 中的元素需要指针操作(这真的很奇怪,指针由 16 位段组成value 和一个 16 位的 offset 值,真实地址是 ((segment

我不知道这些编译器的兼容性如何,但他们必须将 SIZE_MAX 定义为 1M(因为这是在奇怪的指针模型下可以处理的最大对象),但 ptrdiff_t 应该是 16 位整数(不符合要求,因为范围仅为 -32768 到 +32767)。

因此,在正常硬件上的正常 C 实现没有任何理由让 PTRDIFF_MAX 小于 SIZE_MAX。但是可能有一些奇特的硬件(在 80x86 的情况下,当时并不是真正的奇特),它允许您定义大型数组,但不允许您“一次”访问所有数组。在这种情况下,PTRDIFF_MAX 很可能低于 SIZE_MAX/2。

【讨论】:

  • 是的,有几种存储是“分段”的环境,因此指针差异的实际限制小于完整地址大小。任何时候你减去不同段中的两个指针,结果都是没有意义的。这样做的结果基本上是,除非已知两个指针在同一结构或数组中寻址,否则您永远不应期望减法产生良好的结果。如果编译器正在做一些事情以使数组大于段,那么即使这样也是值得怀疑的。
猜你喜欢
  • 2010-10-04
  • 1970-01-01
  • 2011-05-18
  • 1970-01-01
  • 1970-01-01
  • 2018-03-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-15
相关资源
最近更新 更多