什么时候调用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(可能是崩溃,但请记住没有保证)。