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


120

考虑以下代码:

#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)会崩溃,因为xnull指针没有对应的成员。实际上,(a)不会崩溃,因为this从未使用过指针。

因为(b)取消引用this指针((*this).x = 5;),并且this为null,所以程序将输入未定义的行为,因为始终将取消引用null视为未定义的行为。

(a)导致不确定的行为吗?如果两个函数(和x)都是静态的怎么办?


如果两个函数都是静态的,那么如何在baz中引用x ?(x是非静态成员变量)
legends2k 2010年

4
@ legends2k:假装x也是静态的。:)
GManNickG 2010年

当然,但是对于情况(a),它在所有情况下都相同,即,该函数被调用。但是,将指针的值从0替换为1(例如,通过reinterpret_cast),几乎总是崩溃。在情况a中,值分配0和NULL是否表示对编译器特殊的东西?为什么总是与分配给它的任何其他值一起崩溃?
Siddharth Shankaran 2010年

5
有趣:在C ++的下一个修订版中,完全不再需要对指针进行解引用。现在,我们将通过指针执行间接操作。要了解更多信息,请通过此链接进行间接访问:N3362
James McNellis

3
在空指针上调用成员函数始终是未定义的行为。仅通过查看您的代码,我就已经可以感觉到未定义的行为正慢慢爬上我的脖子!
fredoverflow

Answers:


113

(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为null,则无论函数内容如何,结果都是不确定的。

这是从第5.2.5 / 3节得出的:

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

*(E1) 严格解释会导致不确定的行为,并且 .E2并将其转换为右值,从而使弱解释的行为不确定。

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

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


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

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

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

但是,由于E1未在静态成员函数调用中使用,因此如果使用弱解释,则该调用是明确定义的。*(E1)产生左值,静态函数被解析,*(E1)被丢弃并调用该函数。没有从左值到右值的转换,因此没有未定义的行为。

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


5
+1。继续学步,在“弱定义”下,没有为“非X类型的对象”调用非静态成员函数。已经调用了一个根本不是对象的左值。因此,建议的解决方案在您引用的子句中添加文本“或如果左值是空的左值”。
史蒂夫·杰索普

你能澄清一下吗?特别是,通过您的“已解决问题”和“活动问题”链接,问题编号是多少?另外,如果这是一个封闭的问题,那么对静态函数的肯定/否答案到底是什么?我觉得我想了解您的答案时错过了最后一步。
Brooks Moses 2010年

4
我认为CWG缺陷315不像其在“已解决的问题”页面上的显示那样“已结束”。基本原理说,应该允许这样做,因为“ 除非将左值转换为右值,否则在null *p时不是错误p”。但是,这依赖于“空左值”的概念,该概念是针对CWG缺陷232的拟议解决方案的一部分,但尚未被采用。因此,使用C ++ 03和C ++ 0x中的语言,即使没有从左值到右值的转换,对空指针的取消引用仍然是未定义的。
James McNellis 2010年

1
@JamesMcNellis:据我了解,如果p有一个硬件地址在读取时会触发某些动作,但未声明volatile,则该语句*p;不是必需的,但允许实际读取该地址的;&(*p);但是,该声明将被禁止这样做。如果*pvolatile,则需要读取。无论哪种情况,如果指针无效,我都看不到第一条语句不会是未定义行为,但是我也看不到第二条语句为什么会是。
2014年

1
“.E2将其转换为一个右值,” -呃,不,它不需要
MM

30

显然,未定义表示未定义,但有时可以预测。肯定不能保证我将要提供的信息用于工作代码,因为它肯定不能保证,但是在调试时可能会很有用。

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

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

如果要调用虚拟函数,则必须先解除指针的引用才能到达vtable,并确保要获取UB(可能是崩溃,但请记住没有保证)。


1
GetSafeHwnd首先执行!this检查,如果为true,则返回NULL。然后,它开始SEH帧并取消引用指针。如果存在内存访问冲突(0xc0000005),则将捕获此错误并将NULL返回给调用方:)否则,将返回HWND。
ПетърПетров

@ПетърПетров从我查看的代码已经有很多年了GetSafeHwnd,他们可能从那时起就对其进行了增强。并且不要忘记他们对编译器的工作有内幕知识!
马克·兰瑟姆

我指出一个样品可能的实现具有相同的效果,它究竟做的是使用调试器:)反向工程
ПетърПетров

1
“他们对编译器的工作有深入的了解!” -试图使g ++编译调用Windows API的代码的MinGW之类的项目遭受永久麻烦的原因
MM

@MM我想我们都会同意这是不公平的。因此,我还认为存在关于兼容性的法律,使其保持这种合法性有点违法。
v.oddou
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.