C++《类和对象》(中)

在之前C++《类和对象》(上)中我们初步了解了类的相关概念,学习了类的定义和类的实例化等,在本篇中我们将进行学习类的相关知识,将会学习到类当中的6大默认成员函数,以及结合相关的知识实现一个日期类,接下来就开始本篇的学习吧!!!


1.类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。

一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

• 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。
• 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

 1.1构造函数

首先来了解构造函数的定义
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使用的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。

接下来就来了解构造函数是如何创建的,在此之前就要了解构造函数的相关概念
函数名与类名相同且
无返回值。

#include<iostream>;

using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_mouth = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}


private:
	int _year;
	int _mouth;
	int _day;
};

 在以上Date类中的Date函数就是构造函数,函数Date函数名和类名相同并且无返回值

类的构造函数其实是支持函数重载的,就例如以上Date类的构造函数就可以有多个,只要符合函数重载即可

#include<iostream>;

using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};

在此构造函数最重要的特点就是在对象实例化时系统会自动调用对应的构造函数,而不再需要像之前类中的Init函数需要我们自己去调用

就例如以上的Date类,在创建出Date类型的对象d1,d2时就系统就会自动调用构造函数Date

#include<iostream>;

using namespace std;
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}


private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2(2024, 8, 11);
	
	d1.Print();
	d2.Print();


	return 0;
}

注:当在类实例化时无参数时,创建的对象后不能加(),C++规定如此  

以上代码输出结果如下所示: 

 

在此在构造函数中还要了解一种函数叫做默认构造函数,那么默认构造函数的特点是什么呢?

无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函
数。但是这三个函数有且只有一个存在,不能同时存在。

在此无参构造函数和全缺省构造函数都很容易理解,最主要是编译器默认生成的构造函数在此之前我们都没有了解过,它的特点是什么样的呢?
如果类中没有显式定义构造函数(显示定义构造函数就是类当中我们定义的构造函数不是无参构造函数和全缺省构造函数),则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

注:无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。所以这两个函数不能同时存在 。要注意不要把默认构造函数就认为是编译器默认生成那个叫默认构造,这种理解是错误的,无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调
⽤的构造就叫默认构造。

我们不写,编译器默认生成的构造,对内置类型成员变量初始化是不确定的,初始化成什么样是看编译器的。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack pushst;
	MyQueue mq;
	return 0;
}

例如在以上代码中先创建类Stack,因为Stack内的成员变量都是内置类型,虽然我们在此不写默认构造编译器会自己生成但生成的不符合我们的要求因此Stack需要我们自己来实现默认构造。之后又创建了一个类MyQueue,在MyQueue内的成员变量为两个Stack类型的变量,因为Stack内有我们创建的默认构造函数,在此这两个变量为自定义类型成员变量所以在实例化出MyQueue类型的对象mq时就会调用Stack的的默认构造函数

在此若类Stack中的构造函数参数不为缺省参数,那么类Stack中就无默认构造函数,这时再实例化出MyQueue类型的对象mq时编译器就会出现报错

 

构造函数总的来说有以下的特征:
1. 函数名与类名相同。
2. 无返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,一旦用户显式定义编译器将不再生成。 
6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

 

1.2 析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,而是完成对象中的资源清理工作。比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

首先先来了解析构函数结构上的特征:
析构函数名是在类名前加上字符 ~同时跟构造类似无参数无返回值。

例如在以下的类Stack中在构造函数中使用malloc申请了n个大小为int的内存空间,所以类Stack中就需要创建析构函数~Stack来完成资源清理的工作

注意:与构造函数不同,一个类只能有一个析构函数。

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n=4 )
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

在析构函数中若我们显示的写析构函数那么是否和构造函数一样系统会自动调用默认析构函数吗?

确实是这样的当我们没有显示的写析构函数时系统会自己动生成默认的析构函数。并且在此跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。

因此在这些特点下,在以上类Stack中由于该类的成员变量都是内置类型这时如果没有自己写析构函数编译器对内置类型会不做处理,因为该类中的成员变脸是指针并且指向资源不进行清理就会造成内存泄漏

#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
	Stack(int n=4 )
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:
	~MyQueue()
	{

	}
	//编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	Stack st;
	MyQueue mq;
	return 0;
}

而像以上类MyQueue中成员变量都是自定义类型时但我们没有显示写析构函数时,会调用成员变量的析构函数,并且就算我们显示写了析构函数都会调用成员变量的析构函数,也就是说自定义类型成员无论什么情况都会自动调用析构函数。

对象生命周期结束时,系统会自动调用析构函数,并且若一个局部域的多个对象,C++规定后定义的先析构。

因此在以上代码中先定义Stack st后定义MyQueue mq,在整个main函数结束时会先调用mq的析构函数再调用st的析构函数

那么析构函数相比Destory有什么优点呢?来看以上示例

在算法题——20. 有效的括号 - 力扣(LeetCode)中通过以下代码对比一下用C++和C实现的Stack解决括号匹配问题,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用Init和Destory函数了,也方便了不少。

#include<iostream>
using namespace std;
// ⽤最新加了构造和析构的C++版本Stack实现
bool isValid(const char* s) 
{
	Stack st;
	while (*s)
	{
		if (*s == '[' || *s == '(' || *s == '{')
		{
			st.Push(*s);
		}
		else
		{
			// 右括号⽐左括号多,数量匹配问题
			if (st.Empty())
			{
				return false;
			}
			// 栈⾥⾯取左括号
			char top = st.Top();
			st.Pop();
			// 顺序不匹配
			if ((*s == ']' && top != '[')
				|| (*s == '}' && top != '{')
				|| (*s == ')' && top != '('))
			{
				return false;
			}
		}
		++s;
	}
	// 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题
	return st.Empty();
}
// ⽤之前C版本Stack实现
bool isValid(const char* s) 
{
	ST st;
	STInit(&st);
	while (*s)
	{
		// 左括号⼊栈
		if (*s == '(' || *s == '[' || *s == '{')
		{
			STPush(&st, *s);
		}
		else // 右括号取栈顶左括号尝试匹配
		{
			if (STEmpty(&st))
			{
				STDestroy(&st);
				return false;
			}
			char top = STTop(&st);
			STPop(&st);
			// 不匹配
			if ((top == '(' && *s != ')')
				|| (top == '{' && *s != '}')
				|| (top == '[' && *s != ']'))
			{
				STDestroy(&st);
				return false;
			}
		}
		++s;
	}
	// 栈不为空,说明左括号⽐右括号多,数量不匹配
	bool ret = STEmpty(&st);
	STDestroy(&st);
	return ret;
}

析构函数总的来说有以下的特征:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值。 (这里跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4. 对象生命周期结束时,系统会自动调用析构函数。
5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数。
6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。

7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构。

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mljy.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值