【问题标题】:How to tell the destructor is not called? [closed]如何判断析构函数没有被调用? [关闭]
【发布时间】:2015-12-12 06:21:07
【问题描述】:

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

如何告诉析构函数在它应该被调用的时候没有被调用?

如果不调用析构函数,你会怎么做?

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

【问题讨论】:

  • 这个问题很模糊。我们可以了解一些上下文吗?
  • 是否给出了任何上下文?它们的意思是“未调用”、“未在程序退出时调用”、“可能未调用(在某些执行路径上)”还是不同的意思?
  • AFAIK 唯一不调用析构函数的情况是,当您使用 new/new[] 创建了一个指针而您没有 delete/delete[] 它时。
  • 也许面试中问题的重点是让候选人要求澄清并开始讨论?!
  • 也许面试官是想讨论静态成员?在构造时递增并在销毁时递减的静态 int 成员是计算类实例的常用方法,因此您可以使用该计数器来检测析构函数是否已按预期调用。对于清理,我想您还可以存储和维护指向类实例而不是计数器的指针的静态向量,但我无法想象您如何区分合法实例和泄漏实例,除非您想要核化和在某个时候删除该类的每个实例。

标签: c++ c++11


【解决方案1】:

对象的析构函数调用失败的方式有多种:

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

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

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

【讨论】:

    【解决方案2】:

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

    #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完成清理。

    【讨论】:

      【解决方案3】:

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

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

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

      【讨论】:

        【解决方案4】:

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

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

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

        【讨论】:

          【解决方案5】:

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

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

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

          【讨论】:

            【解决方案6】:

            检测“负面事件”并不容易:某件事没有发生。

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

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

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

            如果您想知道“这行代码是否在我运行某某时执行”,那么在其上放置一个调试器断点。

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

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 2011-11-23
              • 2015-12-23
              • 1970-01-01
              • 1970-01-01
              • 2017-11-18
              • 2011-01-24
              • 2012-09-17
              • 2019-10-05
              相关资源
              最近更新 更多