空指针的地址
Address of Null pointer?
我遇到了下面的宏
#define OFFSETOF(TYPE, ELEMENT) ((size_t)&(((TYPE *)0)->ELEMENT))
我有点无法消化这一点,因为在c++中,当我尝试遵循空指针时,我预计会出现意外行为…但它怎么会有地址呢?空地址是什么意思?
对于宏的目的:它假设在地址0处存在一个TYPE
类型的对象,并返回成员的地址,该地址实际上是该成员在结构中的偏移量。
这个答案解释了为什么这是未定义行为。我认为这是最重要的一句话:
如果
E1
的类型是"指向类X的指针",则表达式E1->E2
转换为等价形式(*(E1)).E2
;*(E1)
将导致使用严格解释的未定义行为,.E2
将其转换到一个右值,使其成为弱类型的未定义行为解释。
就是这里的情况。尽管其他人认为这是有效的。值得注意的是,这将在许多编译器上产生正确的结果。
#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
类型。
现在如果一个空指针恰好指向地址0,那么(不存在的)TYPE
对象类型的对象从地址0开始,并且该对象的ELEMENT
成员的地址位于地址0偏移的某个字节数的地址。假设实现定义的从TYPE *
到size_t
的转换以一种直接的方式进行(语言不能保证的其他方式),则整个表达式的结果将是TYPE
类型对象中ELEMENT
成员的偏移量。
所有这些都取决于几个未定义或未指定的行为。在大多数现代系统中,空指针被实现为指向地址0的指针,地址(指针值)被表示为整数,指定在单块寻址空间中特定字节的索引,并且从指针转换为相同大小的整数只是重新解释比特。在具有这些特征的系统上,OFFSETOF
宏可能会工作,并且实现可能会选择为标准offsetof
宏使用类似的定义。(作为实现一部分的代码可以利用实现定义的或未定义的行为;它不需要便携。)
在不具备这些特性的系统上,OFFSETOF
宏可能无法工作——并且必须使用其他方法来实现offsetof
。这就是为什么offsetof
是标准库的一部分;它不能移植地实现,但对于任何系统,它总是可以以某种方式实现。有些实现使用编译器魔法,如gcc的__builtin_offsetof
。
在实践中,像这样定义自己的OFFSETOF
宏没有多大意义,因为任何符合标准的C或c++实现都会在其标准库中提供工作的offsetof
宏。
这不是对指针解引用,而是返回该结构体中元素的偏移量。
例如
typedef struct { char a; char b;} someStruct;
调用OFFSETOF(someStruct, b)
将返回1(假设其已打包等)。
这和下面的操作是一样的:
someStruct str;
offset = (size_t)&(str.b) - (size_t)&str;
除了OFFSETOF
,你不需要创建一个虚拟变量。
当您出于任何原因需要查找类/结构/联合成员的偏移量时,
**编辑
对于那些认为"标准不允许这样做"的人,请再阅读一遍标准。在这种情况下,行为定义得非常好。
**另一个编辑**
我相信没有人注意到第一个参数是类型。我敢肯定,如果你再多想一点,你就会明白你的错误。如果没有——好吧,这也不是第一次有一群无知的看跌者压制正确答案了。
解除空指针的引用(如这个宏所做的)是未定义的行为。编写和使用这样的宏是不合法的,除非实现为您提供了一些特殊的,额外的保证。
C标准库定义了一个宏offsetof
;很多实现一定要使用类似的方法。实现可以这样做,因为它知道编译器在这种情况下实际生成了什么,以及是否它会不会引起问题。标准的实施图书馆可以使用很多你不能使用的东西。
OFFSETOF
的目的是返回成员的地址与其所属聚合的地址之间的距离。
如果编译器没有根据对象的位置改变对象的布局,那么"距离"是恒定的,因此您的起始地址是无关紧要的。在这种情况下,它只是一个地址。
根据c++标准,访问一个无效的地址是"未定义的行为",但是:
-
如果这是编译器支持库的一部分(这是VS2003自带的CRT中的"OFFSETOF"的实际代码!),那可能不是那么"未定义"(对于已知的编译器和平台,该行为为支持库开发人员所知:当然,这必须被认为是"平台特定代码",但不同的平台可能有不同的库版本)
-
在任何情况下,您都没有"操作"元素(因此没有"访问"完成),只是做一些简单的指针算术。就像"如果在位置0有一个对象,它假定的ELEMENT成员将从位置6开始。因此6是偏移量"。事实上,没有真实的这样的对象是无关紧要的。
顺便说一下,如果ELEMENT通过虚拟基由TYPE继承,则该宏会失败(带有分割错误!),因为要定位虚拟基的位置,您需要访问一些运行时信息-通常是对象v表的一部分-其位置无法检测到,因为对象地址不是"真实"地址。这就是为什么标准谨慎地说"解引用无效指针是未定义的行为"。
DOWNVOTERS
:
我为特定于平台的答案提供特定于平台的信息。在拒绝投票之前,请提供一个证明,我说的是假的。
B.空指针-它基本上是一个普通的指针,表示对象位于地址0(地址0的定义是一个无效的地址为实际对象),但指针是自我有效的。
所以这个宏的意思是:如果type类型的对象从地址0开始,他的ELEMENT将在内存中的哪个位置?换句话说,从ELEMENT到TYPE对象开始的偏移量是多少。
这是一个该死的宏,堆积未定义行为…
它试图做的是:获取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)
(来自维基百科)。一些编译器用null解引用来实现它,但是它们是编译器,因此知道它们安全地处理这种情况;这是不便携的…而且不必既然切换编译器,还可以切换C库实现。
littleleadv的意图是正确的。稍微解释一下:强制转换一个指向地址0x0的结构指针,并对它的一个元素解引用。您指向的地址现在是0x0 +元素的偏移量。现在将该值强制转换为size_t类型,并获得元素的偏移量。
我不确定这个构造的可移植性如何。
- 为什么会出现 gettnig 运行时错误:加载类型为"_Bit_type"(stl_bvector.h) 的空指针?
- 运行时错误:引用绑定到类型为"int"的空指针
- 这个失败的测试是将零添加到空指针未定义的行为、编译器错误还是其他什么?
- 为什么我在空指针错误(链表)中获取成员访问权限
- 从向量到空指针的 memcpy(反之亦然)不起作用
- 空指针常量 (nullptr)、空指针值和空成员指针值之间有什么区别?
- 成员访问是否在空指针上定义C++?
- 尝试将对象插入空指针数组时出现分段错误
- 为什么我们需要在 C++ 中检查空指针,而在 Java 中不需要?
- 是否允许向空指针添加零?
- 指向地址的指针似乎调整在范围之外
- 为什么 C 样式字符串的工作空指针检查不?
- Do C和C++标准意味着地址空间中必须只存在一个特殊值来表示空指针的值
- 如何正确地将一个文本整数作为参数作为空指针进行传递,并使用其地址作为整数的实际值
- 以何种形式,可以通过管道将空指针发送到不同进程地址空间中的另一个进程
- 通过空指针获取成员变量的地址是否会产生未定义的行为
- 在返回地址之前清空指针
- 这些是空指针,还是指向地址0的指针
- 空指针的地址
- 空指针的地址是什么