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



,标题说:

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

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


请考虑以下程序

#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。(即:上面示例中访问i的取消注释代码)
如果在函数中未访问成员,则上述代码实际上不应调用未定义的行为。

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

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

有人可以确认吗?

因为它可能可靠的案例数量如此之少,而且这样做仍然是一个不可言喻的愚蠢想法。定义行为没有任何好处。

那么

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

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

生成的代码是否真的需要在特定情况下取消引用指针并不相关,编译器模型的抽象机器原则上确实会取消引用。

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

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

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

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

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

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

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

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


C++11 实际上指出,取消引用无效指针值会导致未定义的行为,而无效指针值的所有其他用法都会导致实现定义的行为。它还指出,在"复制无效指针"的情况下,实现定义的行为可能会导致"系统生成的运行时错误"。因此,实际上有可能小心翼翼地通过 C++11 规范的迷宫,并成功得出结论,即通过无效指针调用非虚拟方法应该会导致上述实现定义的行为。无论如何,"系统生成的运行时故障"的可能性将始终存在。

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

最新更新