前言:学习C++,对象(类)是我们绕不开的话题。对象的构造(Construction)析构(Destruction)是对象生命周期的两个端点,也是 C++ 资源管理的核心机制。

构造是对象的"出生证明",析构是对象的"死亡清理"

目录

一、构造函数

1.1 概念及特征

1.2 构造类型

二、析构函数

2.1 概念及特征

2.2 何时调用

2.3 虚析构函数

三、构造与析构顺序


一、构造函数

1.1 概念及特征

构造函数(Constructor)是类的一个特殊的成员函数主要用于在创建对象时自动调用,完成对象的初始化工作(为对象的数据成员分配内存并设置初始值)。

C++构造函数的基本特点主要是:函数名称与类名相同,无返回值,自动调用,支持重载,可由编译器自动生成。例如:

class AA{
public:
    // 默认构造函数
    AA() {
        std::cout << "默认构造\n";
    }
    
    // 带参构造函数
    AA(int id) : id_(id) {
        std::cout << "带参构造: " << id_ << "\n";
    }
    
private:
    int id_ = 0;
};

1.2 构造类型

根据构造方式的不同,C++的构造函数可以分为多种类型:

1) 默认构造函数:

默认构造函数不接受任何参数。如果我们没有在类中没有显式定义任何构造函数,编译器会自动生成一个默认的构造函数。默认构造函数通常对类的每个非静态数据成员执行默认初始化。

class BB{
public:
    BB() { m_value = 0; }  // 默认构造函数
private:
    int m_value;
};

2) 带参构造函数:

带参构造函数接受一个或多个参数,根据提供的参数值进行对象的初始化。带参构造函数可以重载,以支持不同数量或类型的参数组合。

class CC{
public:
    CC(int a, float b) : m_a(a), m_b(b) {
        // 带参构造函数体:使用参数初始化成员变量
    }
private:
    int m_a;
    float m_b;
};

int main() {
    CC c(3, 1.1f);  // 使用带参构造函数创建对象
    return 0;
}

3) 拷贝构造函数:

拷贝构造函数接受一个同类型对象的引用作为参数,用于创建一个与给定对象内容相同的新对象。其作用是用已存在对象的状态来初始化新对象,进行成员的逐个拷贝。

class DD {
public:
    DD(const DD& other) {
        m_real = other.m_real;
        m_imag = other.m_imag;
    }
private:
    double m_real;
    double m_imag;
};

int main() {
    DD d1(1.0, 2.0);
    DD d2 = c1;  // 调用拷贝构造函数,用d1初始化d2
    return 0;
}

使用拷贝构造函数,需要我们理解两个概念:深拷贝与浅拷贝。大家可以参考作者之前的文章:C++ 深拷贝与浅拷贝_浅拷贝和深拷贝 c++-CSDN博客

特别需要注意的是:如果我们没有显示地定义拷贝构造函数,那么系统默认生成的拷贝构造函数是浅拷贝,如果这时候类中存在指针变量,浅拷贝仅会复制指针的值,并没有副本的拷贝,多个指针指向了同一块内存,存在重复释放的问题。通常,我们需要自定义拷贝构造函数来实现深拷贝,避免该问题。

4) 移动构造函数:

移动构造函数接受一个同类型对象的右值引用,其作用是"直接窃取"源对象的资源,减少不必要的拷贝开销。将新对象的指针指向源对象原本的资源,并将源对象的指针成员置为nullptr,从而实现高效的资源转移。

class EE {
public:
    // 移动构造函数
    EE(EE&& other) noexcept : m_data(other.m_data), m_rows(other.m_rows) {
        other.m_data = nullptr;  // 将源对象的指针置空,防止析构时释放资源
        other.m_rows = 0;
    }
    ~EE() {
        delete[] m_data;
    }
private:
    double* m_data;
    size_t m_rows;
};

二、析构函数

2.1 概念及特征

析构函数(Destructor)是类的一个特殊成员函数,它的主要作用是在对象生命周期结束时自动调用,执行清理工作(释放对象占用的资源)。

C++析构函数的主要特点是:函数名为 ~类名,没有任何参数,也没有返回值;自动调用,不支持重载,可由编译器自动生成。例如:

class FF {
    FILE* fp_;
public:
    FF(const char* path) {
        fp_ = fopen(path, "r");
    }
    
    ~FF() {
        if (fp_) {
            fclose(fp_);
            std::cout << "文件关闭,资源释放\n";
        }
    }
};

2.2 何时调用

析构函数一般在以下几种情况下会被自动调用:

1) 当对象离开作用域时,如 栈上的局部对象离开作用域时;

2) 显示delete,在堆上用new分配的对象,当执行delete操作时;

3) 临时对象的销毁,如 函数返回的临时对象,使用完毕后会自动调用析构函数

2.3 虚析构函数

需要特别注意的一点是:如果某个类是一个基类,我们需要将其析构函数声明为虚函数(virtual)。目的是,通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,确保资源的正确释放。

例如:

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor called\n";
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called\n";
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 由于Base的析构函数是虚函数,delete ptr会正确调用Derived的析构函数
    return 0;
}

三、构造与析构顺序

对于存在继承关系的类,基类与派生类的构造与析构的顺序是怎样的呢?记住一个原则:构造时先基后派,析构时先派后基。

构造的时候,先调用基类的构造函数,再调用派生类的构造函数;析构的时候相反,先调用派生类的析构函数,再调用基类的析构函数。

例如:

#include <iostream>

class Base {
public:
    Base()  { std::cout << "Base 构造函数\n"; }
    virtual ~Base() { std::cout << "Base 析构函数\n"; }
};

class Derived : public Base {
public:
    Derived()  { std::cout << "Derived 构造函数\n"; }
    ~Derived() { std::cout << "Derived 析构函数\n"; }
};

int main() {

    Derived d;
    return 0;
}

输出结果如下:

Base 构造函数
Derived 构造函数
Derived 析构函数
Base 析构函数

更多推荐