哈喽大家好,我是小康!
前言:构造函数和析构函数,真的有那么神秘?
初学 C++ 的朋友们可能都经历过这样的困惑:
“构造函数到底有什么用?析构函数又是什么操作?”
“为什么对象一创建就会调用构造函数,销毁时又要调用析构函数?”
“手写一个构造函数太简单,但大多数时候我觉得好像没啥用啊!”
别急!这篇文章就是为了解开你心中的迷雾,用 最简单的大白话,把构造函数和析构函数的秘密说透,顺便告诉你它们的 隐藏用法和坑。看完这篇文章,保你不再害怕面试官的灵魂拷问:“说说 C++ 的构造函数和析构函数的作用。”
微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。
一、什么是构造函数和析构函数?
构造函数:对象诞生的“接生员”
构造函数(Constructor)是 C++ 对象创建时由 编译器自动调用 的一个特殊函数。它的主要职责是:初始化对象,让对象一出生就拥有良好的“初始状态”。
它的特点:
- 名字和类名完全一样。
- 没有返回值(哪怕是
void
都不行)。 - 可以被重载(也就是说,一个类可以有多个构造函数,但参数不同)。
举个栗子🌰:
假如你在现实生活中领养了一只宠物,你肯定希望它出生后就有名字、有颜色、有健康值,这就是构造函数的作用。
来看看代码怎么写?
#include <iostream>
using namespace std;
class Pet {
public:
string name;
string color;
int health;
// 构造函数
Pet(string n, string c, int h) : name(n), color(c), health(h) {
cout << "宠物 " << name << " 出生啦!" << endl;
}
};
int main() {
Pet myPet("小白", "白色", 100);
cout << "宠物信息:" << myPet.name << ", " << myPet.color << ", 健康值:" << myPet.health << endl;
return 0;
}
运行结果:
宠物 小白 出生啦!
宠物信息:小白, 白色, 健康值:100
析构函数:对象“退场”的清道夫
析构函数(Destructor)则是对象销毁时 编译器自动调用 的特殊函数。它的主要职责是:清理资源,比如释放内存、关闭文件、归还连接池资源等等。
它的特点:
- 名字是类名前面加个波浪号
~
。 - 没有参数,也没有返回值。
- 一个类只能有一个析构函数,不能重载。
还是宠物的例子,当小白 寿终正寝时,我们就需要一个析构函数来负责清理它占用的资源。
class Pet {
public:
string name;
Pet(string n) : name(n) {
cout << "宠物 " << name << " 出生啦!" << endl;
}
~Pet() {
cout << "宠物 " << name << " 离开了这个世界……" << endl;
}
};
int main() {
Pet myPet("小白");
return 0;
}
运行结果:
宠物 小白 出生啦!
宠物 小白 离开了这个世界……
二、构造函数和析构函数,咋就这么重要?
1. 没有构造函数,麻烦事多了去了
想象一下,如果没有构造函数,你的对象初始化全靠手写:
Pet p;
p.name = "小白";
p.color = "白色";
p.health = 100;
这样写不仅繁琐,还容易漏掉某些属性,导致对象的状态不完整。
有了构造函数后:
Pet p("小白", "白色", 100); // 看起来多清晰啊!
是不是一键搞定?构造函数让对象从“出生”就带着完整状态!
2. 没有析构函数,资源管理会出问题
在 C++ 中,我们经常会用到动态内存分配(比如 new
),如果你忘记释放内存,就会发生内存泄漏。
有了析构函数后:
#include <iostream>
using namespace std;
class Pet {
private:
int* healthData; // 动态分配的资源
public:
Pet() {
healthData = new int[100];
cout << "宠物出生啦!分配了健康数据内存。" << endl;
}
~Pet() {
delete[] healthData;
cout << "宠物离开啦!释放了健康数据内存。" << endl;
}
};
int main() {
Pet myPet; // 构造函数分配内存
return 0; // 析构函数释放内存
}
运行结果:
宠物出生啦!分配了健康数据内存。
宠物离开啦!释放了健康数据内存。
总结: 析构函数是对象的“清洁工”,帮你收拾资源。它的作用是保证对象销毁时释放动态分配的资源,避免内存泄漏。
三、那些你必须知道的构造与析构细节
1. 构造函数的默认实现
如果你没有写构造函数,编译器会默认生成一个“无参构造函数”。这个默认构造函数啥都不干,只是“凑个数”,确保你的对象可以被创建。
但是,一旦你自己写了一个带参数的构造函数,编译器就不会再帮你生成默认的无参构造函数了。如果你还想要无参构造函数,就需要自己写。
例子:
#include <iostream>
using namespace std;
class Pet {
public:
string name;
// 带参数的构造函数
Pet(string n) : name(n) {
cout << "宠物 " << name << " 出生了!" << endl;
}
// 手动实现无参构造函数
Pet() {
cout << "无名宠物出生了!" << endl;
}
};
int main() {
Pet p1("小白"); // 调用带参数的构造函数
Pet p2; // 调用无参构造函数,如果类 Pet 中没有实现无参构造函数,这行会报错!
return 0;
}
2. 析构函数不能传参
析构函数是对象生命结束时自动调用的,没法让你手动传参数。所以 C++ 规定析构函数不能有参数,也没有返回值。
例子:
#include <iostream>
using namespace std;
class Pet {
public:
~Pet() {
cout << "宠物离开啦!" << endl;
}
};
int main() {
Pet myPet; // 构造函数调用
return 0; // 析构函数自动调用,无需传参
}
3. 构造函数和析构函数的调用顺序
- 构造函数调用顺序:从基类到派生类(“从老祖宗到小辈”)。
- 析构函数调用顺序:从派生类到基类(“从小辈到老祖宗”)。
例子:
#include <iostream>
using namespace std;
class Animal {
public:
Animal() {
cout << "动物出生了!" << endl;
}
~Animal() {
cout << "动物离开了!" << endl;
}
};
class Pet : public Animal {
public:
Pet() {
cout << "宠物出生了!" << endl;
}
~Pet() {
cout << "宠物离开了!" << endl;
}
};
int main() {
Pet myPet; // 创建对象,构造函数按顺序调用
return 0; // 对象销毁,析构函数按逆序调用
}
输出:
动物出生了!
宠物出生了!
宠物离开了!
动物离开了!
4. 为什么要用初始化列表
在 C++ 中,如果你的类有 const
或引用类型的成员变量,这些成员变量必须在对象创建时初始化,而不能在构造函数内部赋值。这时候,初始化列表就派上用场了。
例子:
#include <iostream>
using namespace std;
class Pet {
private:
const int id; // 宠物的编号,常量
int& health; // 宠物的健康值,引用
public:
// 使用初始化列表初始化 const 和引用
Pet(int i, int& h) : id(i), health(h) {
cout << "宠物编号 " << id << " 出生啦,健康值:" << health << endl;
}
};
int main() {
int health = 100;
Pet myPet(1, health); // 初始化编号和健康值
return 0;
}
输出:
宠物编号 1 出生啦,健康值:100
总结: 初始化列表不仅能简化代码,还能提高效率,尤其是对于 const
和引用变量来说,是唯一的初始化方式。
5. 析构函数与动态内存管理
如果类中使用了动态内存分配,析构函数必须负责释放这些资源。否则,程序会发生 内存泄漏。
什么是内存泄漏? 内存泄漏就是你申请了内存但忘记释放,导致这些内存永远占着不用,最后程序越来越卡甚至崩溃。
例子:
#include <iostream>
using namespace std;
class Pet {
private:
int* healthHistory; // 动态分配的健康记录
public:
Pet() {
healthHistory = new int[100]; // 分配内存
cout << "宠物出生啦!分配了健康记录的内存!" << endl;
}
~Pet() {
delete[] healthHistory; // 释放内存
cout << "宠物离开啦!释放了健康记录的内存!" << endl;
}
};
int main() {
Pet myPet; // 创建宠物对象
return 0; // 自动调用析构函数,释放资源
}
输出:
宠物出生啦!分配了健康记录的内存!
宠物离开啦!释放了健康记录的内存!
总结:构造与析构的细节要牢记
- 默认构造函数会被覆盖,记得显式声明无参构造函数。
- 析构函数不能传参,自动调用无需干预。
- 构造函数从基类到派生类,析构函数反过来。
- 初始化列表是
const
和引用成员变量的好帮手。 - 动态分配内存一定要在析构函数里释放,别让内存泄漏成为你的“宠物负担”!
掌握这些细节,你会发现构造和析构的世界不再神秘!
微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。
四、常见问题与注意事项
学习 C++ 的构造函数和析构函数,难免会遇到一些让人挠头的问题。这里用几条通俗易懂的解释+例子,手把手带你避开坑。读完,你就能轻松应对那些常见的“坑爹场景”了!
1. 构造函数能不能调用析构函数?
答案:绝对不能!
为什么?
构造函数是让对象“出生”,析构函数是让对象“离开”。你能想象一个小孩刚出生,立刻安排“谢幕”吗?这种操作会让程序直接崩溃!
错误例子:
#include <iostream>
using namespace std;
class Pet {
public:
Pet() {
cout << "宠物出生啦!" << endl;
this->~Pet(); // 这里调用了析构函数,错误!
}
~Pet() {
cout << "宠物离开啦!" << endl;
}
};
int main() {
Pet myPet; // 构造函数和析构函数冲突
return 0;
}
运行结果:
宠物出生啦!
宠物离开啦!
宠物离开啦!(程序崩溃)
记住:构造函数负责“出生”,析构函数负责“离开”,两者不要乱插队,默认的顺序最好别动!
2. 析构函数能不能抛出异常?
答案:万万不能!
为什么?
析构函数是对象“谢幕”的时候自动执行的。如果这个阶段抛出异常,程序就可能炸掉,尤其是在异常处理过程中,再抛出一个异常,系统直接“懵了”。
错误例子:
#include <iostream>
#include <stdexcept>
using namespace std;
class Pet {
public:
~Pet() {
throw runtime_error("析构函数里出错了!"); // 不要这样写!
}
};
int main() {
try {
Pet myPet; // 创建对象
} catch (const exception& e) {
cout << "捕获到异常:" << e.what() << endl;
}
return 0;
}
结果:程序行为不可预测,甚至崩溃!
正确做法: 析构函数内部捕获异常,自行处理:
#include <iostream>
using namespace std;
class Pet {
public:
~Pet() {
try {
throw runtime_error("错误发生!");
} catch (...) {
cout << "析构函数内部处理了异常。" << endl;
}
}
};
int main() {
Pet myPet; // 析构函数会捕获异常
return 0;
}
运行结果:
析构函数内部处理了异常。
记住:析构函数要低调点,能自己处理的事情就别给外界添麻烦。
3. 对象数组初始化需要注意什么?
当你用对象数组时,编译器会默认调用无参构造函数来初始化每个数组元素。如果你的类只有带参数的构造函数,编译器会很迷惑:“我该用什么参数来初始化这些对象?”
如果你没写无参构造函数,编译器就只能罢工,报个错给你看。
有无参构造函数时,一切正常
例子:
#include <iostream>
using namespace std;
class Pet {
public:
Pet() {
cout << "无名宠物出生了!" << endl;
}
};
int main() {
Pet pets[3]; // 每个数组元素调用无参构造函数
return 0;
}
运行结果:
无名宠物出生了!
无名宠物出生了!
无名宠物出生了!
编译器轻松搞定,每个数组元素用无参构造函数初始化。
只有带参数构造函数时,编译器发愁
例子:
#include <iostream>
using namespace std;
class Pet {
public:
Pet(string name) {
cout << "宠物 " << name << " 出生了!" << endl;
}
};
int main() {
Pet pets[3]; // 错误!没有无参构造函数
return 0;
}
错误提示:
error: no matching function for call to ‘Pet::Pet()’
问题: 编译器默认需要无参构造函数来初始化数组,但你的类里没有,怎么办?
解决办法:显式初始化数组
你可以显式告诉编译器每个数组元素该怎么初始化:
例子:
#include <iostream>
using namespace std;
class Pet {
public:
Pet(string name) {
cout << "宠物 " << name << " 出生了!" << endl;
}
};
int main() {
Pet pets[3] = {Pet("小白"), Pet("小黑"), Pet("小黄")}; // 显式初始化
return 0;
}
运行结果:
宠物 小白 出生了!
宠物 小黑 出生了!
宠物 小黄 出生了!
小结一下:
- 对象数组初始化时,编译器默认调用无参构造函数。
- 如果类中没有无参构造函数,编译器会报错,无法完成数组初始化。
- 遇到这种情况,可以用显式初始化方式,为数组的每个元素指定参数。
总之:编译器不会帮你猜参数,得你自己明确告诉它!
4. 析构函数为什么不能重载?
析构函数的名字是固定的 ~ClassName()
,没有参数,也没有返回值。这是因为析构函数是系统自动调用的,不需要你手动去管。所以,重载完全没有意义。
记住:析构函数干的就是单纯的“善后工作”,设计的初衷就是不需要外界干预,所以不用也不能重载。
5. 为什么构造函数不能是虚函数?
简单理解: 构造函数负责初始化对象,而虚函数是为了实现多态。问题是,多态依赖于对象的类型信息(即虚函数表指针),但对象在构造阶段还没完全初始化完成 ,类型信息也就不完整,所以谈不上多态。
再打个比方: 构造函数好比“新生儿的出生登记表”,它负责给对象安上属性和身份。如果你连“新生儿是谁”都没确认,就要搞“成人身份证”(虚函数表)去干复杂的多态事情,这显然是不可能的。
总之: 构造函数的职责是“出生”,虚函数的职责是“多态”,两者分工明确,互不干涉。构造函数的目标是让对象能正确初始化,等初始化完成后再谈多态问题。
6. 什么情况下构造函数和析构函数必须手动写?
类中有动态内存分配时
动态分配的资源要在析构函数中释放,否则就会内存泄漏。
例子:
#include <iostream>
using namespace std;
class Pet {
private:
int* healthHistory; // 动态分配资源
public:
Pet() {
healthHistory = new int[100]; // 分配内存
cout << "宠物出生啦!分配了健康记录。" << endl;
}
~Pet() {
delete[] healthHistory; // 释放内存
cout << "宠物离开啦!健康记录被释放了。" << endl;
}
};
int main() {
Pet myPet; // 构造分配,析构释放
return 0;
}
输出:
宠物出生啦!分配了健康记录。
宠物离开啦!健康记录被释放了。
类中有 const 或 引用 成员时
这些成员变量必须在对象创建时初始化,必须用构造函数的初始化列表。
例子:
#include <iostream>
using namespace std;
class Pet {
private:
const int id; // 宠物的编号,常量
int& health; // 宠物的健康值,引用
public:
Pet(int i, int& h) : id(i), health(h) {
cout << "宠物编号 " << id << " 出生啦,健康值:" << health << endl;
}
};
int main() {
int health = 100;
Pet myPet(1, health);
return 0;
}
输出:
宠物编号 1 出生啦,健康值:100
总结
- 构造函数不能乱调析构函数,职责分清楚。
- 析构函数不要抛异常,自己处理自己的问题。
- 对象数组初始化时,必须注意构造函数的类型。
- 析构函数不能重载,构造函数不能是虚函数,各干各的活。
- 动态内存分配要记得用析构函数释放,不然程序哭给你看!
const
和引用成员变量,用初始化列表才能搞定。
读到这里,你是不是觉得构造函数和析构函数其实没那么难?只要注意上面的这些坑,写起来就稳了!
五、总结:构造函数和析构函数,这些点你要记住!
1.构造函数是干啥的?
对象一出生,构造函数帮它“准备好一切”,比如初始化属性,让它能正常用。
2.析构函数是干啥的?
对象“退场”时,析构函数帮忙“收拾残局”,比如释放内存、关闭文件,不留麻烦。
3.它俩的调用顺序?
- 构造: 先基类,再派生类。
- 析构: 先派生类,再基类。
4.动态内存要小心!
如果类里面用了new
分配内存,记得用析构函数里的delete
释放,不然会出现“内存泄漏”。
一句话记住: 构造函数管“出生”,析构函数管“善后”,俩人搭配,才能保证对象“活得好,走得清”。
如果你觉得这篇文章简单好懂,别忘了点赞、收藏、关注 或者留言聊聊你的想法!下一篇,我们聊聊 C++ 的 “拷贝构造函数” 和它带来的那些“深浅问题”,准备好再升级一波你的 C++ 技能吧!
也欢迎大家来关注我公众号 「跟着小康学编程」,这里会持续分享计算机编程硬核技术文章!
怎么关注我的公众号?
微信搜索 【跟着小康学编程】,关注我,定期分享计算机编程硬核技术文章。
另外,小康还建了一个技术交流群,专门聊技术、答疑解惑。如果你在读文章时碰到不懂的地方,随时欢迎来群里提问!我会尽力帮大家解答,群里还有不少技术大佬在线支援,咱们一起学习进步,互相成长!
想找我?加我微信即可,微信号:jkfwdkf ,备注 「加群」