什么时候调用 null 实例的成员函数会导致未定义行为?

考虑以下代码:

#include <iostream>


struct foo
{
// (a):
void bar() { std::cout << "gman was here" << std::endl; }


// (b):
void baz() { x = 5; }


int x;
};


int main()
{
foo* f = 0;


f->bar(); // (a)
f->baz(); // (b)
}

我们预计 (b)会崩溃,因为空指针没有对应的成员 x。实际上,(a)不会崩溃,因为从来不使用 this指针。

因为 (b)解引用了 this指针((*this).x = 5;) ,而且 this是 null,所以程序进入了未定义行为,因为解引用 null 总是被认为是未定义行为。

如果两个函数(和 x)都是静态的,那么 (a)会导致未定义行为吗?

17016 次浏览

无论是 (a)还是 (b)都会导致未定义行为。通过空指针调用成员函数总是有未定义行为的。如果函数是静态的,那么它在技术上也是未定义的,但是存在一些争议。


首先需要理解的是,为什么取消对空指针的引用是未定义行为的。在 C + + 03中,实际上有一点模糊。

虽然 “解除空指针的引用会导致未定义行为”在1.9/4和8.3.2/4的注释中都有提到,但它从未明确说明过(注释是非规范的)

然而,人们可以试着从3.10/2推断出:

左值指的是一个对象或函数。

取消引用时,结果是左值。空指针 没有指向一个对象,因此当我们使用左值时,我们有未定义行为。问题是前面的句子从来没有陈述过,那么“使用”左值意味着什么呢?甚至只是生成它,或者在更正式的意义上使用它来执行从左值到右值的转换?

无论如何,它肯定不能转换为 rvalue (4.1/1) :

如果 lvalue 所指的对象不是一个类型为 T 的对象,也不是一个类型为派生自 T 的对象,或者如果该对象未初始化,则需要进行这种转换的程序将失去未定义行为。

这里绝对是未定义行为。

这种模棱两可的未定义行为在于,是否应该从一个无效的指针(也就是说,获取一个左值,但不要将其转换为右值)中引用 但不能用的值。如果没有,那么 int *i = 0; *i; &(*i);是定义良好的。这是 激活的问题

所以我们有一个严格的“解引用空指针,获得未定义行为”视图和一个弱的“使用解引用空指针,获得未定义行为”视图。

现在我们来考虑这个问题。


是的,(a)导致未定义行为。事实上,如果 this为空,那么 不管函数的内容如何的结果是未定义的。

这是从5.2.5/3开始的:

如果 E1具有类型“指向类 X 的指针”,那么表达式 E1->E2将转换为等效的形式 (*(E1)).E2;

*(E1)将导致严格解释的未定义行为,而 .E2将其转换为 rvalue,使其成为弱解释的未定义行为。

它也是直接来自(9.3.1/1)的未定义行为:

如果为非 X 类型的对象或从 X 派生的类型调用类 X 的非静态成员函数,则该行为是未定义的。


对于静态函数,严格解释和弱解释是有区别的。严格地说,它是未定义的:

静态成员可以使用类成员访问语法来引用,在这种情况下,将计算对象表达式。

也就是说,它的计算就好像它是非静态的一样,我们再次用 (*(E1)).E2取消引用空指针。

但是,由于 E1不用于静态成员函数调用,因此如果使用弱解释,则调用是定义良好的。*(E1)导致一个左值,解析静态函数,丢弃 *(E1),并调用该函数。没有从左到右的转换所以没有未定义行为。

在 C + + 0x 中,从 n3126开始,歧义仍然存在。

显然,未定义意味着它是 没有定义,但有时它可以是可预测的。我将要提供的信息绝对不应该用于工作代码,因为它当然不能得到保证,但是在调试时它可能会很有用。

您可能认为对对象指针调用函数会解引用指针并导致 UB。在实践中,如果函数不是虚函数,编译器会将其转换为一个普通函数调用,将指针作为第一个参数 这个传递,绕过解引用并为被调用的成员函数创建一个定时炸弹。如果成员函数没有引用任何成员变量或虚函数,那么它实际上可能成功而没有错误。记住,成功属于“未定义”的宇宙!

微软的 MFC 功能 去找安全之神实际上依赖于这种行为,我不知道他们在吸什么。

如果调用虚函数,指针必须解引用才能到达 vtable,而且肯定会得到 UB (可能是崩溃,但请记住,没有保证)。