【C++的奇妙冒险】构造函数和析构函数(初始化和清理)

希 腊 奶 2024-06-17 08:35:03 阅读 58

文章目录

前言一、默认成员函数二、构造函数构造函数的概念构造函数的特性默认构造函数 三、析构函数析构函数的概念析构函数的特性析构函数的先后析构问题为什么要有析构函数?析构函数的特性检验


前言

一、默认成员函数

如果一个类中什么成员都没有,简称为空类。

class Date { };

空类中真的什么都没有吗?nonono~,并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

在这里插入图片描述

如上图所示,类共有6个默认成员函数,咱们逐一攻破,先Follow me拿下前两个 构造函数和析构函数,小细节很多哦~瞪大眼睛嗷👀。

首先,对于默认成员函数,如果我们不写,编译器是会自己生成一份滴。(谁家的编译器这么良心 ❤️!)

这里有老铁就要问了,哎哥们儿~哥们儿,那它们都有啥作用啊?哼哼哼,不妨告诉你嗷,它们可有用了,就比如:栈(Stack)哥们儿是不是要自己写个初始化函数(Init)和销毁函数(Destroy),有些粗心的老铁就非常容易忘记,要么忘了写初始,上来就push,要么最后忘了销毁,导致内存泄漏,对于初始化和清理,构造函数析构函数就可以帮助我们完成,是的构造函数就像Init,析构函数就像Destroy。下面话不多说,先从构造函数下手。👻

二、构造函数

先看一个普通的日期类

class Date{ public:void GetDate(int year, int month, int day){ _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.GetDate(2024, 5, 19);d1.Print();Date d2;d2.GetDate(2024, 5, 20);d2.Print();return 0;}

在Date类中,我们可以新建对象,并通过使用自己写的GetDate来给对象内的内容进行更改,可是每次设置都要去调用GetDate,拜托~这样子真的超逊的!那有没有更好的办法呢?哎!你还别说,你还真别说,我还真有一计,接下来有请重量级人物“构造函数”闪亮登场!✨️✨️✨️

构造函数的概念

✨️✨️✨️首先,构造函数是一个特殊的成员函数

名字与类名相同创建类类型对象时由编译器自动调用能保证每个数据成员都有一个合适的初始值,并在对象的生命周期内只调用一次。

等等🫸🏼,需要留意的是,构造构造,虽然名字叫构造但它的作用是初始化,并不会开辟空间。

构造函数的特性

构造函数的函数名和类是相同的构造函数无返回值构造函数支持重载会在对象实例化的时候自动调用对象定义出来

😆厉害吧!居然自动调用(o゜▽゜)o☆

代码演示👇

class Date{ public: //无参的构造函数 Date() { _year = 0; _month = 1; _day = 2; } //带参的构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; }private: int _year; int _month; int _day;};int main(){ //Date d1()//错的 Date d1;//对象实例化,此时触发构造,调用无参构造 d1.Print(); Date d2(2024, 5, 17);//对象实例化,此时触发构造,调用带参构造函数 d2.Print(); return 0;}

运行结果

在这里插入图片描述

看到咩,你给参数它就调用带参构造函数,你不给它就调用无参的

是不是看着非常之简单,no no no!

在这里插入图片描述

事情并没有你想象的那么简单,需要注意的可多了

因为构造函数是特殊成员函数,人家也说了,人家是特殊的,所以它肯定不是常规的成员函数,肯定不能像其它函数那样直接调用,比如:d1.Dat(),那你只会喜提编译器的红色波浪号。

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 d1;d1.Date(); return 0;}

报错结果

在这里插入图片描述

那咋办呀😭别急继续往下看😢

如果通过无参构造函数创建对象,对象后面不用跟括号,不然就变成了函数声明!

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 d1();//错啦!调用无参要把后面的括号去掉 Date d1;像这样Date d2(2024, 5, 19);d1.Print();d2.Print(); return 0;}

报错结果

在这里插入图片描述

⚠⚠⚠ 记住了昂,调用无参构造函数不需要加括号!⚠⚠⚠

如果你写的不是缺省函数的话,调用带参构造函数时,需要传递三个参数,而且这三个参数的位置从左往右一一对应的。

在这里插入图片描述

当然咯 少一个也不行,不可以对自己的程序太偏心,给了year和month唯独不给day,小心被说缺心眼🙃

在这里插入图片描述

如果你自己没有手动去写构造函数,编译器会自动生成一个无参的默认构造函数的,若是你自己写了,编译器自然就不会再多此一举了,不会生成了。

class Date{ public: void Print() { cout << _year << _month << _day << endl; }private: int _year; int _month; int _day;};int main(){ Date d1;//无参数 d1.Print(); return 0;}

编译结果

在这里插入图片描述

在这里插入图片描述

这是随机值?至于为什么是随机值我们先不理他,待会儿再来解释,先继续往下看。

默认构造函数

无参构造函数、全缺省构造函数、自动生成的构造函数都被称为 默认构造函数(不传参就可以调的构造函注意:默认构造函数只能有一个。

在这里插入图片描述

语法上来讲 无参的和全缺省的是可以同时存在的,但会存在歧义,编译器也是有选择困难症的,鱼和熊掌不可兼得,所以编译器表示我到底该选谁?

在这里插入图片描述

这里就会有人问了,兄嘚那我呢?俺也有选择困难,别问,问就是无敌的全缺省,反正你写全缺省就完事了,超级无敌好用! 一句话来说就是进可攻退可守,看好嗷~让全缺省给你露一手 🫱🏼

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 d1;Date d2(2024);Date d3(2024, 5);Date d4(2024, 5, 20);d1.Print();d2.Print();d3.Print();d4.Print();return 0;}

在这里插入图片描述

通过以上知识点我们可以知道任何一个类的默认构造函数 只有三种:

无参构造函数全缺省构造函数编译器自己生成的构造函数(我们自己不写)

我们现在回到刚刚的④ 假设我不写构造函数,让编译器帮我们写。

class Date{ public://我可没写构造函数昂 void Print() { cout << _year << _month << _day << endl; }private: int _year; int _month; int _day;};int main(){ Date d1;//无参数 d1.Print(); return 0;}

编译结果

在这里插入图片描述

由上可见,我并没有写构造函数(类中未显示定义),编译器自动生成构造函数

看似编译器默认生成的构造函数也No咋滴啊,净是些没用的随机值…→_→。

d1调用了编译器生成的默认构造函数,但d1对象内的成员变量依旧是随机值。

好了丢人了,编译器默认生成的默认构造好像真就No咋滴

救救救>_<!因为:C++把类型分成了内置类型和自定义类型。

内置类型就是 int/char/double/指针啊这些语法定义好的类型自定义类型就是 class/struct自己定义的类型咯

咱们接着往下看哈,看下面这串代码,你会发现编译器生成的默认构造函数,会对自定义类型成员函数_t 调用它的默认成员函数(纳尼?套娃0.o?)

class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; }private: int _hour; int _minute; int _second;};class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t;};int main(){ Date d; return 0;}

编译结果

在这里插入图片描述

再来一个,证明:对自定义类型处理,会调用默认构造函数(不用参数就可以调用的函数)

#include<iostream>using namespace std;class A { public:// 默认构造函数(不用参数就可以调的)A() { cout << " A() " << endl;_a = 0;}private:int _a;};class Date { public://空~private:int _year;int _month;int _day;A _aa; // 对自定义类型处理,此时会调用默认构造函数 A() {...}};int main(void){ Date d1;return 0;}

在这里插入图片描述

自定义类型的尽头还是内置类型0.0

在这里插入图片描述

C++早就表明了,只要我们不写,编译器生成的默认构造函数对内置类型的成员变量不做处理。

但是啊,对于自定义类型的成员变量会去调用它的默认构造函数去处理,初始化。

没有默认构造函数一样会报错,即不用参数就可以调用的构造函数。

你要么就不要带参,要么就别写,你不写我编译器还会默认生成呢

class A { public:// 如果没有默认的构造函数,会报错。A(int a) //故意写的 带参{ cout << " A() " << endl;_a = 0;}private:int _a;};class Date{ public:private:// 如果没有默认构造函数就会报错int _year;int _month;int _day;A _aa;};int main(void){ Date d1;return 0;}

报错结果

在这里插入图片描述

那我不写,再看看编译器什么反应

class A { public:private:int _a;};class Date{ public:private:int _year;int _month;int _day;A _aa;};int main(void){ Date d1;return 0;}

在这里插入图片描述

在这里插入图片描述

错不了了,虽然他依旧还是随机值,但也确实证明了它只对自定义类型做处理了,内置类型不处理,所以是随机值。

构造函数到这里就直接拿下了。

在这里插入图片描述

三、析构函数

析构函数的概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

好家伙Σ(⊙▽⊙"a,自动清理,回想起之前学过的数据结构,老是要写个什么Destroy,主要是容易忘记,不过现在好了,自从有了“析构函数”,那些担心都是多余的了🐼🐼🐼

我们知道构造函数是特殊成员函数,作用是初始化而不是开辟空间

那么析构函数也一样,任务是清理,而不是销毁对象

析构函数的特性

析构函数是特殊的成员函数,其特征如下:

析构函数名是在类名前加上字符 ~。无参数无返回值类型。一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。对象生命周期结束时,C++编译系统系统自动调用析构函数。

🚩代码演示:

class Date{ public:Date(int year, int month, int day){ _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(2024, 5, 21);d1.Print();return 0;}

运行结果:

在这里插入图片描述

由运行结果可以看出,~Date函数确实被调用了。

接下来我们再来看:

析构函数的先后析构问题

依旧是这串代码,创建了对象d1和d2

🚩思考,程序结束先析构d1?还是先析构d2?!

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;}~Date(){ cout << "~Date()" << endl;}private:int _year;int _month;int _day;};int main(){ Date d1;Date d2;return 0;}

解答:先析构d2,再析构d1

运行结果:

在这里插入图片描述

在这里插入图片描述

这是什么原理,张力知道吧…咳咳咳,其实原理就像栈一样,先定义的先构造,后定义的后构造,后定义的先析构,先定义的后析构,对!满足类似先进后出的顺序。

在这里插入图片描述

那么接下来我们再深入探讨一下析构的顺序问题

看几个例子:

🚩🚩

猜猜看d1,d2,d3谁先析构?

#include<iostream>using namespace std;class Date{ public:Date(int year=1, int month=1, int day=1){ _year = year;_month = month;_day = day;}void Print() { };~Date(){ cout << "~Date()->" << _year << endl;}private:int _year;int _month;int _day;};int main(){ Date d1(1);Date d2(2);static Date d3(3);return 0;}

在这里插入图片描述

想必各位大佬早已看透一切了吧,运行一下看看吧 O(∩_∩)O

运行结果:

在这里插入图片描述

解答:生命周期结束,先销毁main函数里面的局部变量,d1和d2,又因为后定义先析构,先析构d2,然后再是d1,最后销毁全局的d3.

🚩🚩

猜猜看谁先析构

看代码样例:

class Date{ public:Date(int year=1, int month=1, int day=1){ _year = year;_month = month;_day = day;}void Print() { };~Date(){ cout << "~Date()->" << _year << endl;}private:int _year;int _month;int _day;};void func(){ Date d3(3);static Date d4(4);}int main(){ Date d1(1);Date d2(2);func();return 0;}

那如果我再调用个func函数呢,再看看谁先析构?

编译结果:

在这里插入图片描述

解答:func函数结束了,d3先销毁全局的不销毁,然后main函数结束,d2先销毁,再销毁d1,最后销毁d4

🚩🚩

还没结束!继续!

在这里插入图片描述

class Date{ public:Date(int year=1, int month=1, int day=1){ ...};void Print() { ...};~Date(){ cout << "~Date()->" << _year << endl;}private:int _year;int _month;int _day;};void func(){ Date d3(3);static Date d4(4);}Date d5(5);static Date d6(6);int main(){ Date d1(1);Date d2(2);func();return 0;}

运行结果:

*

加粗样式

在这里插入图片描述

🏁🏁🏁总结顺序:局部对象(后定义的先析构)—》局部的静态—》 全局对象(后定义先析构)

为什么要有析构函数

“前面说了一大堆也没见得析构函数的价值啊”

在这里插入图片描述

我们知道,析构就类似于Destroy,我们还是那 栈(Stack)举例子吧,因为栈是需要Destroy,清理开辟的空间的。

让我们现在来看看,析构函数的真正魅力吧!

代码:

#include<iostream>using namespace std;typedef int STDateType;class Stack{ public://构造函数,使用缺省参数初始化容量,默认给4,相当于初始化Stack(int capacity=4){ _array = (STDateType*)malloc(sizeof(STDateType) * capacity);if (_array == NULL){ cout << "malloc fail" << endl;exit(-1);}_top = 0;_capacity = capacity;}void Push(STDateType Date){ _array[_top] = Date;_top++;}//析构函数,用来清理开辟的空间,防止内存泄漏 相当于Destroy~Stack(){ free(_array);_array = nullptr;_capacity = _top = 0;}//private:int* _array;int _top;int _capacity;};int main(){ Stack st1;//初始化st1的_capacity 未给参数,所以使用默认构造的参数4Stack st2(8);//初始化st2 给_capacity8个大小的容量return 0;}

☁️☁️☁️

解答:我们刚刚学了构造函数,上面一大串代码,定义容量_capacity的时候利用了缺省参数,默认给4个大小的空间容量,这样不传参默认给的就是4,如果不想传4可以像st2一样自己传。

有了构造函数就能保证我们定义栈的时候一定会被初始化,然后析构帮我们自动销毁,厉不厉害o( ̄▽ ̄)d,妈妈再也不用担心内存泄漏啦😄😄😄。

析构函数的特性检验

关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。

#include<iostream>using namespace std;class Time{ public:~Time(){ cout << "~Time()" << endl;}private:int _hour;int _minute;int _second;};class Date{ private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){ Date d;return 0;}

运行结果:

在这里插入图片描述

那么问题又来了

在这里插入图片描述

❓️❓️在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数

🥳解答: 因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在 d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数 中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁 main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数

注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

概括:

如果我们不自己写析构函数,让编译器自动生成,那么这个 默认析构函数:

对于 “内置类型” 的成员变量:不作处理对于 “自定义类型” 的成员变量:会调用它对应的析构函数

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。