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) {}
三、拷贝构造函数:复制一个对象
拷贝构造,就是用已有对象去初始化新对象。它的签名长这样:
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 动态内存、关闭文件句柄、释放锁等等。如果你没写析构函数,编译器会生成一个默认的,但它不会帮你释放动态内存。
析构函数的调用顺序和构造顺序相反——先构造的后析构。这个规则在继承和多态场景下尤其重要。
五、知识体系总览
下面这张图把四种函数的定义、调用时机、注意事项串起来了,建议保存下来当速查表:
六、避坑总结
| 函数类型 | 常见坑 | 我的建议 |
|---|---|---|
| 默认构造 | 基本类型成员未初始化 | 手动写默认构造,初始化所有成员 |
| 带参构造 | 参数名与成员名冲突 | 用初始化列表,或者加 this-> |
| 拷贝构造 | 浅拷贝导致 double free | 有指针成员必须深拷贝 |
| 析构函数 | 忘记释放资源 | new 和 delete 成对写,用 RAII 思想 |
嗯,这四种函数是 C++ 面向对象的基石。你想想看,一个类连构造和析构都写不好,后面谈什么继承、多态、模板?我个人建议:每写一个类,先把这四种函数想清楚——哪些需要自己写,哪些可以用默认的。养成这个习惯,能省下大量调试时间。
最后分享一个小技巧:C++11 以后可以用 = default 显式要求编译器生成默认版本,用 = delete 禁止某个函数。比如你想禁止拷贝,就写 Student(const Student&) = delete;。这比把拷贝构造声明为 private 更清晰。