何时在null实例上调用成员函数会导致未定义的行为?

时间:2010-03-18 23:20:45

标签: c++ undefined-behavior language-lawyer standards-compliance null-pointer

请考虑以下代码:

#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总是被认为是未定义的行为

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

2 个答案:

答案 0 :(得分:110)

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


要理解的第一件事是为什么取消引用空指针是未定义的行为。在C ++ 03中,这里实际上有点含糊不清。

虽然“取消引用空指针导致未定义的行为”在§1.9/ 4和§8.3.2/ 4的注释中提到,但它从未明确说明。 (注释是非规范性的。)

但是,可以尝试从§3.10/ 2中推断出它:

  

左值是指对象或函数。

取消引用时,结果是左值。空指针引用一个对象,因此当我们使用左值时,我们有未定义的行为。问题是前一句从未说过,所以“使用”左值是什么意思?甚至可以生成它,或者以更正式的方式使用它来执行左值到右值的转换?

无论如何,它肯定无法转换为右值(§4.1/ 1):

  

如果左值引用的对象不是T类型的对象,并且不是从T派生的类型的对象,或者如果对象未初始化,则需要此转换的程序具有未定义的行为。

这里肯定是未定义的行为。

模糊性来自于它是否是未定义的行为以引用而不是使用来自无效指针的值(即,获取左值但不将其转换为右值)。如果没有,则int *i = 0; *i; &(*i);定义明确。这是active issue

因此我们有一个严格的“取消引用空指针,获取未定义的行为”视图和弱“使用解除引用的空指针,获取未定义的行为”视图。

现在我们考虑这个问题。


是的,(a)会导致未定义的行为。实际上,如果this为null,则无论函数的内容是什么,结果都是未定义的。

以下是§5.2.5/ 3:

  

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

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

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

  

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


对于静态函数,严格与弱的解释会产生差异。严格来说,它是未定义的:

  

可以使用类成员访问语法引用静态成员,在这种情况下,将对object-expression进行求值。

也就是说,它被评估为非静态的,我们再次使用(*(E1)).E2取消引用空指针。

但是,因为E1未在静态成员函数调用中使用,所以如果我们使用弱解释,则调用是明确定义的。 *(E1)导致左值,静态函数被解析,*(E1)被丢弃,函数被调用。没有左值到右值的转换,因此没有未定义的行为。

在C ++ 0x中,从n3126开始,模糊性仍然存在。现在,安全:使用严格的解释。

答案 1 :(得分:28)

显然未定义意味着未定义,但有时它可以预测。我要提供的信息永远不应该依赖于工作代码,因为它当然不能保证,但在调试时可能会有用。

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

Microsoft的MFC函数GetSafeHwnd实际上依赖于此行为。我不知道他们在吸烟。

如果您正在调用虚函数,则必须取消引用指针才能进入vtable,并确保您将获得UB(可能是崩溃,但请记住没有保证)。