【问题标题】:When does invoking a member function on a null instance result in undefined behavior?何时在空实例上调用成员函数会导致未定义的行为?
【发布时间】:2011-01-29 06:18:20
【问题描述】:

考虑以下代码:

#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 为空,所以程序进入未定义行为,因为取消引用空总是被称为未定义行为。

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

【问题讨论】:

  • 如果两个函数都是static,那么如何在baz中引用x呢? (x 是一个非静态成员变量)
  • @legends2k:假装x 也被设为静态。 :)
  • 当然可以,但是对于情况 (a),它在所有情况下都一样,即调用函数。但是,将指针的值从 0 替换为 1(例如,通过 reinterpret_cast),它几乎总是会崩溃。 0 和 NULL 的值分配,如案例 a,是否代表了编译器的特殊之处?为什么它总是在分配给它的任何其他值时崩溃?
  • 有趣:C++ 的下一个版本,将不再有指针的解引用。我们现在将通过指针执行间接。要了解更多信息,请通过此链接执行间接:N3362
  • 在空指针上调用成员函数是总是未定义的行为。只看你的代码,我已经能感觉到未定义的行为慢慢爬上了我的脖子!

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


【解决方案1】:

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


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

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

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

左值指的是对象或函数。

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

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

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

这绝对是未定义的行为。

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

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

现在我们考虑这个问题。


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

这来自第 5.2.5/3 节:

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

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

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

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


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

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

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

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

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

【讨论】:

  • +1。继续迂腐,在“弱定义”下,非静态成员函数没有被“为非 X 类型的对象”调用。它被称为一个根本不是对象的左值。因此,建议的解决方案将文本“或者如果左值是空左值”添加到您引用的子句中。
  • 你能澄清一下吗?特别是,对于您的“已关闭问题”和“活动问题”链接,问题编号是多少?另外,如果这是一个已解决的问题,那么静态函数的是/否答案到底是什么?我觉得我错过了试图理解你的答案的最后一步。
  • 我认为 CWG 缺陷 315 并不像它出现在“已关闭的问题”页面上所暗示的那样“已关闭”。理由是它应该被允许,因为“当p 为空时,*p 不是错误,除非将左值转换为右值。”然而,这依赖于“空左值”的概念,这是向CWG defect 232 提出的决议的一部分,但尚未被采纳。因此,对于 C++03 和 C++0x 中的语言,取消引用空指针仍然是未定义的,即使没有左值到右值的转换。
  • @JamesMcNellis:据我了解,如果p 是一个硬件地址,在读取时会触发某些操作,但未声明volatile,则不需要声明*p;,但允许实际读取该地址;然而,声明&amp;(*p); 将被禁止这样做。如果*pvolatile,则需要读取。在任何一种情况下,如果指针无效,我看不到第一个语句不会是未定义行为,但我也看不出为什么第二个语句会是。
  • ".E2 将其转换为右值," - 呃,它没有
【解决方案2】:

显然未定义意味着它未定义,但有时它是可以预测的。我将要提供的信息绝对不能用于工作代码,因为它当然不能保证,但在调试时它可能会派上用场。

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

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

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

【讨论】:

  • GetSafeHwnd 首先进行 !this 检查,如果为真,则返回 NULL。然后它开始一个 SEH 帧并取消对指针的引用。如果存在内存访问冲突(0xc0000005),则会被捕获并将 NULL 返回给调用者:) 否则返回 HWND。
  • @ПетърПетров 自从我查看GetSafeHwnd 的代码以来已经有好几年了,他们可能从那时起对其进行了增强。并且不要忘记他们对编译器工作有内幕知识!
  • 我在说明一个具有相同效果的可能实现示例,它的真正作用是使用调试器进行逆向工程:)
  • “他们对编译器的工作有内幕!” - 像 MinGW 这样试图让 g++ 编译调用 Windows API 的代码的项目造成永久麻烦的原因
  • @AnOccasionalCashew 是的,我知道,我不提出来是不负责任的。我最喜欢的帖子是Undefined behavior can result in time travel (among other things, but time travel is the funkiest)
猜你喜欢
  • 2013-01-07
  • 2017-03-27
  • 1970-01-01
  • 1970-01-01
  • 2018-12-31
  • 2017-08-14
  • 2019-12-28
相关资源
最近更新 更多