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满天飞。