也谈析构:析构函数何时被调用

本文结合代码示例,介绍了C++析构函数在三种情况下的调用:对象生命周期结束、delete指向对象的指针、对象成员的析构函数调用。还说明了析构函数与构造函数调用顺序相反,以及基类析构函数声明为虚函数的重要性,强调了虚函数是多态的基础。

为什么要说“也”?用google搜索“析构函数”是,google会说“约有81,500项符合 析构函数 的查询结果”,我最近复习c++是有所心得,所以“也”想谈谈“析构函数”。我不想像教科书似的介绍它,而是从它何时被调用来浅谈一下。

析构函数在下边3种情况时被调用:
1.对象生命周期结束,被销毁时;
2.delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
3.对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

情况1请看下边代码:
#include<iostream.h>
class A
{
 public:
 A()
 {
  cout<<"constructing A"<<endl;
 } 
 ~A()
 {
  cout<<"destructing A"<<endl;
 }
 private:
 int a;
};
class B: public A
{
 public:
 B()
 {
  cout<<"constructing B"<<endl;
 }
 ~B()
 {
  cout<<"destructing B"<<endl;
 }
 private:
 int b;
};

void main()
{
 B b;
}

运行结果为:

constructing A
constructing B
destructing B
destructing A

上述代码还说明了一件事:析构函数的调用顺序与构造函数的调用顺序相反。

情况2则正好说明了为什么基类应该把析构函数声明为虚函数,请先看下边的例子:

#include<iostream.h>
class A
{
 public:
 A()
 {
  cout<<"constructing A"<<endl;
 } 
 ~A()
 {
  cout<<"destructing A"<<endl;
 }
 private:
 int a;
};
class B: public A
{
 public:
 B()
 {
  cout<<"constructing B"<<endl;
 }
 ~B()
 {
  cout<<"destructing B"<<endl;
 }
 private:
 int b;
};

void main()
{
 A* a = new B;
 delete a;
}

运行结果为:

constructing A
constructing B
destructing A

若将class A中的析构函数声明为虚函数,运行结果将变成:

constructing A
constructing B
destructing B
destructing A

由此还可以看出虚函数还是多态的基础,才c++中没有虚函数就无法实现多态。因为不声明成虚函数就不能“推迟联编”,所以不能实现多态。这点上和java不同,java总是“推迟联编”的,所以也剩了这些麻烦。

扯远了,再看情况3,通过下边代码表示:
#include<iostream.h>
class A
{
 public:
 A()
 {
  cout<<"constructing A"<<endl;
 }
 ~A()
 {
  cout<<"destructing A"<<endl;
 }
 private:
 int a;
};

class C
{
 public:
 C()
 {
  cout<<"constructing C"<<endl;
 }
 ~C()
 {
  cout<<"destructing C"<<endl;
 }
 private:
  int c;
};

class B: public A
{
 public:
 B()
 {
  cout<<"constructing B"<<endl;
 }
 ~B()
 {
  cout<<"destructing B"<<endl;
 }
 private:
 int b;
 C c;
};

void main()
{
 B b;
}

运行结果为:

constructing A
constructing C
constructing B
destructing B
destructing C
destructing A

b的析构函数调用之后,又调用了b的成员c的析构函数,同时再次验证了析构函数的调用顺序与构造函数的调用顺序相反。

若将上边的代码中的main()函数内容改成

 A* a = new B;
 delete a;
 
由情况2我们知道,这将不会调用class B的析构函数不会被调用,所以class C的析构函数也不会被调用。
正如我们想的,运行结果为:

constructing A
constructing C
constructing B
destructing A

俗话说温故而知新,我却不想做什么师,只是希望能够和大家分享一下对析构函数和虚析构函数的更深的认识。以上代码在VC++6.0上测试通过,如有疏漏或错误的认识请大家指正:)

<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]: 派生类析自动调用基类析
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值