为啥在已删除指针上调用非虚拟成员函数是未定义的行为?

Posted

技术标签:

【中文标题】为啥在已删除指针上调用非虚拟成员函数是未定义的行为?【英文标题】:Why is calling non virtual member function on deleted pointer an undefined behavior?为什么在已删除指针上调用非虚拟成员函数是未定义的行为? 【发布时间】:2012-12-10 08:27:51 【问题描述】:

正如标题所说:

为什么在删除的指针上调用非虚成员函数是未定义的行为?

请注意,问题不会询问它是否是未定义的行为,而是询问为什么这是未定义的行为。


考虑 following program

#include<iostream>
class Myclass

    //int i
    public:
      void doSomething()
      
          std::cout<<"Inside doSomething";
          //i = 10;
      
;

int main()

    Myclass *ptr = new Myclass;
    delete ptr;

    ptr->doSomething();

    return 0;


在上面的代码中,编译器在调用成员函数doSomething()时实际上并没有取消引用this。请注意,该函数不是虚函数,编译器通过将 this 作为第一个参数传递给函数将成员函数调用转换为通常的函数调用(据我所知,这是实现定义的)。他们可以这样做,因为编译器可以准确地确定在编译时调用哪个函数。所以实际上,通过删除指针调用成员函数不会取消引用this。仅当在函数体内访问任何成员时,this 才会被取消引用。(即:在上面访问 i 的示例中取消注释代码) 如果在函数中未访问成员,则上述代码实际上不会调用未定义的行为。

那么为什么标准要求通过删除的指针调用非虚成员函数是未定义的行为,而事实上它可以可靠地说取消引用 this 应该是导致未定义行为的语句?仅仅是为了让语言的用户简单,标准只是对其进行概括,还是在这个任务中涉及一些更深层次的语义?

我的感觉是,也许因为它是实现定义了编译器如何调用成员函数,这可能是标准无法强制执行 UB 发生的实际点的原因。

有人可以确认吗?

【问题讨论】:

标准没有规定任何东西;这就是未定义行为的整个想法。说你声称它可以“可靠地说”的事情将是强制性的。 编译器永远不能“取消引用”任何东西。解引用是语言结构的一部分。它与代码生成无关。混淆语言和生成的代码是很危险的。该语言说调用成员函数会评估隐式实例参数,就是这样。 如果你想要你正在使用的行为,你应该使成员函数静态。当且仅当它不需要任何每个对象的状态时调用它在道德上是安全的,这意味着它应该是静态的。 【参考方案1】:

因为它可能可靠的情况很少,这样做仍然是一个难以言喻的愚蠢想法。定义行为没有任何好处。

【讨论】:

+1。有充分的理由使用没有“this”的 static 类成员(最后我检查它仍然是)来支持这一点。它在过去为我节省了大量的打字工作。但对于普通会员(无论是否虚拟),我认为没有任何好处。 另外 +1 - 如果你有一个非静态成员函数对你的对象的状态没有任何作用,为什么它是一个成员函数? :D 虽然乍一看似乎没有理由让这样的方法不是静态的,但我遇到了这样做的需要。例如,静态方法不能实现接口,所以像实用方法这样的东西,它的实现隐藏在接口后面(或者实用程序可能有多个实现)是纯方法,它必须是实例方法才能实现接口.此外,您可能希望实用程序方法是虚拟的,以便它可以被另一个实用程序类覆盖。 静态方法可以实现静态接口。如果接口是非静态的,那么您需要一个有效的this 来查找预期的功能。 @jam40jeff,如果函数是virtual那么你需要“取消引用this”才能访问vtable,所以它与这个问题无关,具体说是非虚拟的。跨度> 【参考方案2】:

那么为什么标准要求通过删除指针调用非虚成员函数是未定义的行为,而事实上它可以可靠地说取消引用 this 应该是导致未定义行为的语句?

[expr.ref] 第 2 段说像 ptr-&gt;doSomething() 这样的成员函数调用等价于 (*ptr).doSomething(),因此调用非静态成员函数 取消引用。如果指针无效,那是未定义的行为。

生成的代码是否真的需要针对特定​​情况解引用指针并不相关,编译器建模的抽象机原则上会做解引用。

使语言复杂化以准确定义只要不访问任何成员就允许的情况几乎为零。在看不到函数定义的情况下,你不知道调用它是否安全,因为你不知道函数是否使用this

只是不要这样做,没有充分的理由,语言禁止这样做是一件好事。

【讨论】:

【参考方案3】:

在 C++ 语言中(根据 C++03),尝试使用无效指针的值已经导致未定义的行为。没有必要取消对 UB 的引用。只需读取指针值就足够了。当您仅尝试读取该值时导致 UB 的“无效值”概念实际上扩展到几乎所有标量类型,而不仅仅是指针。

delete 之后,指针在特定意义上通常是无效的,即读取一个据称指向刚刚被“删除”的东西的指针会导致未定义的行为。

int *p = new int();
delete p;
int *p1 = p; // <- undefined behavior

通过无效指针调用成员函数只是上述的一个特例。该指针用作隐式参数this 的参数。传递一个指针是一个非引用参数是一种读取它的行为,这就是为什么在你的例子中行为是未定义的。

所以,您的问题实际上归结为为什么读取无效指针值会导致未定义的行为。

嗯,这可能有很多特定于平台的原因。例如,在某些平台上,读取指针的行为可能会导致指针值被加载到某个专用的地址特定寄存器中。如果指针无效,硬件/操作系统可能会立即检测到它并触发程序故障。事实上,这就是我们流行的 x86 平台在段寄存器方面的工作方式。我们没有听到太多关于它的唯一原因是流行的操作系统坚持平面内存模型,根本不主动使用段寄存器。


C++11 实际上声明 取消引用 无效指针值会导致 undefined 行为,而无效指针值的所有其他用途会导致 implementation-defined 行为。它还指出,在“复制无效指针”的情况下实现定义的行为可能会导致“系统生成的运行时错误”。因此,实际上有可能小心翼翼地穿过 C++11 规范的迷宫,并成功得出结论:通过无效指针调用非虚拟方法应该导致 implementation-defined上述行为。无论如何,“系统生成的运行时错误”的可能性总是存在的。

【讨论】:

很难不同意这些。尽管如此,delete 的想法可能会使指针变得如此无效,以至于硬件只会看到它就会出现段错误,这似乎几乎达到了飞蜥级别。据我所知,没有删除操作符物理反汇编内存芯片的C++实现,尽管标准当然不排除这样的实现。不过,我不确定我是否想把它放在我的桌子上。 :) C++11 并没有这么说,对 implementation-defined 的更改是由 DR 在 2012 年 10 月做出的​​,所以只是在最新的 WP 中,而不是任何标准。 @rici,DR 解释了为什么访问无效指针的值可能会出现段错误。 注:如果delete p 的实现将p 设置为nullptr,那么您的示例代码就可以了,这是允许的。 我对此表示反对,因为这意味着允许读取无效指针值的实现必须允许通过无效指针调用成员函数,这不是对标准的正确阅读。 p-&gt;foo() 之类的成员函数调用被定义为等效于 (*p).foo()(参见 [expr.ref]/2),它仍然是指针的取消引用,因此如果 p 无效,则行为未定义。 @rici 没有 C++ 实现中删除操作符物理反汇编内存芯片 实际上标准允许计算机对释放的内存做任何事情,所以这种如果计算机设计者认为它适合他们的目的,则允许实施。此外,如果底层机器代码需要这样做,或者需要针对特定​​平台进行一些特定优化,编译器可能会为指针生成解引用。可能有些微控制器就是这样做的。【参考方案4】:

在这种情况下,this 的取消引用实际上是一个实现细节。我并不是说this 指针不是由标准定义的,因为它是,但是从语义抽象的角度来看,允许使用已被破坏的对象的目的是什么,只是因为有一个极端情况在实践中它将是“安全的”吗?没有。所以不是。不存在对象,因此您不能在其上调用函数。

【讨论】:

所以上面的代码可以工作,因为内存被释放但没有重写?我们刚刚释放了地址?

以上是关于为啥在已删除指针上调用非虚拟成员函数是未定义的行为?的主要内容,如果未能解决你的问题,请参考以下文章

在 C++ 中删除空指针是不是被认为是未定义的行为? [复制]

在非构造的“对象”上调用非虚拟成员函数是不是定义明确? [复制]

指向匿名联合成员的指针是不是相等?

为啥在 C++ 中第二次调用析构函数未定义的行为?

Don Box - Essential COM:当删除运算符是成员函数时,为啥要将创建运算符定义为非成员函数?

致命错误:在第 7 行调用非对象上的成员函数 prepare()。我想不通,为啥会出现此错误?