delete了,析构函数却没有调用

本文详细解释了C++中析构函数的作用,特别关注了delete操作与析构函数之间的交互。通过一个示例程序,展示了在不同情况下,析构函数是如何被调用的,以及不调用析构函数可能导致的问题。通过反汇编代码对比,直观展示了加入与未加入特定头文件对析构函数调用的影响。

析构函数在对象的生命结束时,会自动调用,大家所熟知的智能指针就是根据析构函数的这种特性而实现的,包括Qt的内存管理机制,也都是利用了析构函数的这一机制来实现的。c++创始人Bjarne Stroustrup在创造析构函数也是出于这种目的的,可见如果析构函数用的好的话,可以省去我们很多工作量,你不再需要手工调用删除对象使用的堆内存,你只需要把要删除的堆内存放入析构函数就行了,因为当对象离开其生命周期的时候,析构函数会自动调用,C++语言规范是这样规定析构函数的调用的:

Destructors are invoked implicitly (1) for a constructed object with static storage duration (3.7.1) at program termination (3.6.3), (2) for a constructed object with automatic storage duration (3.7.2) when the block in which the object is created exits (6.7), (3) for a constructed temporary object when the lifetime of the temporary object ends (12.2), (4) for a constructed object allocated by a newexpression (5.3.4), through use of a deleteexpression (5.3.5), (5) in several situations due to the handling of exceptions (15.3). A program is illformed if an object of class type or array thereof is declared and the destructor for the class is not accessible at the point of the declaration. Destructors can also be invoked explicitly.

大意是:

析构函数可以由以下五种方式隐含调用:

(1)在静态存储区(也即全局对象或静态对象,这个对象放在程序的数据区)里面构造的对象,当程序结束时,对象的析构函数会自动调用。

(2)在自动存储区(也即局部对象,这个对象放在程序的栈里)里面构造的对象离开其区域时,如1个函数内声明的对象,离开函数作用域时,对象的构造函数会自动调用。

(3)临时构造的对象当离开其生命周期时,该对象的析构函数会调用,此处同(2)。

(4)new构造的对象(即对象在堆区),通过delete删除,析构会调用

(5)在try,catch处理异常的情况下,当在try块中对象,因为异常,进入catch分支,会在catch分支中调用构造函数

以上5种是通过编译器生成的默认调用方式,当然了,还有1种就是可以通过显示的方式调用,也即像调用函数一样调用析构函数

有了上面的介绍,接下来进入我们的主题, delete了,却不调用析构函数的情况,这与上面说的C++的第(4)条规范相悖,下面我以一个简单的示例来说明:

示例由7个文件组成,testa.cpp,testa.h, testb.cpp,testb.h,testapp.h,testapp.cpp,main.cpp

testa.h文件
#ifndef _TEST_A_H
#define _TEST_A_H
class CTestA {
public:
    CTestA();
    ~CTestA();
};
#endif

testa.cpp文件

#include "testa.h"
#include <qdebug.h>
CTestA::CTestA()
{

}

CTestA::~CTestA()
{
    qDebug() << "~CTestA()";
}

testb.h文件

#ifndef _TEST_B_H
#define _TEST_B_H
class CTestA;
class CTestB {
public:
    static CTestB *getInstance();
    CTestA *getTestA();

private:
    CTestB();
    ~CTestB();

private:
    CTestA *pTestA;
    static CTestB mSelf;
};
#endif

testb.cpp文件
#include "testa.h"
CTestB CTestB::mSelf;
CTestB *CTestB::getInstance()
{
    return &mSelf;
}
CTestB::CTestB()
{
    pTestA = new CTestA();
}

CTestB::~CTestB()
{
    delete pTestA;
    qDebug() << "~CTestB()";
}

CTestA *CTestB::getTestA()
{
    return pTestA;
}

testapp.h文件

#ifndef _TEST_APP_H
#define _TEST_APP_H
class CTestA;
class CTestApp {
public:
    CTestApp(CTestA *pTestA);
    ~CTestApp();
private:
    CTestA *pTestA;
};
#endif

testapp.cpp文件

#include "testapp.h"
#include <qdebug.h>
//#include "testa.h"
CTestApp::CTestApp(CTestA *pTestA)
{
    this->pTestA = pTestA;
}

CTestApp::~CTestApp()
{
    delete pTestA;
    qDebug() << "~CTestApp()";
}

main.cpp文件
#include "testb.h"
#include "testcpp.h"
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    CTestB *pTestB = CTestB::getInstance();
    CTestApp *pTestApp = new CTestApp(pTestB->getTestA());
    delete pTestApp;
    return app.exec();
}
下面是输出结果,

~CTestApp() 

说明delete pTestA;后没有调用pTestA的析构函数,当把testapp.cpp文件的//#include "testa.h"注释取消,delete后会调用pTestA析构函数,这是去掉注释后的输出结果:

~CTestA()

~CTestApp() 

注以上的编译环境是ming32-make,不知道其他的编译环境会不会出现如此问题,这个留给大家去验证。

下面是反汇编代码,这是在testapp.cpp里面加了testa.h的delete pTestA反汇编代码:

delete pTestA;
0x0040255c  <+8>:            	mov    0x8(%ebp),%eax
0x0040255f  <+11>:            	mov    (%eax),%ebx
0x00402561  <+13>:            	test   %ebx,%ebx
0x00402563  <+15>:            	je     0x402575 <~CTestApp+33>
0x00402565  <+17>:            	mov    %ebx,(%esp)
0x00402568  <+20>:            	call   0x403082 <~CTestA>
0x0040256d  <+25>:            	mov    %ebx,(%esp)
0x00402570  <+28>:            	call   0x40aa68 <_ZdlPv>

这是在testapp.cpp里面没有加testa.h的delete pTestA反汇编代码:

delete pTestA;
0x00402550  <+8>:            	mov    0x8(%ebp),%eax
0x00402553  <+11>:            	mov    (%eax),%eax
0x00402555  <+13>:            	mov    %eax,(%esp)
0x00402558  <+16>:            	call   0x40aa48 <_ZdlPv>
可以看到加了testa.h的反汇编中,调用了析构函数~CTestA, call 0x403082 <~CTestA>



<think>我们正在讨论C++中的析构函数调用时机。根据提供的引用和之前的知识,我将系统地解释析构函数在C++中被调用的各种情况。析构函数是类的一个特殊成员函数,其名称是在类名前加上波浪线(~),例如`~ClassName()`。它用于在对象销毁时执行清理工作,比如释放内存、关闭文件等。析构函数在以下情况下被调用:###1.**局部对象离开作用域**当对象在某个作用域内被创建(例如在函数内部定义),当程序执行离开这个作用域时,对象的析构函数会被自动调用。```cppvoidfunc(){MyClassobj;//造函数被调用//...使用obj}//函数结束,obj离开作用域,析构函数调用```###2.**动态分配的对象被删除**使用`new`运算符动态创建的对象,当使用`delete`运算符释放该对象时,其析构函数会被调用。```cppMyClass*ptr=newMyClass();//造函数被调用//...使用ptrdeleteptr;//析构函数调用```###3.**对象作为函数参数(按值传递)**当对象按值传递给函数时,会创建该对象的副本(调用拷贝造函数)。当函数结束时,这个副本对象的析构函数会被调用[^4]。```cppvoidfunc(MyClassobj){//拷贝造函数被调用//...使用obj}//函数结束,副本obj的析构函数调用intmain(){MyClassobj1;func(obj1);//传递obj1的副本}```###4.**临时对象生命周期结束**临时对象(例如函数返回的对象)在表达式结束时被销毁。```cppMyClasscreateObject(){returnMyClass();//创建临时对象}intmain(){MyClassobj=createObject();//临时对象在表达式结束后被销毁(可能涉及拷贝优化)}```注意:编译器可能会进行返回值优化(RVO)或命名返回值优化(NRVO),从而避免创建临时对象。###5.**容器中的元素被移除或容器销毁**当容器(如`vector`、`list`等)被销毁,或者从容器中移除元素时,每个元素的析构函数会被调用。```cppstd::vector<MyClass>vec;vec.push_back(MyClass());//临时对象被添加到容器vec.pop_back();//被移除元素的析构函数调用//vec销毁时,剩余元素的析构函数调用```###6.**派生类对象销毁时基类析构函数调用**当派生类对象被销毁时,其析构函数执行完毕后会自动调用基类的析构函数(即使基类析构函数不是虚函数)[^5]。但需要注意的是,如果基类指针指向派生类对象,且基类析构函数不是虚函数,那么通过基类指针删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类资源泄漏[^2]。```cppclassBase{public:~Base(){std::cout<<"Basedestructor\n";}};classDerived:publicBase{public:~Derived(){std::cout<<"Deriveddestructor\n";}};intmain(){Base*p=newDerived();deletep;//如果Base的析构函数不是虚函数,则只调用Base的析构函数}```因此,通常建议将基类的析构函数声明为虚函数:```cppclassBase{public:virtual~Base(){...}//虚析构函数};```###7.**异常抛出时的栈展开(StackUnwinding)**在抛出异常时,栈展开过程中会销毁局部对象,从而调用析构函数。```cppvoidfunc(){MyClassobj;throwstd::runtime_error("error");//obj的析构函数会被调用}```###总结析构函数调用时机主要与对象的生命周期结束相关:-局部对象离开作用域-动态对象被`delete`-函数参数(按值传递)在函数结束时-临时对象在表达式结束时-容器元素被移除或容器销毁-派生类对象销毁时基类析构函数调用(顺序:先派生类后基类)-异常抛出时的栈展开正确理解析构函数调用时机对于资源管理至关重要,尤其是当类拥有资源(如动态内存、文件句柄等)时,必须确保在析构函数中正确释放这些资源,避免内存泄漏。---###相关问题1.为什么基类析构函数通常需要声明为虚函数?2.在造函数中抛出异常会导致析构函数调用吗?3.如何避免析构函数中抛出异常?4.C++中的RAII(资源获取即初始化)原则与析构函数有什么关系?5.在多线程环境下析构函数需要注意什么?[^1]:造函数中申请的资源需要在析构函数中释放[^2]:基类析构函数非虚函数可能导致派生类资源泄漏[^4]:函数参数(按值传递)在函数结束时[^5]:派生类时会自动调用基类析构函数</think>### C++ 析构函数调用时机详解 析构函数在以下 6 种情况下会被自动调用: #### 1. **局部对象离开作用域** 当对象在其定义的作用域结束时(如函数返回、代码块结束),会自动调用析构函数: ```cpp void test() { MyClass obj; // 造函数调用 } // obj离开作用域,析构函数自动调用[^4] ``` #### 2. **动态分配对象被删除** 使用 `new` 创建的对象,在 `delete` 时调用析构函数: ```cpp MyClass* p = new MyClass(); // 造函数 delete p; // 析构函数调用[^1] ``` #### 3. **对象作为函数参数(按值传递)** 当对象按值传递给函数时,函数结束时形参对象的析构函数会被调用: ```cpp void process(MyClass param) { // 函数结束时param的析构函数调用[^4] } ``` #### 4. **临时对象生命周期结束** 表达式中的临时对象在表达式结束后销毁: ```cpp MyClass createTemp() { return MyClass(); // 创建临时对象 } // 表达式结束,临时对象析构函数调用[^3] ``` #### 5. **容器元素被销毁** 容器(如 `vector`, `list`)销毁或删除元素时: ```cpp std::vector<MyClass> vec(3); vec.pop_back(); // 被删除元素的析构函数调用 vec.clear(); // 所有元素析构函数调用 ``` #### 6. **继承体系中的对象销毁** 派生类对象销毁时,顺序为: 1. 先调用派生类析构函数 2. **自动调用**基类析构函数(无需显式调用)[^5] ```cpp class Base { ~Base() {} }; class Derived : public Base { ~Derived() {} }; Derived d; // d销毁时:~Derived() → ~Base() 自动调用[^2][^5] ``` --- ### 关键注意事项 1. **继承场景中的虚析构函数** 当通过基类指针删除派生类对象时,**必须将基类析构函数声明为虚函数**,否则只会调用基类析构函数,导致派生类资源泄漏[^2]: ```cpp class Base { public: virtual ~Base() {} // 必须为虚函数 }; ``` 2. **造函数异常时的** 若造函数抛出异常,**已造的成员**和**基类子对象**的析构函数会被调用,但当前类的析构函数**不会执行**。 3. **顺序规则** - 成员变量:按声明顺序**逆序** - 继承体系:从派生类到基类**自顶向下** 4. **显式调用析构函数(危险!)** 手动调用析构函数可能导致**重复释放**: ```cpp MyClass obj; obj.~MyClass(); // 危险!离开作用域时会再次调用 ``` --- ### 完整示例 ```cpp #include <iostream> class Item { public: Item(int id) : id(id) {} ~Item() { std::cout << "Destroying item " << id << "\n"; } private: int id; }; int main() { Item a(1); // 局部对象 Item* p = new Item(2); // 动态对象 delete p; // item2 [^1] { // 新作用域 Item b(3); } // b离开作用域,item3 [^4] return 0; } // a离开作用域,item1 ``` 输出: ``` Destroying item 2 Destroying item 3 Destroying item 1 ``` --- ### 相关问题 1. 为什么基类析构函数需要声明为虚函数? 2. 造函数抛出异常时析构函数如何执行? 3. C++中的RAII(资源获取即初始化)原则如何利用析构函数? 4. 在多线程环境下析构函数需要注意什么? 5. 如何防止析构函数被意外调用? [^1]: 动态分配对象需手动释放 [^2]: 虚析构函数确保正确调用派生类 [^3]: 临时对象在表达式结束时销毁 [^4]: 函数形参在函数结束时 [^5]: 派生类自动调用基类
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RabinSong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值