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


前言

一、默认成员函数

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

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 未给参数,所以使用默认构造的参数4
	Stack 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类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

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

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

在这里插入图片描述

  • 94
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 122
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 122
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值