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

时间:2015-08-21 03:47:27

标签: c++ gcc clang undefined-behavior virtual-destructor

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

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

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

2 个答案:

答案 0 :(得分:2)

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

以下都假设你的析构函数中至少发生了一些微不足道的破坏(例如,对象删除了一些内存,或者包含了自己删除某些内存的对象)。

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

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

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

enter image description here

在典型的情况下,第一个基类将位于派生对象的开头,因此使用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(如果我们只从一个基类派生,并使用指向该指针的指针)基类,我们得到相同:只有基类'析构函数运行。)

答案 1 :(得分:-5)

如果删除没有虚析构函数的对象,编译器可能会认为删除的地址是派生最多的对象的地址。

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

当然编译器不会调用派生类的析构函数,也不会调用派生类的operator delete(如果有的话)。