13、构造函数与析构函数:默认构造函数、带参构造函数、拷贝构造函数、析构函数、初始化列表

好,咱们今天聊一个C++里绕不开的话题——构造函数和析构函数。说白了,这就是对象的“生”与“死”。

你想想看,一个对象从出生到销毁,总得有人管吧?谁负责给它初始化?谁负责在它“咽气”前打扫战场?嗯,就是这两个家伙。

我个人习惯把构造函数理解为“对象的出生证”,而析构函数就是“死亡证明”。没有它们,你的对象就是个黑户,内存里乱飘,谁也管不了。

13.1 默认构造函数:啥也不传也能干活

默认构造函数,就是不需要你传任何参数就能调用的构造函数。如果你没写任何构造函数,编译器会帮你生成一个——但别高兴太早,它啥也不干。

class Student {
public:
    // 默认构造函数
    Student() {
        name = "未知";
        age = 0;
        std::cout << "默认构造函数被调用" << std::endl;
    }

    void show() {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

private:
    std::string name;
    int age;
};

int main() {
    Student s;  // 调用默认构造函数
    s.show();
    return 0;
}
小提示:一旦你写了任何一个构造函数,编译器就不会再自动生成默认构造函数了。这时候如果你还想用无参构造,就得自己写一个。

13.2 带参构造函数:给对象“上户口”

带参构造函数,说白了就是让你在创建对象时就能把数据传进去。我在项目中遇到过很多新手,先创建对象再一个个赋值,其实完全可以在构造时就搞定。

class Student {
public:
    // 带参构造函数
    Student(std::string n, int a) {
        name = n;
        age = a;
        std::cout << "带参构造函数被调用" << std::endl;
    }

    void show() {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

private:
    std::string name;
    int age;
};

int main() {
    Student s("张三", 20);  // 直接传参
    s.show();
    return 0;
}

13.3 拷贝构造函数:复制一个“双胞胎”

拷贝构造函数,就是用已有对象去初始化新对象。说白了就是“克隆”。

为什么会用到它?比如你传参时按值传递,或者函数返回一个对象,都会触发拷贝构造。

class Student {
public:
    Student(std::string n, int a) : name(n), age(a) {}

    // 拷贝构造函数
    Student(const Student &other) {
        name = other.name;
        age = other.age;
        std::cout << "拷贝构造函数被调用" << std::endl;
    }

    void show() {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

private:
    std::string name;
    int age;
};

int main() {
    Student s1("李四", 22);
    Student s2 = s1;  // 调用拷贝构造函数
    s2.show();
    return 0;
}
注意:如果你在类里管理了动态内存(比如用new分配了堆内存),一定要自己写拷贝构造函数,否则默认的浅拷贝会让你吃大亏。我曾经就因为忘了写拷贝构造函数,导致两个对象指向同一块内存,析构时double free,程序直接崩了。

13.4 析构函数:对象“临终”前的最后交代

析构函数在对象销毁时自动调用。它的主要任务就是释放资源——比如关闭文件、释放堆内存、断开网络连接等。

class FileHandler {
public:
    FileHandler(const std::string &filename) {
        file.open(filename);
        std::cout << "文件已打开" << std::endl;
    }

    // 析构函数
    ~FileHandler() {
        if (file.is_open()) {
            file.close();
            std::cout << "文件已关闭" << std::endl;
        }
    }

private:
    std::fstream file;
};

int main() {
    FileHandler fh("test.txt");
    // 函数结束时,fh对象销毁,自动调用析构函数
    return 0;
}
小技巧:析构函数不能有参数,也不能重载。一个类只能有一个析构函数。

13.5 初始化列表:更高效的初始化方式

初始化列表,就是在构造函数参数列表后面用冒号开头,直接初始化成员变量。我个人非常推荐这种方式,因为它比在函数体内赋值更高效。

为什么?因为初始化列表是直接构造,而函数体内赋值是先默认构造再赋值,多了一步操作。

class Student {
public:
    // 使用初始化列表
    Student(std::string n, int a) : name(n), age(a) {
        std::cout << "初始化列表构造完成" << std::endl;
    }

    void show() {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

private:
    std::string name;
    int age;
};
重点:const成员变量和引用成员变量必须使用初始化列表,因为它们不能在函数体内赋值。

13.6 知识体系总览

下面这张图帮你理清构造函数和析构函数的整体脉络:

构造函数与析构函数知识体系 对象的生命周期 默认构造函数 无参,编译器自动生成 带参构造函数 传参初始化成员 拷贝构造函数 克隆已有对象 析构函数 释放资源,对象销毁 初始化列表:高效初始化成员变量

13.7 各种构造函数的对比

类型 调用时机 特点 注意事项
默认构造函数 创建对象时不传参 无参数,编译器可自动生成 写了其他构造后需手动定义
带参构造函数 创建对象时传参 参数个数和类型可自定义 可配合默认参数使用
拷贝构造函数 用已有对象初始化新对象 参数为const引用 管理动态内存时必须自定义
析构函数 对象销毁时自动调用 无参数,无返回值 不能重载,一个类只有一个
避坑指南:我曾经在项目中遇到一个bug,排查了整整两天——就是因为拷贝构造函数没写,两个对象共享了同一块堆内存,析构时double free。从那以后,只要类里有指针成员,我第一件事就是写拷贝构造函数和赋值运算符。

好了,构造函数和析构函数就聊到这儿。记住一句话:对象的生与死,全在构造函数和析构函数里。写好了,程序稳如泰山;写不好,bug满天飞。


公众号:蓝海资料掘金营,微信deep3321