[C++类的默认成员函数——lesson5.构造函数析构函数]
目录
一、前言
为什么会出现构造函数和析构函数
构造函数
📖构造函数的概念
📖构造函数特性
1. 函数名与类名完全相同
2. 无返回值(无需写void或其他类型)
3:对象实例化时编译器自动调用对应的构造函数
4:构造函数支持重载
5:如果类中没有显式定义构造函数(),则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(避免歧义)
补充:内置类型初始化的解决方案(C++11 特性)
析构函数
📖析构函数概念
析构函数特性
前言
在我们前面学习的类中,我们会定义成员变量和成员函数,这些我们自己定义的函数都是普通的成员函数,❓但是如若我们定义的类里什么也没有呢?是真的里面啥也没吗?如下:
class Date {};
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成6个默认成员函数。
【默认成员函数概念】:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
其中两个默认成员函数是用来初始化和清理的分别为:构造函数、析构函数
⭐ 本次博客将详解为什么会出现这两个函数,这两个函数将如何使用 的问题⭐
1.为什么会出现构造函数和析构函数
首先看下面这段 C语言代码:
typedef struct Date {int year;int month;int day; }D;void Init(D* date) {date->year = 2023;date->month = 10;date->day = 21; } void Printf(D* date) {cout << date->year << "-" << date->month << "-" << date->day << endl << endl; } void Destory(D* date) {date->year = 0;date->month = 0;date->day = 0; } int main() {D date;Init(&date);Printf(&date);Destory(&date);return 0; }
⚠ 注意:大家在日常写代码和刷题的时候,肯定会有过忘记初始化,或者忘记销毁,这些小细节很容易被大家忽略,但是出现在代码中,就会出现报错,导致我们写代码的时候,就很烦。
⚠ 忘记写初始化:输出随机值,结果会错误
⚠ 忘记写销毁:时间久了便会造成【内存泄漏】
💦你是否发现若是我们要去使用一个Date的话,通常不会忘了去往里面入数据或者是出数据,但是却时常会忘了【初始化】和【销毁】。这要如何是好呢🧐
🔑 解决方案:
1️⃣:在上一文的学习中,我们学习到了一个类中的一个东西叫做this指针,只要是在成员函数内部都可以进行调用。而且还知晓了C++中原来是使用this指针接受调用对象地址的机制来减少对象地址的传入,减轻了调用者的工作。这也是C++区别于C很大的一点
2️⃣:那C++中是否还有东西能够替代【初始化】和【销毁】这两个工作呢?答案是有的,就是我们接下来要学习的【构造函数】和【析构函数】
2构造函数
2.1📖构造函数的概念
如下的日期类:
class Date { public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << "今日日期输出:" << endl;cout << _year << "-" << _month << "-" << _day << endl;} private:int _year;int _month;int _day; }; int main() {Date d1;d1.Init(2023, 10, 21);d1.Print();return 0; }
运行效果:
正常情况下,我们写的这个日期类,首先初始化,其次打印。❓但如果说你突然忘记初始化了,直接就开始访问会怎么样呢?
- 从运行结果上看,没初始化直接访问输出的是随机值。 忘记初始化其实是一件很正常的事情,C++大佬在这一方面为了填补C语言的坑(必须得手动初始化)。因而就设计出了构造函数。
- 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。其目的就是为了方便我们不需要再初始化。
2.2📖构造函数特性
构造函数是特殊的成员函数,但需要注意的是,构造函数虽名为 “构造”,但❓核心任务并非 “开空间创建对象”(对象的内存空间在实例化时由编译器自动分配),而是对已创建的对象进行初始化(为成员变量赋予合理初始值),避免未初始化导致的随机值问题。
构造函数的 6 大特性如下:
1. 函数名与类名完全相同
2. 无返回值(无需写
void
或其他类型)3. 对象实例化时编译器自动调用
4. 支持函数重载(可定义多个不同参数的构造函数)
5. 编译器默认生成规则(用户未显式定义时)
6. 默认构造函数的唯一性(避免歧义)
如下即为构造函数:
Date() {_year = 1;_month = 1;_day = 1; }
1函数名与类名完全相同
- 例如
Date
类的构造函数名必须为Date
,而非DateInit
等其他名称:class Date { public:// 构造函数:名称与类名一致Date() { _year = 1; _month = 1; _day = 1; } private:int _year, _month, _day; };
2无返回值(无需写void
或其他类型)
- 构造函数不允许指定返回值,也无需返回任何内容,即使是
void
也不能写,示例同上。
3:对象实例化时编译器自动调用对应的构造函数
- 无需手动调用,创建对象时编译器会 “自动触发” 构造函数完成初始化。
- 文档调试示例:实例化
Date d1
时,调试窗口显示_year=1, _month=1, _day=1
,证明构造函数已自动执行。
4:构造函数支持重载
允许通过不同参数列表实现 “默认初始化”“指定值初始化” 等多种逻辑,例如:
class Date { public:// 无参构造(默认初始化)Date() { _year = 1; _month = 1; _day = 1; }// 带参构造(指定值初始化)Date(int year, int month, int day) { _year = year; _month = month; _day = day; } }; // 调用时根据参数自动匹配 Date d1; // 调用无参构造 Date d2(2023,10,21); // 调用带参构造
来输出和我们先前的构造函数对比看看:
- 注意:没有参数时我在调用的时候不能加上括号(),切忌!!构造函数尤为特殊
- 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
无参的情况下必须要像我们刚开始实例化的d1那样:
Date d1; d1.Print();
谈到产生歧义这一块,再来给读者扩展一个知识点,既然讲到了【函数重载】,就来顺便说说【缺省参数】吧
- 仔细观察着两个构造函数,是否可以将它们做一个整合呢,就用到我们前面所学的缺省参数
//Date() //{ // _year = 1; // _month = 1; // _day = 1; //}//Date(int y, int m, int d) //{ // _year = y; // _month = m; // _day = d; //} Date(int y = 1, int m = 1, int d = 1) {_year = y;_month = m;_day = d; }
- 那这样我们传参的时候就很灵活了,若是不传对应的参数就按照缺省值来进行初始化
- 但是不可以像下面这样,【默认无参构造】和【全缺省有参构造】是不可以同时存在的,会产生歧义。
- 因为在没有给到初始值的时候编译器不知道使用哪个作为默认的初始值,所以【存在多个默认构造函数】
- 构造函数的重载我们推荐写成全缺省的样子:
所以为了不产生这样的歧义,一般这两个构造函数不会同时存在,我们会选择下面这种全缺省的构造函数,可以代替无参的构造
//普通的构造函数Date(){_year = 1;_month = 1;_day = 1;} //全缺省的构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
首先,普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:
class Date { public: //普通的构造函数Date(){_year = 1;_month = 1;_day = 1;} //全缺省的构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;} private:int _year;int _month;int _day; }; int main() {Date d1;d1.Print(); }
此时我实例化的d1到底是调用普通的构造函数?还是调用全缺省的构造函数?并且此段代码编译出现错误。何况我在没有调用函数的情况下编译是没错的。
🔑 由此可见:它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,我们更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。
5:如果类中没有显式定义构造函数(),则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
看如下代码,自己不去写构造函数,使用编译器默认的构造函数:
class Date { public:// 我们不写,编译器会生成一个默认无参构造函数/*Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;} private:int _year;int _month;int _day; }; int main() {// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数Date d;d.Print(); }
不是说好我不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这难道也算初始化?别急,搞清楚这个得先明白默认构造函数:
⚠ 默认构造函数:
- 1. 我们不写编译器默认生成的那个构造函数,叫默认构造
- 2. 无参构造函数也可以叫默认函数
- 3. 全缺省也可以叫默认构造
总结: 可以不传参数就调用构造,都可以叫默认构造
⚠ C++把变量分成两种:
1️⃣:内置类型/基本类型:int、char、double、指针……
2️⃣:自定义类型:class、struct去定义的类型对象C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量)
让我们来看看自定义类型是如何处理的。class A { public:A(){cout << "A()" << endl;_a = 1;} private:int _a; };
首先,这是一个名为A的类,有成员变量_a,并且还有一个无参的构造函数,对_a初始化为1。接着:
class Date { public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;cout << endl;} private:int _year = 2023;int _month = 11;int _day = 11;A _aa; };int main() {Date d1;d1.Print();return 0; }
通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。
什么时候使用默认构造函数会凸显出其价值呢?就比如我们之前写的括号匹配这道题:
class Stack { public:Stack(){_a = nullptr;_top = _capacity;} private:int* _a;int _top;int _capacity;};class MyQueue { public://默认生成的构造函数就可以用了void push(int x){}int pop() {} private:Stack _S1;Stack _s2; };
此时我队列里自定义类型_s1和_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。
🔑总结:
如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(避免歧义)
🔑 默认构造函数:
- 1. 我们不写编译器默认生成的那个构造函数,叫默认构造
- 2. 无参构造函数也可以叫默认函数
- 3. 全缺省也可以叫默认构造
总结: 可以不传参数就调用构造,都可以叫默认构造
- 🔑既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:
class Date { public:// 我们不写,编译器会生成一个默认无参构造函数/*Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;} private:// C++11 打的补丁,针对编译器自己默认成员函数不初始化问题int _year = 2023;int _month = 10;int _day = 21; }; int main() {// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数Date d;d.Print(); }
7.补充:内置类型初始化的解决方案(C++11 特性)
针对编译器默认构造 “不初始化内置类型” 的缺陷,C++11 允许在成员变量声明时直接赋予默认值,即使使用编译器生成的默认构造,内置类型也会被初始化
class Date {
private:// C++11补丁:内置类型声明时指定默认值int _year = 2023; int _month = 10;int _day = 21;
};
// 即使不写构造函数,实例化后_year=2023(非随机值)
Date d;
综上,构造函数的核心价值是 “自动化初始化”,避免 C 语言中手动调用
Init
函数的繁琐与遗忘风险;使用时需重点关注 “默认构造的唯一性” 和 “内置类型初始化” 问题,推荐优先显式定义全缺省构造。
总结:
👍构造函数使用建议:
- 尽量自己写:默认生成的构造函数对内置类型不初始化(随机值),自定义类型才调用默认构造
- 二选一原则:无参构造和全缺省构造不能同时存在,会引发调用歧义
- 推荐写法:用全缺省构造函数,既灵活又避免歧义
- C++11 补丁:内置类型可在声明时直接赋值,即使使用默认构造也能避免随机值
3.析构函数
3.1📖析构函数概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
3.2析构函数特性
- 函数名称:析构函数名是在类名前加 “~” 构成,如类名为
Student
,则其析构函数名为~Student()
。- 返回类型:析构函数不指定返回类型,也没有返回值,甚至不能是
void
类型。- 参数情况:析构函数没有形参,因此不能被重载,一个类只能有一个析构函数。
- 调用时机:在撤销对象前由系统自动调用,比如对象所在函数调用完毕、使用
delete
操作符释放动态分配的对象、对象作为容器元素被移除或程序结束时(针对全局对象和静态对象)等情况,析构函数都会被调用。- 主要作用:用于释放对象在生命周期内所占用的资源,如动态分配的内存、打开的文件句柄、网络连接等,还可以执行一些清理操作,如重置对象的成员变量,确保对象在销毁后不会留下未处理的状态或残留数据,防止资源泄漏。
- 默认析构函数:如果没有显式定义析构函数,编译器将生成一个公有的缺省析构函数,其函数体为空,仅执行一些基本的清理操作,如删除成员变量,但无法清理动态分配的资源。
我们实际写一个析构函数看看:
~Date() {cout << "~Date()" << endl; }
带入示例再看看:
class Date { public:Date(int year = 2023, int month = 10, int day = 21){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}~Date(){cout << "~Date()" << endl;} private:int _year;int _month;int _day; }; int main() {Date d1;d1.Print();return 0; }
首先,我实例化出的d1会调用它的默认构造函数进行初始化,其次,出了作用域后又调用其析构函数,这也就是为什么输出结果会是~Date()
析构的目的是为了完成资源清理,什么样的才能算是资源清理呢?像我这里定义的年月日变量就不需要资源清理,因为出了函数栈帧就销毁,真正需要清理的是malloc、new、fopen这些的,就比如清理栈里malloc出的
class Stack { public://构造函数Stack(int capacity = 10){_a = (int*)malloc(sizeof(int) * capacity);assert(_a);_top = 0;_capacity = capacity;}//析构函数~Stack(){free(_a);_a = nullptr;_top = _capacity = 0;} private:int* _a;int _top;int _capacity; }; int main() {Stack st; }
这里不难感慨C++的构造函数就像先前C语言常写的Init,而析构函数就像Destroy
- 看如下的题目:现在我用类实例化出st1和st2两个对象,首先,st1肯定先构造,st2肯定后构造,这点毋庸置疑,那关键是谁先析构呢?
int main() {Stack st1;Stack st2; }
答案:st2先析构,st1后析构
解析:这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。
若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下:
class Stack { public://构造函数Stack(int capacity = 10){_a = (int*)malloc(sizeof(int) * capacity);assert(_a);_top = 0;_capacity = capacity;}//析构函数~Stack(){cout << "~Stack():" << this << endl;free(_a);_a = nullptr;_top = _capacity = 0;} private:int* _a;int _top;int _capacity;}; class MyQueue { public://默认生成的构造函数可以用//默认生成的析构函数也可以用void push(int x){}int pop(){} private:Stack _S1;Stack _s2; }; int main() {MyQueue q; }
对于MyQueue而言,我们不需要写它的默认构造函数,因为编译器对于自定义类型成员(_S1和_S2)会去调用它的默认构造,Stack提供了默认构造,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()……
🔑总结:
- 对于未申请资源的类,可使用默认析构函数。当类中没有申请诸如动态内存、文件句柄等资源时,编译器生成的默认析构函数就足够了。如 Date 类,通常只包含一些基本数据成员(如年、月、日),没有额外资源需要释放,无需自定义析构函数,默认析构函数可完成对象销毁时的相关操作。
- 对于申请了资源的类,必须自定义析构函数。若类在使用过程中申请了资源,如 Stack 类可能会在内部动态分配内存来存储元素,此时就需要自定义析构函数,以便在对象生命周期结束时释放这些资源。否则,会导致资源泄漏,程序运行效率下降,甚至出现内存不足等问题。
结束语
以上是我对C++类的默认成员函数--------构造函数&&析构函数的理解
感谢你的三连支持!!!