通过空指针获取成员变量的地址是否会产生未定义的行为

Does taking address of member variable through a null pointer yield undefined behavior?

本文关键字:未定义 是否 地址 空指针 获取 成员 变量      更新时间:2023-10-16

以下代码(或使用null文本的显式强制转换来消除临时变量的等效代码)通常用于计算类或结构中特定成员变量的偏移量:

class Class {
public:
    int first;
    int second;
};
Class* ptr = 0;
size_t offset = reinterpret_cast<char*>(&ptr->second) -
                 reinterpret_cast<char*>(ptr);

&ptr->second看起来相当于以下内容:

&(ptr->second)

这又相当于

&((*ptr).second)

其解引用对象实例指针并产生空指针的未定义行为。

那么,原来是罚款还是产生UB?

尽管它什么都不做,但char* foo = 0; *foo;可能是未定义的行为。

取消引用空指针可能是未定义的行为。是的,ptr->foo等价于(*ptr).foo*ptr取消引用空指针。

目前,工作组中存在一个悬而未决的问题,即如果你不读或写*(char*)0,它是否是未定义的行为。标准的部分内容意味着它是,其他部分则意味着它不是。目前那里的注释似乎倾向于定义它。

现在,这是理论上的。练习怎么样?

在大多数编译器中,这是因为在去引用时不进行检查:null指针指向的内存被保护不受访问,而上面的表达式只是获取null周围某个东西的地址,它不会读取或写入那里的值。

这就是为什么cpp引用offsetof列出了很多可能的实现技巧。事实上,一些(我检查过的大多数编译器)编译器以类似或等效的方式实现offsetof,这并不意味着行为在C++标准下得到了很好的定义。

然而,考虑到歧义性,编译器可以自由地在每个取消引用指针的指令上添加检查,如果确实取消了对null的引用,则可以执行任意代码(例如,故障快速错误报告)。这样的工具甚至可能有助于找到错误发生的地方,而不是症状发生的地方。在0附近有可写存储器的系统上,这种仪器可能是关键(OSX之前的MacOS有一些可写存储器,可以控制0附近的系统功能)。

这样的编译器仍然可以用这种方式编写offsetof,并引入pragmas等来阻止生成的代码中的插入。或者他们可以转换为内在的。

更进一步,C++在如何排列非标准布局数据方面留下了很大的自由度。理论上,类可以实现为相当复杂的数据结构,而不是我们所期望的几乎标准的布局结构,并且代码仍然是有效的C++。访问非标准布局类型的成员变量并获取它们的地址可能会有问题:我不知道是否可以保证非标准布局中成员变量的偏移量不会在实例之间发生变化!

最后,一些编译器有积极的优化设置,可以找到执行未定义行为的代码(至少在某些分支或条件下),并使用这些代码将该分支标记为不可访问。如果确定null取消引用是未定义的行为,这可能是一个问题。一个经典的例子是gcc的激进有符号整数溢出分支消除器。如果标准规定某个东西是未定义的行为,编译器可以自由地认为该分支是不可访问的。如果null解引用不在函数的分支后面,编译器可以自由地声明调用该函数的所有代码都是不可访问的,并递归。

在当前版本中,而是在下一版本的编译器中,这样做是免费的。

编写标准有效的代码不仅仅是编写今天编译干净的代码。虽然目前定义的去引用和不使用空指针的程度不明确,但依赖定义不明确的东西是有风险的。