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

时间:2014-09-08 13:23:02

标签: c++ offset undefined-behavior

以下代码(或使用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?

1 个答案:

答案 0 :(得分:9)

尽管它没有做任何事情,但char* foo = 0; *foo; 可能是未定义的行为。

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

如果您不读取或写入*(char*)0是未定义的行为,工作组中当前有open issue。标准的一部分暗示它是,其他部分暗示它不是。目前的笔记似乎倾向于定义它。

现在,这是理论上的。在实践中怎么样?

在大多数编译器中,这是有效的,因为在解除引用时没有进行检查:空指针所指向的内存被保护以防止访问,并且上面的表达式只是取一些null的地址,它不读取或写入那里的价值。

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

然而,考虑到模糊性,编译器可以自由地在每个取消引用指针的指令上添加检查,并执行任意代码(例如,如果确实取消引用了null,则报告快速错误报告)。这样的检测甚至可能有助于发现它们发生的错误,而不是出现症状的位置。在0附近有可写内存的系统上,这样的仪器可能是关键的(OSX前MacOS有一些可写内存可以控制0附近的系统功能)。

这样的编译器仍然可以用这种方式编写offsetof,并引入pragma等来阻止生成的代码中的检测。或者他们可以切换到固有的。

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

最后,一些编译器具有积极的优化设置,可以找到执行未定义行为的代码(至少在某些分支或条件下),并使用它来将该分支标记为无法访问。如果确定null解除引用是未定义的行为,则这可能是一个问题。一个典型的例子是gcc的积极签名整数溢出分支消除器。如果标准规定了某些未定义的行为,编译器可以自由地考虑该分支不可达。如果null取消引用不在函数中的分支后面,则编译器可以自由地声明调用该函数的所有代码都无法访问,并递归。

可以自由地在当前版本中执行此操作,但是编译器的下一个版本。

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