126

考虑以下代码:

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

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

4

2 回答 2

121

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


首先要了解的是为什么取消引用空指针是未定义的行为。在 C++03 中,这里实际上有点模棱两可。

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

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

左值是指对象或函数。

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

无论如何,它绝对不能转换为右值(§4.1/1):

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

这绝对是未定义的行为。

歧义来自于是否遵循未定义的行为但不使用来自无效指针的值(即,获取左值但不将其转换为右值)。如果不是,则int *i = 0; *i; &(*i);定义明确。这是一个活跃的问题

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

现在我们考虑这个问题。


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

这来自第 5.2.5/3 节:

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

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

它还遵循直接来自(§9.3.1/1)的未定义行为:

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


对于静态函数,严格解释与弱解释会有所不同。严格来说,它是未定义的:

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

也就是说,它的评估就像它是非静态的一样,我们再次使用(*(E1)).E2.

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

在 C++0x 中,从 n3126 开始,歧义仍然存在。现在,请注意安全:使用严格的解释。

于 2010-03-18T23:21:13.253 回答
34

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

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

Microsoft 的 MFC 函数GetSafeHwnd实际上依赖于这种行为。我不知道他们抽什么烟。

如果您正在调用虚函数,则必须取消对指针的引用才能到达 vtable,并且您肯定会获得 UB(可能会崩溃,但请记住,没有任何保证)。

于 2010-09-29T21:51:49.873 回答