【问题标题】:Address of Null pointer?空指针的地址?
【发布时间】:2011-09-22 07:37:21
【问题描述】:

我遇到了下面的宏

#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))

我有点无法理解这一点,因为在 C++ 中,当我尝试遵循空指针时,我预计会出现意想不到的行为……但它怎么会有地址呢?空地址是什么意思?

【问题讨论】:

  • 即使在 C++ 中,在取消引用无效指针时也不会引发异常。这只是未定义的行为。
  • @pmr.. 好的.. 同意...但是你看到我的第二个问题的答案了吗?

标签: c++ c


【解决方案1】:

就宏而言: 它假定在​​地址 0 处有一个 TYPE 类型的对象,并返回成员的地址,该地址实际上是该成员在结构中的偏移量。

This answer 解释了为什么这是未定义的行为。我认为这是最重要的一句话:

如果E1 的类型为“指向X 类的指针”,则表达式E1->E2 将转换为等价形式(*(E1)).E2*(E1) 将导致 具有严格解释的未定义行为,.E2 将其转换 为右值,使其成为弱者的未定义行为 解释。

这里就是这样。尽管其他人认为这是有效的。需要注意的是,这将在许多编译器上产生正确的结果。

【讨论】:

  • 计算偏移量为 0 的定义非常明确。
  • @littleadv 你能以某种方式引用它吗?我看不出这与问题中所做的有什么不同。
  • @littleadv:不,不是,只是恰好大多数编译器都支持它。
  • @littleadv ((Type*)0) 将 0 转换为 pointer to class Type 并且从那里开始一切都是一样的。我不明白你的意思。
  • @littleadv 与您的所有指控相反,我已阅读链接。关键不是E2 是左值,而是operator. 触发将空指针转换为右值。尽管如此,& 要求它的参数是一个左值。
【解决方案2】:
#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))

与标准offsetof() 宏的相当常见的定义非常相似,在<stddef.h>(C 语言)或<cstddef>(C++ 语言)中定义。

0 是一个空指针常量。将其转换为TYPE * 会产生一个TYPE * 类型的空指针。请注意,该语言不保证(甚至暗示)空指针的值为 0,尽管它很常见。

所以(TYPE *)0 理论上是TYPE 类型对象的地址,该对象位于空指针指向的任何地址,((TYPE *)0)->ELEMENT)) 是该对象的ELEMENT 成员。

& 运算符获取此 ELEMENT 成员的地址,然后转换将该地址转换为类型 size_t

现在 if 一个空指针恰好指向地址 0,那么 TYPE 对象类型的(不存在的)对象从地址 0 开始,并且该对象的 ELEMENT 成员的地址对象位于从地址 0 偏移一定数量字节的地址。假设实现定义的从 TYPE *size_t 的转换以简单的方式表现(语言不保证的其他内容),结果整个表达式将是 ELEMENT 成员在 TYPE 类型的对象中的偏移量。

所有这一切都取决于几个未定义或未指定的行为。在大多数现代系统上,空指针被实现为指向地址 0 的指针,地址(指针值)被表示为好像它们是指定整体寻址空间中特定字节的索引的整数,并从指针转换为整数相同大小的只是重新解释位。在具有此类特征的系统上,OFFSETOF 宏可能会起作用,并且实现可能会选择对标准offsetof 宏使用类似的定义。 (作为实现一部分的代码可以利用实现定义或未定义的行为;它不需要是可移植的。)

在不具备这些特性的系统上,这个OFFSETOF 宏可能不起作用——并且实现必须使用其他方法来实现offsetof。这就是为什么offsetof 是标准库的一部分;它不能以可移植的方式实现,但对于任何系统,它总是可以以某种方式实现。有些实现使用了编译器魔法,比如 gcc 的 __builtin_offsetof

实际上,像这样定义自己的 OFFSETOF 宏没有多大意义,因为任何符合 C 或 C++ 的实现都会在其标准库中提供一个有效的 offsetof 宏。

【讨论】:

  • 第二段是doesn't吗?
  • @pmr 是的,我做到了(已修复)。
【解决方案3】:

这不是解引用指针,而是返回结构中元素的偏移量。

例如对于

typedef struct { char a; char b;} someStruct;

调用 OFFSETOF(someStruct, b) 将返回 1(假设它已打包等)。

这和这样做是一样的:

someStruct str;
offset = (size_t)&(str.b) - (size_t)&str;

除了OFFSETOF,您不需要创建虚拟变量。

当您出于某种原因需要找到类/结构/联合成员的偏移量时,这是需要的。

** 编辑 **

对于所有认为“标准不允许这样做”的仓促投票者,请再次阅读标准。在这种情况下,行为定义得非常好。

** 另一个编辑 **

我相信没有人注意到第一个参数是 type。我敢肯定,如果您的思考时间超过了投反对票所需的半秒时间,您就会明白自己的错误。如果不是 - 好吧,这不会是一群无知的反对者压制正确答案的第一个。

【讨论】:

  • 但要获得该地址,它在 UB 的 nullptr 上使用 operator->。你的解释是正确的。仍然是 OP 的代码触发 UB。这里没有意义。
  • @littleadv:标准确实如此。 :) 不过,许多编译器确实支持这一点。
  • @littleadv 这里的标准很清楚:a->b 完全等同于(*a).b*a 是未定义的行为,a 是空指针。这个问题之前已经讨论过,这里的标准真的没有歧义。取消引用空指针不是未定义行为的唯一地方是在诸如 sizeof 之类的上下文中,其中不计算表达式。
  • @littleadv 您仍然忽略了基本问题。有问题的代码取消引用空指针。你没有办法解决这个问题。并且取消引用空指针是未定义的行为。如果你不清楚你为什么错了,你只是没有阅读标准。 (FWIW:委员会讨论了这个确切的宏,并在标准中引入了offsetof,因为该宏具有未定义的行为。)
  • @littleadv 您引用的文档提出了一些更改,这将使表达式合法(并引入了 C 和 C++ 之间的不兼容)。然而,所有提议的更改都没有被采纳,几个月前通过的草案中也没有“空左值”的概念(在 C++03 中也从未出现过)。
【解决方案4】:

取消引用空指针(如该宏所做的那样)是未定义的行为。 编写和使用这样的宏是不合法的,除非 实施为您提供了一些特殊的额外保证。

C标准库定义了一个宏offsetof;许多实现 确实使用类似的东西。实现可以做到这一点,因为 它知道在这种情况下编译器实际生成了什么,以及是否 是否会引起问题。标准的实施 图书馆可以使用很多你不能使用的东西。

【讨论】:

    【解决方案5】:

    OFFSETOF的目的是返回一个成员的地址和它所属的聚合地址之间的距离。

    如果编译器不根据其位置更改对象布局,则“距离”是恒定的,因此您开始的地址是无关紧要的。 0,在这种情况下,它只是一个地址。

    根据 C++ 标准,访问无效地址是“未定义行为”,但是:

    • 如果这是编译器支持库的一部分(这是 VS2003 随附的 CRT 中“OFFSETOF”的实际代码!),那可能不是那么“未定义”(对于已知的编译器和平台,行为是支持库开发者知道的:当然,这必须被认为是“平台特定的代码”,但不同的平台可能会有不同的库版本)

    • 1234563 Thnk 作为一般演示,例如“如果在位置 0 有一个对象,则其假定的 ELEMENT 成员将从位置 6 开始。因此 6 是偏移量”。没有真正的这样的对象这一事实是无关紧要的。 1234563您需要访问一些运行时信息 - 通常是对象 v-table 的一部分 - 其位置无法检测到,因为对象地址不是“真实”地址。 这就是为什么标准谨慎地说“取消引用无效指针是未定义的行为”。

    致反对者:

    我为特定于平台的答案提供特定于平台的信息。 在投反对票之前,请提供一个证明我所说的是错误的。

    【讨论】:

      【解决方案6】:

      A.动作有效,不会抛出异常,因为你没有尝试访问指针指向的内存。
      B. 空指针 - 它基本上是一个普通的指针,表示对象位于地址 0(根据定义,地址 0 是真实对象的无效地址),但指针自身有效。

      所以这个宏的意思是:如果一个 TYPE 类型的对象从地址 0 开始,他的 ELEMENT 将在内存中的什么位置?换句话说,从 ELEMENT 到 TYPE 对象开头的偏移量是多少。

      【讨论】:

      • 不,该操作无效。取消引用nullptr时的UB与内存访问无关。
      • @pmr - 重复错误并不能使它正确。 “返回 0+某个数字”中没有任何未定义的内容。
      • @pmr - 据我所知,取消引用意味着访问(读取\写入内存指向的指针)。所以你在这里并没有真正取消引用。你只是在做一些指针运算,这是 C 中不同的常见做法(检查一些 Linux 内核代码以全面了解它)。
      • @littleadv 忽略标准中所写的内容确实会产生错误的答案。这里的标准很清楚:如果表达式 a->b 用于对其进行评估的上下文中,则程序包含未定义的行为。
      【解决方案7】:

      这是一个非常糟糕的宏,堆积了未定义的行为......

      它试图做什么:获取struct 成员的偏移量。

      它是如何做到的:

      • 使用空指针(代码中的值为 0)
      • 取元素(让编译器计算它的地址,从 0 开始)
      • 获取元素的地址(使用&
      • 将地址转换为size_t

      有两个问题:

      • 取消引用空指针是未定义的行为,因此从技术上讲,任何事情都可能发生
      • 将指针转换为 size_t 不是应该做的事情(问题是不能保证指针适合)

      怎么做:

      • 使用实物
      • 计算地址的差异

      在代码中:

      #define OFFSETOF(Object, Member) \
        ((diffptr_t)((char*)(&Object.Member) - (char*)(&Object))
      

      但它需要一个对象,因此可能不适合您的目的。

      应该怎么做

      #include <cstddef>
      #define OFFSETOF(Struct, Member) offsetof(Struct, Member)
      

      但这没什么意义……对吧?

      对于好奇的人,定义可能类似于:__builtin_offsetof(st, m)(来自Wikipedia)。一些编译器使用 null 取消引用来实现它,但它们编译器,因此知道它们安全地处理这种情况;这不是可移植的......并且不必因为切换编译器,您还切换C库实现。

      【讨论】:

      • “正确定义”取决于编译器;没有办法可移植地实现这个宏。您的正确定义示例仅适用于 gcc;恕我直言,就实现质量而言,它是最好的,但许多编译器确实使用了与原始宏类似的东西。在这种情况下,如果宏是实现的一部分,或者编译器做出了一些明确的保证(但我从未见过第二种情况),则宏是“正确的”。
      • @James:我想我的句子是模棱两可的,显然我的意思是鼓励使用cstddef 中定义的offsetof 宏,并且仅包括它的确切定义作为示例。重新实现 C 库为您提供的东西是没有意义的 :)
      • @Matthieu M 我完全同意整个帖子(这比我的要好得多);我只是担心有人可能会将您的最后陈述误解为暗示不使用某种内置魔法的标准库中的定义在某种程度上是“不正确的”。虽然从 QoI 的角度来看,我更喜欢 gcc 解决方案,但您不能真正发送错误报告,因为 VC++ 使用 hacky 宏;他们的库实现的作者与编译器的作者合作,以确保它正常工作。
      • @James:我明白了......并且打算立即进行编辑,但我的连接挂了:/ 现在已更正以减少歧义,感谢您的评论 :)
      【解决方案8】:

      littleadv 的构造意图恰到好处。稍微解释一下:你转换了一个指向地址 0x0 的结构指针并取消引用它的元素。您指向的地址现在位于 0x0 + 元素具有的任何偏移量。现在将此值转换为 size_t 并获取元素的偏移量。

      不过,我不确定这个构造的可移植性如何。

      【讨论】:

      • 问题在于“它的元素”。地址 0 处没有对象,所以你不能谈论它的元素。 (如果 litteadv 是对的,那么它就是可移植的,并且在 C 标准中不需要 offset_of 宏。
      • littleadv 搞错了:表达式调用了未定义的行为。
      猜你喜欢
      • 2019-05-10
      • 1970-01-01
      • 2013-05-09
      • 2020-12-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多