如何分辨析构函数未被调用

How to tell the destructor is not called?

本文关键字:调用 析构函数      更新时间:2023-10-16

我刚刚有一个面试问题,面试官问

如何判断析构函数在应该调用的时候没有被调用?

如果不调用析构函数,您将怎么做?

老实说,我不知道答案。我的猜测是将析构函数放在一个try catch块中,但我从未见过有人这样做。有没有更好的解决方案?

有许多

方法可以调用对象的析构函数:

  • 调用 abort_exit(即使exit也会使堆栈变量保持不变(。
  • 让构造函数引发异常。 (从技术上讲,如果构造函数抛出,则对象永远不会开始存在,因此没有对象可以调用其析构函数(。
  • 调用未定义的行为(此时C++标准允许发生任何事情(。 在分配了 new [] 的数组上调用 delete 是调用未定义行为的一种方法,一种常见的行为是仅调用第一个对象的析构函数(保留第二个和后续未销毁( - 但它仍然是未定义的行为。
  • 调用
  • 未定义行为的另一种方法是,一种很可能使析构函数不被调用的方法是一种具有指向基的指针,该指针实际上指向派生对象,并在指针到基的指针上调用删除。 如果基类没有虚拟析构函数,则存在未定义的行为。
  • 您尚未在分配了new的指针上调用delete(如果您有内存泄漏,这尤其成问题(。 (这实际上是一种特别常见的"析构函数尚未运行"的情况(。

如果您正在尝试调试程序并想要了解是否正在调用析构函数,则

  • 设置断点并在调试器下运行
  • printf 或您正在使用的任何日志记录框架。

这是另一个经典的不破坏:

#include <iostream>
#include <memory>
class Base
{
public:
    Base()
    {
        std::cout << "All your base, sucker!" << std::endl;
    }
    ~Base() <-- note lack of virtual
    {
        std::cout << "Base destroyed!" << std::endl;
    }
};
class Sub: public Base
{
public:
    Sub()
    {
        std::cout << "We all live in a Yellow Submarine..." << std::endl;
    }
    ~Sub()
    {
        std::cout << "Sub destroyed" << std::endl;
    }
};
int main()
{
    std::unique_ptr<Base> b(new Sub());
}

输出:

All your base, sucker!
We all live in a Yellow Submarine...
Base destroyed!

因为Base的析构函数不是虚拟的,所以~Base被调用而不是~Sub销毁,~Base甚至不知道Sub存在,也无法调用~Sub来完成清理。

例如,您可以在要测试的类中放置一个静态布尔值,在构造函数中将其设置为 true,在析构函数中将其设置为 false。当析构函数未被调用时,布尔值将保持为真。或者它可以是静态 int,构造函数中的增量和析构函数中的递减(并检查范围前后的计数(。这是检查资源泄漏的简单方法之一。我已经在单元测试中使用了这种技术,以便在自定义智能指针超出范围时轻松检查是否调用了正确的构造函数。

在许多情况下,可能不会调用析构函数,通常是由于编程错误。例如:

    通过基类
  • 指针删除继承的类,而不具有虚拟析构函数(则仅调用基析构函数(
  • 删除指向前向声明类的指针(这种情况很棘手,因为只有一些编译器发出警告(
  • 完全忘记删除(内存泄漏(
  • 通过放置 new 而不是手动调用析构函数来初始化对象(这是放置 new 所必需的(
  • 不匹配的数组/非数组运算符(通过 new[] 分配并通过常规删除删除 - 如果它没有崩溃,它只调用第一项的析构函数(

我不知道面试官想问你什么,因为上下文不清楚,但以下几点可能会有所帮助

对于堆栈上的对象 - 当对象超出范围时调用析构函数。

对于在堆上创建的对象 - 对于 new 创建的每个对象,删除将调用析构函数。如果程序在删除之前终止,则可能不会调用析构函数,则应进行这种适当的处理(我建议使用智能指针来避免这种情况(

下面是一个不调用析构函数的示例:

#include <iostream>
class A {
  public:
     ~A() { std::cout << "Destructor called" << std::endl;}
};
int main()
{
   A *a = new A;
   return 0;
}

还有很多其他的例子。像铸造,静态,...

检测"负面事件"并不容易:有些事情没有发生。

相反,我们测试的是无条件发生的一些事件,并且总是在我们试图检测的有趣事件之后(当该事件确实发生时(。当另一个甚至发生时,我们就知道我们已经过了有趣的事情应该发生的时间点(如果它真的发生了的话(。 在这一点上,我们有理由寻找一些积极的证据来确定有趣的事件是否发生。

例如,我们可以让析构函数设置某种标志,或者调用一些回调函数或其他什么。 我们还知道C++程序按顺序执行语句。因此,假设我们不知道在执行语句 S1 期间是否调用了给定的析构函数 S1 ; S2 。 我们只是安排在执行S1之前收集证据,然后在S2或之后,我们寻找该证据(是否设置了标志,是否调用了回调,...

如果这只是在调试期间,请使用调试器或代码覆盖率工具!

如果你想知道"这行代码是在我运行某某代码时执行的吗",那就在上面放一个调试器断点。

或者运行代码覆盖率工具,然后分析结果:它会告诉你程序的行被达到多少次。未执行的行将被标记为从未到达(无覆盖范围(。 代码覆盖率可以累积程序多次运行的覆盖率信息;它们可以帮助您查找未被测试用例命中的代码。