【问题标题】:Why is a destructor called if it's deleted and not called if it's not deleted?为什么析构函数在被删除时调用,而在未删除时不调用?
【发布时间】:2015-01-15 04:25:15
【问题描述】:

考虑以下代码:

#include <iostream>

struct A 
{
    A(){ };
    ~A(){ std::cout << "~A::A()" << std::endl; };
};

struct B: A { };

B *b = new B; //Doesn't produce any side-effect.

int main(){ }

DEMO

程序不产生任何输出,这意味着没有调用析构函数。但是如果我们用delete 说明符替换析构函数的主体,程序甚至不会编译。

#include <iostream>

struct A 
{
    A(){ };
    ~A() = delete; //{ std::cout << "~A::A()" << std::endl; };
};

struct B: A { };

B *b = new B; //Error: use of deleted function

int main(){ }

DEMO

由于调用了已删除的函数。这就是在这种情况下被调用的析构函数。为什么会有这样的差异?

即使我们显式定义B的构造函数也行不通:

#include <iostream>

struct A 
{
    A(){ };
    ~A() = delete; //{ std::cout << "~A::A()" << std::endl; };
};

struct B: A 
{ 
    B(){ };
};

B *b = new B;

int main(){ }

DEMO

【问题讨论】:

  • 如果您使用new 分配但从不使用delete 解除分配,则不会调用析构函数。
  • 另外,如果您更仔细地阅读演示的输出;该错误是由调用已删除的 constructor 引起的
  • @CoffeeandCode main.cpp:9:12: error: use of deleted function 'A::~A()' 这正是析构函数,而不是构造函数。
  • 为什么是-1?这是一个非常有趣的问题,简单明了,我非常想知道正确答案(我发布了一个但不是 100% 确定我答对了)。
  • 析构函数是被调用还是可能被调用是两个完全不同的问题。在您的演示代码中,由于显而易见的原因,没有调用析构函数。但是,为了使代码可编译,基类析构函数 A::~A() 仍然必须存在。因此,如果您使用 'deletel' 说明符显式删除该函数,它不会编译,尽管没有被调用。

标签: c++ c++11 destructor


【解决方案1】:

问题是B 的构造函数被编译器删除了,否则默认定义将是错误的。那是因为A没有析构函数,如果A不能被销毁,B的默认构造函数不能从A构造一个B。如果您使用 g++ 编译,这就是您遇到的错误:

note: 'B::B()' is implicitly deleted because the default definition would be ill-formed:

或clang++:

error: call to implicitly-deleted default constructor of 'B'

如果你显式声明构造函数B(){},那么它会报错,因为它不能破坏BA 部分,因为A::~A() 的删除。 B 掩埋其父 A 的能力在编译时被检查,所以你得到一个错误。

+1 回答问题。似乎您不能从具有已删除析构函数的类继承(然后使用实例),尽管这是我第一次遇到这个问题。

【讨论】:

  • @remyabel 也在查看 12.4,您可能是对的,似乎 12.4/7 可以。还有 12.4/8,间接:After executing the body of the destructor and destroying any automatic objects allocated within the body, a destructor for class X calls the destructors for X’s direct non-variant non-static data members, the destructors for X’s direct base classes and, if X is the type of the most derived class (12.6.2), its destructor calls the destructors for X’s virtual base classes. 它不能“调用”已删除的析构函数,所以我们得到一个错误。
  • @remyabel 我还得说,对于标准,我不知道删除的 dtor 是否意味着“已定义”,如“定义为已删除”,尽管它不会做太多感觉。
  • 而 8.4.3 则更加模糊:A function definition of the form &lt;attribute-specifier-seqopt decl-specifier-seqopt declarator virt-specifier-seqopt = delete ;&gt; is called a deleted definition
  • 这些标准引号并不能准确解释为什么 B::B() 格式不正确,是吗?
【解决方案2】:

标准的适用部分是(引用N4140):

§12.4 [class.dtor]/p11:

一个析构函数可能被调用如果它被调用或指定 在 5.3.4 和 12.6.2 中。如果析构函数为 潜在调用被删除或无法从上下文中访问 调用。

§12.6.2 [class.base.init]/p10:

在非委托构造函数中,每个潜在的析构函数 类类型的构造子对象可能被调用(12.4)。 [ 注意:此规定确保可以为完全构造的子对象调用析构函数,以防抛出异常(15.2)。 —尾注 ]

§12 [特殊]/p5:

对于一个类,它的非静态数据成员,它的非虚拟直接基 类,并且,如果类不是抽象的(10.4),它的虚拟基 类被称为其可能构造的子对象

由于AB 的非虚拟直接基类,因此是B可能构造的子对象,它的析构函数可能在@ 中调用 987654325@,并且由于该析构函数被删除,因此程序格式错误。

另见CWG issue 1424

【讨论】:

  • N4140 不是标准!不过答案很好;只是发现这个逻辑已经被添加到线程中-.-
  • @LightnessRacesinOrbit 我同意,答案很明确......这正是我想要的......有点
【解决方案3】:

关于我认为你的基本问题:为什么你不能构造 B,尽管它只是 A析构函数 存在:在B的构造函数中,编译器自动生成 如果有异常,代码调用A 的析构函数。如果A 是 要用作基类,它必须有一个可访问的析构函数 (公共的或受保护的)。

当然,在你的情况下,B 的构造函数不能抛出,所以 A::~A() 永远不会真正被调用。但编译器不能总是 确定是否是这种情况,并且标准不要求 它甚至尝试。编译器必须假定B::B() 的主体可能 抛出(在完全构建 A 之后)。而且即使可以 确定B::B() 不能抛出,并且不生成代码 调用A的析构函数,这是优化,不是 允许更改代码的合法性。

【讨论】:

  • 如果将构造函数标记为noexcept 会怎样?在这种情况下代码也无法编译。
  • @vsoftco noexcept 不会告诉编译器构造函数不会抛出;它只指定一个特定的行为,以确保异常不会传播。知道不会传播异常对客户端代码非常有用,但对编译器没有任何帮助。
  • James,感谢您的澄清,我之前没有完全理解noexcept
【解决方案4】:

基于第一个代码:

#include <iostream>
struct A
{
  A(){ };
  ~A(){ std::cout << "~A::A()" << std::endl; };
};

struct B: A { };

B *b = new B; //Doesn't produce any side-effect.

int main(){ }
  1. 对象 b 被声明为全局对象,因此它的生命周期与程序运行一样长。
  2. 对象 b 是动态分配的,因此需要“裸”删除。

尝试以下方法:

#include <iostream>

struct A 
{
    A(){ };
    ~A(){ std::cout << "~A::A()" << std::endl; };
};

struct B: A { };

int main(){
    B b;
}

【讨论】:

    【解决方案5】:

    C++14 对此很清楚:

    [C++14: 12.6.2/10]: 在非委托构造函数中,每个可能构造的类类型子对象的析构函数都可能被调用 (12.4)。 [注意:此规定确保在抛出异常的情况下可以为完全构造的子对象调用析构函数(15.2)。 ——尾注]

    C++11 中没有 it was added in issue 1424 这样的措辞,但由于这个事实不容忽视,在 C++11 和 C++98/03 的实现中也是如此。

    因此,尽管您仍然没有调用析构函数,但 inheritance 的存在意味着 B() 要求 ~A() 是可调用的:基本上,这意味着 可访问 并且没有删除

    【讨论】:

      【解决方案6】:

      回答您的标题问题:如果您不调用delete,则不会发生删除,尽管对象使用的内存可以通过终止创建它的程序来释放,这可能导致以下情况之一有几件事要发生:

      • 内存被操作系统标记为空闲/可用,因为拥有的进程已终止,然后内存再次可供操作系统/其他进程使用
      • 当主进程终止时,所有对象的析构函数都会被调用,无论是隐式析构函数还是显式声明的析构函数(然后内存再次可供操作系统/其他进程使用)。
      • 来自原始代码的纯粹原始意志力超越了编程功能,清除了由未删除对象分配的内存内容,并将内存返回给操作系统。

      嗯,除了最后一个,你明白了要点,最后一个只是为了支持编码人员的自我而喜欢的多愁善感:D

      唯一的条件是孤立对象(指向内存位置的指针在代码中的某个点丢失,see here for example)和带有已删除析构函数的对象(see here for a brief explanation):这些不能删除,因为在第一种情况下它们不再可寻址,而在第二种情况下,它们是错误的*对象,因为它们没有析构函数(*错误,因为不符合标准使用/规范,但是有时您可能想要阻止对象被自己删除/在拥有进程终止之前,例如特殊情况singleton,但是,良好的编程/逻辑应该防止对析构函数的需求根本不可用。

      如果您需要更多信息,或者以上任何内容需要澄清,请告诉我,我将非常乐意提供帮助:)

      【讨论】:

      • 事后看来,我回答得太快了,错过了问题的主旨,当我有时间时,我会修正我的答案。干杯巴里指出这一点:)
      猜你喜欢
      • 1970-01-01
      • 2013-11-16
      • 2013-10-10
      • 2011-03-23
      • 2019-01-29
      • 1970-01-01
      • 2018-01-21
      • 2012-03-13
      相关资源
      最近更新 更多