什么时候调用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,所以程序进入未定义的行为,因为dereferencing的null总是被认为是未定义的行为。

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

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


首先要理解的是为什么它是未定义的行为来解引用空指针。 在C ++ 03中,这里实际上有点不明确。

尽pipe在第1.9 / 4节和第8.3.2 / 4节的注释中提到了“解除引用空指针导致未定义的行为” ,但从未明确指出。 (注释是非规范的。)

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

左值是指对象或函数。

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

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

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

这绝对是未定义的行为。

不明确性来自于它是否是未定义的行为, 而不是使用来自无效指针的值(即,得到左值而不是将其转换为右值)。 如果不是,那么int *i = 0; *i; &(*i); int *i = 0; *i; &(*i); 是明确的。 这是一个活跃的问题 。

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

现在我们考虑这个问题。


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

从§5.2.5/ 3开始:

如果E1的types为“指向类X的指针”,则expression式E1->E2被转换为等价forms(*(E1)).E2;

*(E1)会导致一个严格解释的未定义的行为,并且.E2将它转换为一个右值,使得它对弱解释的行为不明确。

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

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


对于静态函数,严格与弱解释有所不同。 严格来说,这是不明确的:

可以使用类成员访问语法来引用静态成员,在这种情况下对对象expression式进行评估。

也就是说,它的评估就好像它是非静态的,我们再次用(*(E1)).E2解引用空指针。

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

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

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

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

微软的MFC函数GetSafeHwnd实际上依赖于这种行为。 我不知道他们在吸烟。

如果你正在调用一个虚拟函数,指针必须解除引用才能进入虚表,而且肯定会得到UB(可能是崩溃,但请记住没有保证)。