系列文章目录

后续会将类与对象中重要的六个默认成员函数(构造、析构、拷贝构造、赋值重载、普通对象和const对象的取地址重载)链接放在这目录里,有学习需求的可以先点个收藏~



前言

在 C++ 类与对象体系中,构造函数负责对象创建时的初始化,而析构函数则是对象生命周期的 “收尾者”。二者成对出现,是 C++ 六大默认成员函数中最核心的两组函数。

很多初学者会认为:析构函数是用来销毁对象本身的内存。但这是个误区!!!
对象的栈 / 堆内存由操作系统或new/delete管理,析构函数的核心使命是释放对象持有外部资源(堆内存、文件句柄、网络套接字等)。


一、析构函数核心定义与作用

1. 核心定位

析构函数(Destructor)是类的特殊成员函数,属于 C++ 六大默认成员函数之一。

  • 执行时机对象生命周期结束时,系统自动调用,无需手动触发;
  • 核心功能:清理对象在运行过程中申请的外部资源(堆内存、打开的文件、硬件句柄等);
  • 对比 C 语言:C 语言使用结构体时,需要手动编写Destroy销毁函数并主动调用;C++ 依靠析构函数自动完成资源清理,彻底避免 “忘记释放资源导致内存泄漏” 的问题。

2. 关键误区纠正(必看)

  1. 错误认知:析构函数负责释放对象本身的内存。
  2. 正确认知:析构函数仅清理对象内部引用的外部资源(比如栈类中数组指向的堆内存)。

解释:
局部栈对象:内存由函数栈帧管理,函数结束后栈帧自动回收,和析构无关;
堆对象(new创建):对象内存由delete释放,delete会先调用析构函数,再释放内存。

3. 应用场景区分

  • 无外部资源的类(如:Date日期类,成员均为int内置类型):无需手动写析构,编译器会默认生成析构函数;
  • 持有动态内存 / 文件句柄的类(如:Stack栈类):必须手动实现析构函数,否则必然造成内存泄漏。

二、析构函数语法规则

1. 基础语法格式

析构函数命名规则:类名前加波浪号 ~,格式如下:

class 类名
{
public:
    ~类名()
    {
        // 资源清理逻辑
    }
};

2. 七大硬性规则(考点汇总)

规则 1:命名固定,格式唯一

析构函数名必须是 ~类名,波浪号是析构函数的专属标识,不能修改。

规则 2:无返回值、无参数

  • 不写返回值(连void都不需要,C++ 语法强制规定);
  • 没有任何形参。

延伸推论:因为没有参数,析构函数无法重载。一个类有且只能有一个析构函数(构造函数可以重载,这是二者核心区别)。

规则 3:自动调用,禁止手动常规调用

对象生命周期结束时系统自动执行,正常开发中不需要手动调用。语法上虽然支持显式调用,但毫无意义且可能引起重复析构问题。

规则 4:编译器会生成默认析构

如果我们没有显式定义析构函数,编译器会自动生成一个默认析构函数
默认析构的行为分两种成员处理:

  1. 内置类型成员(int、char、指针等):默认析构不做任何处理
  2. 自定义类成员(类中嵌套其他类对象):自动调用该成员自身的析构函数,逐层清理。

规则 5:自定义析构不会影响成员析构

哪怕我们手动实现了析构函数,类中的自定义类型成员依然会在当前析构函数执行完毕后,自动调用自身析构。

规则 6:局部对象析构顺序

在同一个局部作用域中,后定义的对象先析构(遵循栈 “后进先出” 的特性)。

规则 7:资源类必须手动实现析构

只要类中通过malloc/new/fopen等申请了外部资源,必须手写析构函数进行释放资源,否则会内存泄漏。

3. 无资源析构代码示例:

Date类成员均为内置int,无外部资源,使用默认析构即可:

#include <iostream>
using namespace std;

class Date
{
public:
    // 全缺省构造函数
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print()
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }

    // 编译器自动生成默认析构,此处省略
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2026, 6, 8);
    d1.Print();
    // 函数结束,d1生命周期结束,自动调用默认析构
    return 0;
}

三、析构函数的四大调用时机

不同存储类型的对象,析构函数触发时机完全不同。

场景 1:局部栈对象(最常用)

触发时机:对象所在局部作用域结束(函数结束、代码块{}结束),自动调用析构。

#include <iostream>
using namespace std;

class Test
{
public:
    ~Test()
    {
        cout << "Test 析构函数被调用" << endl;
    }
};

void Func()
{
    Test t1; // 定义局部对象
    cout << "函数Func执行中" << endl;
} // 作用域结束,t1销毁 → 调用析构

int main()
{
    Func();
    cout << "main函数继续执行" << endl;
    return 0;
}

运行结果:
在这里插入图片描述

场景 2:全局 / 静态对象

触发时机:整个程序正常退出时,才会调用析构。
全局 / 静态对象生命周期贯穿整个程序,函数结束不会触发析构。

#include <iostream>
using namespace std;

class Test
{
public:
    ~Test()
    {
        cout << "Test 析构函数被调用" << endl;
    }
};

Test g_t; // 全局对象

void Func()
{
    static Test s_t; // 静态局部对象
    cout << "函数Func执行中" << endl;
}

int main()
{
    Func();
    cout << "main函数执行完毕" << endl;
    return 0; // 程序退出,全局、静态对象依次析构
}

运行结果:
在这里插入图片描述

场景 3:堆对象(new 创建)

触发时机:必须手动使用delete释放对象,delete会先调用析构函数,再释放堆内存
如果只newdelete:析构永远不执行,造成内存泄漏

#include <iostream>
using namespace std;

class Test
{
public:
    ~Test()
    {
        cout << "Test 析构函数被调用" << endl;
    }
};

int main()
{
    Test* p = new Test; // new创建堆对象,仅调用构造
    cout << "堆对象使用中" << endl;
    delete p; // 先调用析构,再释放堆内存
    p = nullptr;
    return 0;
}

运行结果:
在这里插入图片描述

场景 4:临时对象

触发时机:创建临时对象的完整表达式执行完毕,立即调用析构。

#include <iostream>
using namespace std;

class Test
{
public:
    Test() { cout << "构造" << endl; }
    ~Test() { cout << "析构" << endl; }
};

int main()
{
    cout << "开始" << endl;
    Test(); // 创建临时对象,表达式结束立即析构
    cout << "结束" << endl;
    return 0;
}

运行结果:

#include <iostream>
using namespace std;

class Test
{
public:
    Test() { cout << "构造" << endl; }
    ~Test() { cout << "析构" << endl; }
};

int main()
{
    cout << "开始" << endl;
    Test(); // 创建临时对象,表达式结束立即析构
    cout << "结束" << endl;
    return 0;
}

四、默认析构函数实战:嵌套类成员

例子:用两个栈实现队列
MyQueue类包含两个Stack类型的成员对象,用来理解自定义成员的析构规则。

1. 代码实现

#include <iostream>
#include <cstdlib>
using namespace std;

typedef int STDateType;

// 栈类:持有堆内存,手动实现析构
class Stack
{
public:
    // 全缺省构造,默认容量4
    Stack(int n = 4)
    {
        _a = (STDateType*)malloc(sizeof(STDateType) * n);
        if (nullptr == _a)
        {
            perror("malloc 申请空间失败");
            exit(1);
        }
        _capacity = n;
        _top = 0;
    }

    // 手动实现析构:释放堆内存
    ~Stack()
    {
        cout << "~Stack() 栈析构" << endl;
        free(_a);
        _a = nullptr;
        _capacity = 0;
        _top = 0;
    }

private:
    STDateType* _a;   // 指向堆内存
    size_t _capacity;
    size_t _top;
};

// 队列类:嵌套两个Stack成员(无自定义析构,使用默认析构)
class MyQueue
{
private:
    Stack pushst; // 入栈成员
    Stack popst;  // 出栈成员
};

int main()
{
    MyQueue mq; // 创建队列对象
    return 0;   // 作用域结束,mq析构
}

2. 流程解析

  1. MyQueue没有手动写析构,使用编译器默认析构
  2. 默认析构不会处理内置成员,但会依次调用所有自定义成员(pushst、popst)的析构函数
  3. 最终两个Stack对象的析构函数被执行,堆内存正常释放,无内存泄漏。

3. 结论

  • 嵌套自定义成员的类,哪怕不写析构,成员的资源也会被自动清理;
  • 只有当前类自身申请外部资源时,才需要手动实现析构。

五、有资源析构实战(Stack 栈类)

通过Stack栈类,对比C 语言手动销毁C++ 析构自动销毁的差异。

1. C 语言版本

#include <stdio.h>
#include <stdlib.h>
typedef int STDataType;

typedef struct Stack
{
    STDataType* _a;
    size_t _capacity;
    size_t _top;
}ST;

// 初始化
void STInit(ST* st, int n)
{
    st->_a = (STDataType*)malloc(sizeof(STDataType)*n);
    st->_capacity = n;
    st->_top = 0;
}

// 手动销毁(必须主动调用,容易遗忘)
void STDestroy(ST* st)
{
    free(st->_a);
    st->_a = NULL;
}

int main()
{
    ST st;
    STInit(&st, 4);
    // 业务逻辑...
    STDestroy(&st); // 忘记调用会内存泄漏
    return 0;
}

2. C++ 版本

#include <iostream>
#include <cstdlib>
using namespace std;

typedef int STDateType;
class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc 申请空间失败");
            exit(1);
        }
        _capacity = n;
        _top = 0;
    }

    // 析构自动释放堆内存,无需手动调用
    ~Stack()
    {
        free(_a);
        _a = nullptr;
        _capacity = 0;
        _top = 0;
    }

    void Push(STDataType x)
    {
        _a[_top++] = x;
    }

private:
    STDateType* _a;
    size_t _capacity;
    size_t _top;
};

int main()
{
    Stack st; // 构造初始化
    st.Push(10);
    st.Push(20);
    // 无需手动销毁,函数结束自动析构释放内存
    return 0;
}

六、析构顺序(重点!!!)

1. 基础规则

同一局部作用域内的多个对象:

  • 构造顺序:从上到下(代码书写顺序)
  • 析构顺序:从下到上(后构造的对象先析构)

本质原因:局部对象存储在栈中,栈遵循 后进先出(LIFO) 规则。

2. 代码示例:

#include <iostream>
using namespace std;

class Test
{
public:
    Test(int id) 
    : _id(id)
    {
        cout << "构造对象:" << _id << endl;
    }
    ~Test()
    {
        cout << "析构对象:" << _id << endl;
    }
private:
    int _id;
};

int main()
{
    Test t1(1); // 第一个构造
    Test t2(2); // 第二个构造
    Test t3(3); // 第三个构造
    cout << "执行完毕" << endl;
    return 0;
}

运行结果:
在这里插入图片描述
可以清晰看到:t3最后构造,最先析构;t1最先构造,最后析构。

3. 嵌套成员析构顺序

对于包含自定义成员的类:

  • 构造顺序:先构造成员 → 再构造当前对象
  • 析构顺序:先析构当前对象 → 再析构成员(逆序)

总结

下一篇讲拷贝构造函数

更多推荐