20、构造函数与析构函数:默认构造、带参构造、拷贝构造、析构函数

说实话,C++ 里最基础也最容易翻车的地方,就是构造函数和析构函数。很多新手觉得这玩意儿不就是初始化对象和清理对象嘛,有啥好讲的?但我在项目中见过太多因为构造/析构没写好导致的诡异 bug——内存泄漏、野指针、double free……嗯,今天咱们就把这四种函数彻底捋清楚。

一、默认构造函数:编译器帮你写的那个

如果你写了一个类,没写任何构造函数,编译器会悄悄给你生成一个默认构造函数。它啥也不干,就是把成员变量按默认方式初始化——基本类型不初始化(值是随机的),类类型会调用它自己的默认构造。

class Student {
public:
    string name;  // 调用 string 的默认构造,初始化为空字符串
    int age;      // 不初始化,值是随机的!
};

int main() {
    Student s;    // 编译器生成的默认构造被调用
    cout << s.age; // 危险!age 的值不确定
    return 0;
}
⚠️ 注意:一旦你写了任何一个构造函数,编译器就不再生成默认构造。这时候如果你还试图用无参方式创建对象,编译会报错。

我个人习惯:只要类里有基本类型成员,就手动写默认构造函数,把它们初始化成安全值(比如 0、nullptr)。别指望编译器帮你擦屁股。

二、带参构造函数:给对象传参数

带参构造让对象在创建时就能接收外部数据。说白了,就是让对象「带着身份出生」。

class Student {
public:
    string name;
    int age;

    // 带参构造函数
    Student(string n, int a) {
        name = n;
        age = a;
    }
};

int main() {
    Student s("张三", 18);  // 调用带参构造
    return 0;
}

这里有个小坑:如果成员变量名和参数名相同,比如写成 name = name,那编译器会傻眼——两个 name 都是参数,成员变量根本没被赋值。我建议用初始化列表,既清晰又高效:

Student(string name, int age) : name(name), age(age) {}
💡 初始化列表比在函数体内赋值效率更高,尤其是对 const 成员、引用成员、以及没有默认构造的类类型成员。这是 C++ 的硬性规定,不是风格问题。

三、拷贝构造函数:复制一个对象

拷贝构造,就是用已有对象去初始化新对象。它的签名长这样:

Student(const Student& other) : name(other.name), age(other.age) {}

什么时候会调用拷贝构造?三种情况:

  • 用对象初始化另一个对象:Student s2 = s1;
  • 函数传参(按值传递):void func(Student s);
  • 函数返回对象(按值返回):Student getStudent();

我在项目中遇到过最经典的 bug:类里有指针成员,用了编译器生成的默认拷贝构造,结果两个对象指向同一块内存。析构时 double free,程序直接崩了。这就是所谓的「浅拷贝」问题。

深拷贝 vs 浅拷贝

浅拷贝:只复制指针值,两个指针指向同一块内存。

深拷贝:复制指针指向的内容,每个对象拥有独立的内存。

如果你的类管理了动态内存(new/delete),一定要自己写拷贝构造函数做深拷贝。

class StringWrapper {
private:
    char* data;
public:
    // 深拷贝构造
    StringWrapper(const StringWrapper& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    ~StringWrapper() {
        delete[] data;
    }
};

四、析构函数:对象销毁前的最后一步

析构函数在对象生命周期结束时自动调用。它没有参数,没有返回值,名字前面加个波浪号:

~Student() {
    // 释放资源
}

析构函数主要用来释放资源:delete 动态内存、关闭文件句柄、释放锁等等。如果你没写析构函数,编译器会生成一个默认的,但它不会帮你释放动态内存。

⚠️ 曾经有个同事,在析构函数里忘了 delete 数组,结果内存泄漏了整整一个季度才发现。从那以后,我每写一个 new,就立刻写对应的 delete,绝不留到后面补。

析构函数的调用顺序和构造顺序相反——先构造的后析构。这个规则在继承和多态场景下尤其重要。

五、知识体系总览

下面这张图把四种函数的定义、调用时机、注意事项串起来了,建议保存下来当速查表:

构造函数与析构函数知识体系 默认构造函数 无参数,编译器自动生成 基本类型不初始化 ⚠ 手动写更安全 带参构造函数 接收参数初始化对象 推荐初始化列表 💡 效率更高 拷贝构造函数 用已有对象初始化新对象 深拷贝 vs 浅拷贝 ⚠ 指针成员要深拷贝 析构函数 对象销毁时自动调用 释放动态资源 ⚠ 别忘了 delete 调用时机 对象创建 → 构造函数(默认/带参/拷贝) → 使用对象 → 对象销毁 → 析构函数 避坑指南 写了带参构造记得写默认构造 | 指针成员必须深拷贝 | 析构顺序与构造相反 | 虚析构解决多态内存泄漏

六、避坑总结

函数类型 常见坑 我的建议
默认构造 基本类型成员未初始化 手动写默认构造,初始化所有成员
带参构造 参数名与成员名冲突 用初始化列表,或者加 this->
拷贝构造 浅拷贝导致 double free 有指针成员必须深拷贝
析构函数 忘记释放资源 new 和 delete 成对写,用 RAII 思想

嗯,这四种函数是 C++ 面向对象的基石。你想想看,一个类连构造和析构都写不好,后面谈什么继承、多态、模板?我个人建议:每写一个类,先把这四种函数想清楚——哪些需要自己写,哪些可以用默认的。养成这个习惯,能省下大量调试时间。

最后分享一个小技巧:C++11 以后可以用 = default 显式要求编译器生成默认版本,用 = delete 禁止某个函数。比如你想禁止拷贝,就写 Student(const Student&) = delete;。这比把拷贝构造声明为 private 更清晰。

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