当前位置: 首页 > 知识库问答 >
问题:

Clang和GCC在使用非虚拟析构函数“删除”基类时会做什么?

傅正豪
2023-03-14

已经有一个问题询问了删除指向缺少虚拟析构函数的基类的指针的“真实世界”行为,但问题仅限于非常有限的情况(派生类没有具有非平凡析构函数的成员),并且接受的答案只是说如果不检查每个编译器的行为就无法知道。

....但这实际上没什么帮助。知道每个编译器的行为可能不同并不能告诉我们任何特定编译器的行为。那么,Clang和G在这种情况下做什么呢?我假设他们会简单地调用基类析构函数,然后释放内存(对于整个派生类)。是这样吗?

或者,如果无法确定所有版本的 GCC 和 Clang,那么 GCC 4.9 和 5.1 以及 Clang 3.5 到 3.7 呢?

共有2个答案

吴浩博
2023-03-14

如果在没有虚拟析构函数的情况下删除对象,编译器可能会假设删除的地址是最派生对象的地址。

除非使用主基类删除对象,否则情况并非如此,因此编译器将使用错误的地址调用operator delete

当然编译器不会调用派生类的析构函数,或者派生类的运算符delete(如果有的话)。

范弘亮
2023-03-14

首先,标准免责声明:这是未定义的行为,因此,即使使用一个特定的编译器,更改编译器标志、星期几或您查看计算机的方式也可能会改变行为。

以下所有假设您的析构函数中发生了某种至少稍微不平凡的破坏(例如,对象删除了一些内存,或者包含了自己删除了一些内存的其他对象)。

在简单的情况下(单一继承),您通常会得到大致相当于静态绑定的东西——也就是说,如果您通过指向基对象的指针销毁派生对象,则只会调用基构造函数,因此该对象不会被正确销毁。

如果使用多重继承,并且通过“first”基类销毁派生类的对象,则它通常与使用单继承时大致相同 - 将调用基类析构函数,但派生类析构函数不会被调用。

如果具有多重继承,并且通过指向第二个(或后续)基类的指针销毁派生对象,则程序通常会崩溃。使用多重继承时,派生对象中有多个偏移量的基类对象。

在典型情况下,第一个基类将位于派生对象的开头,因此使用 derived 的地址作为指向第一个基类对象的指针的工作方式与在单继承情况下大致相同 - 我们得到静态绑定/静态调度的等效项。

如果我们对任何其他基类尝试这样做,指向派生对象的指针不会指向该基类的对象。指针需要调整为指向第二个(或后续)基类,然后才能用作指向该类型对象的指针。

对于非虚拟析构函数,通常会发生的情况是,代码基本上会采用第一个基类对象的地址,对其执行大致相当于reinterpret_cast的操作,并尝试将该内存用作指针指定的基类的对象(例如,base2)。例如,假设base2在偏移量14处有一个指针,而base2的析构函数试图删除它指向的内存块。使用非虚拟析构函数时,它可能会接收到指向base1主题的指针——但它仍然会从那里查看偏移量14,并尝试将其视为指针,然后将其传递给delete。可能是base1在该偏移量处包含一个指针,它实际上指向一些动态分配的内存,在这种情况下,这可能会成功。同样,它也可能是完全不同的东西,程序会死掉,并显示一条错误消息,比如试图释放一个无效指针。

也有可能base1的大小小于14字节,所以这实际上是在base2中操作(比方说)偏移量4。

底线:对于这样的案件,事情在匆忙中变得非常丑陋。你能指望的最好的是,程序会迅速而响亮地死去。

只是为了好玩,快速演示代码:

#include <iostream>
#include <string>
#include <vector>

class base{ 
    char *data;
    std::string s;
    std::vector<int> v;
public:
    base() { data = new char;  v.push_back(1); s.push_back('a'); }
    ~base() { std::cout << "~base\n"; delete data; }
};

class base2 {
    char *data2;
public:
    base2() : data2(new char) {}
    ~base2() { std::cout << "~base2\n"; delete data2; }
};

class derived : public base, public base2 { 
    char *more_data;

public:
    derived() : more_data(new char) {}
    ~derived() { std::cout << "~derived\n"; delete more_data; }
};

int main() {
    base2 *b = new derived;
    delete b;
}

g/Linux:分段故障
clang/Linux:分段故障
VC /Windows:弹出:“foo.exe已停止工作”“一个问题导致程序停止正常工作。请关闭程序。”

如果我们将指针更改为base而不是base2,我们会从所有编译器中获得~base(如果我们只从一个基类派生,并使用指向该基类的指针,我们会得到相同的结果:只有该基类的析构函数运行)。

 类似资料:
  • 我读过一些文章,正如他们所说,主要的虚拟析构函数用例是: > 派生类可能具有来自堆的动态数据分配,即“拥有”该数据对象。所以,他们需要在析构函数中使用一些删除例程。通过基类指针删除需要在所有派生类中声明析构函数,直到那些具有动态数据分配的析构函数(基类也需要它) 此类具有< code >虚拟方法。但这对我来说不清楚。仅仅通过基类指针调用< code >虚拟方法总是会导致派生实现最多的调用。他们唯一

  • 我理解,只要你有一个多态基类,基类就应该定义一个虚拟析构函数。因此,当删除指向派生类对象的基类指针时,它将首先调用派生类的析构函数。如果我错了,请纠正我。 此外,如果基类析构函数是非虚拟的,那么删除指向派生对象的基类指针将是未定义的行为。如果我错了也纠正我。 所以我的问题是:为什么当基类析构函数非虚拟时,对象不会被正确销毁? 我假设这是因为虚拟函数具有某种表,每当调用虚拟函数时都会记住和查阅该表。

  • 在C 11中,我们能够声明一个析构函数是自动生成的: 此外,我们可以将析构函数声明为纯虚: 我的问题是:如何将析构函数声明为自动生成和纯虚拟?看起来以下语法不正确: 这一个也不是: 也不是这个: 编辑:关于问题目的的一些澄清。基本上,我希望一个空类是不可实例化的基类,但派生类是可实例化的,那么该类必须有一个纯虚拟析构函数。但另一方面,我不想在.cpp文件中提供定义。因此,我需要某种与等效的机制。我

  • 我读到虚拟析构函数必须在具有虚拟方法的类中声明。我只是不明白为什么必须宣布它们是虚拟的。我从下面的例子中知道为什么我们需要虚拟析构函数。我只是想知道为什么编译器不为我们管理虚拟析构函数。关于虚拟析构函数的工作,有什么我需要知道的吗?以下示例显示,如果析构函数未声明为虚拟,则派生类的析构函数不会被调用,这是为什么?

  • 我最近在读虚函数和虚析构函数,下面的问题引起了我的兴趣。 例如,我有以下继承链。 我知道基类的虚函数在派生类中默认是隐式虚的。所以,我认为这同样适用于析构函数。 我想知道,派生类的析构函数是否默认是虚拟的。如果没有,如果您提供一些解释,我将很高兴。