C++类和对象(中)

目录

1.类的默认成员函数

2.构造函数

3.析构函数

4.拷贝构造函数

5.赋值运算符重载

 5.1运算符重载

operator==

operator>

operator>=

operator<=

 operator!=

 operator++(前置++)

operator++(int)

operator--

operator--(int)

operator<

operator<=

operator+=

operator+

operator-=

operator-

总结:


1.类的默认成员函数

        默认成员函数就是用户没有显式实现,编译器自动生成的成员函数叫做默认成员函数。一个类,通常情况下,我们不写的情况编译器会生成6个默认成员函数。如:构造函数主要完成初始化操作;析构函数主要完成清理操作;拷贝构造是使用同类对象初始化创建对象;赋值重载主要是把一个对象赋值给另一个对象。我们先简单了解前面四个就行,后面两个取地址重载不重要,我们了解就行。

        默认成员函数很重要,也很复杂,我们需要从来两个方面去学习。

        首先,我们不写的时候,编译器是怎么操作的;然后,编译器生成的是否满足我们的需求,如果不满足,我们就需要自己动手去写,但是其实大部分情况下都需要我们自己动手去写。

2.构造函数

        我们首先需要学习的是构造函数,需要知道的是,虽然名字叫做构造函数,但是它的主要内容不是创建对象,而是实例化时初始化对象。目的就是要替代我们以前的Init函数的功能。构造函数的自动调用的特点就完美的替代了我们以前的Init函数

构造函数的特点:

1.函数名和类名相同。

2.没有返回值。

3.对象实例化的时候自动调用对应的构造函数。

4.构造函数可以重载。

5.如果没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,只要我们写了构造函数,编译器就不会生成了。

6.无参构造函数,全缺省构造函数我,我们不写的时候编译器自动生成的构造函数,都称为默认构造函数。这三个里面只能有一个函数存在。

我们下面举的例子都默认为日期类。

可以看到,这三个构造函数的特点,首先函数名都是类名,然后没有返回值。其次是这三个函数是不能同时存在的,如果同时写的话,编译器会自动报错,大家可以自己试试。

因为我们上面在类里面写了三个构造函数,所以编译器不知道调用哪一个,所以出现报错。

我们现在就直接用第一个全缺省构造函数来举列子看看效果。

但是我们一般都是使用全缺省的构造函数,因为这样我们也能输入我们自己想要输入的值。

3.析构函数

        析构函数与构造函数相反,析构函数不是完成对对象的销毁,比如局部对象是存在于栈帧的,函数结束了,它就销毁了。析构函数需要做的是对有资源申请的函数进行释放。就比如我们在写栈的时候我们使用malloc去向堆上申请了一块空间,再函数结束的时候,我们需要释放他们,否则的话就会出现内存泄漏。其它的内置类型(如int ,double等)都不需要释放,编译器自己生成的析构函数都能解决。比如我们写的Date类就不需要写析构函数。

析构函数的特点:

1.析构函数名是在类名前加一个~。

2.无参数,无返回值。

3.一个类只能有一个析构函数。如果没有显式定义,编译器会自动生成一个析构函数。

4.对象生命周期结束后,系统会自动调用 析构函数。

5.如果类中没有申请资源,析构函数可以不写,直接使用编译器默认生成的析构函数,如Date;如果默认生成的析构函数可以用,就不用写析构。比如MyQueue,它是两个Stack构成的,它会默认调用Stack的析构函数。

6.一个局部域有多个对象时,C++规定先定义的后析构。

#include <iostream>
using namespace std;

class Stack
{
public:
	Stack(int n=4)
	{
		arr = (int*)malloc(sizeof(int) * n);
		if (arr == nullptr)
		{
			perror("malloc fail");
			return;
		}
		capacity = n;
		top = 0;
	};

	~Stack()
	{
		free(arr);
		arr = nullptr;
		capacity = top = 0;
	}

private:
		int* arr;
		int top;
		int capacity;
};
int main()
{
	Stack tmp;

	return 0;
}

可以简单看看上面写的一个栈的构造函数和析构函数。

class MyQueue
{
public :
	//不用写,直接调用栈的构造函数
	MyQueue()
	{}

	//也不用写,调用栈的析构函数
	~MyQueue()
	{}

private:
	Stack pushst;
	Stack popst;
};

我们使用栈实现队列的时候,就不用显式的实现构造和析构了。

4.拷贝构造函数

        如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。

拷贝构造的特点:
1.拷贝构造是构造函数的一个重载。

2.拷贝构造函数的参数只有一个且必须是类类型的引用,使用传值方式编译器直接报错,因为这样会引发无穷递归。

3.C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

4. 如果没有显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝(浅拷贝),对自定义类型成员变量会调用它的拷贝构造。

浅拷贝:举个例子就是当我们实现栈的时候,因为栈是指向了资源的,所以当我们进行浅拷贝的时候,S1,S2就会指向同一个空间,在入栈的时候就会导致数据错乱,析构的时候将一块空间析构两次,就会导致程序错误。

5.像Date这种没有指向资源,都是内置类型,编译器自动生成的拷贝构造就可以完成需要拷贝。我们记住,只要指向了资源的就需要我们自己写拷贝构造,没有指向资源,就用编译器自动生成的拷贝构造也行。

我们还是具体看代码吧。

可以看到拷贝构造看起来和构造函数差不多,但是形参完全不一样。自己可以下来写一写看一看。

需要注意的是,拷贝构造是需要在定义类的时候就使用拷贝构造,不能先定义,再拷贝赋值,这样的话就是我们马上后面需要学习的运算符重载的知识了。

像这样,先声明后拷贝就是错误的,这就涉及到运算符重载的知识了。

 像刚刚说了当我们申请资源的时候,需要深拷贝,就需要重新申请资源了。我们就一Stack的拷贝构造来看。

	//拷贝构造
	Stack(const Stack& st)
	{
		//重新申请一段和st大小一样的空间,再赋给*this
		int* tmp = (int*)malloc(sizeof(int) * st.capacity);
		if (tmp== nullptr)
		{
			perror("malloc fail");
			return;
		}
		arr = tmp;
		capacity = st.capacity;
		top = st.top;
	}

这样就行了。

5.赋值运算符重载

 5.1运算符重载

        当运算符用于类对象的时候,C++允许我们通过运算符重载的形式指定新的含义。C++规定类类型对象使用运算符的时候,必须转换调用对应运算符重载,若没有对应的运算符重载,则编译器会报错。我们常见的运算符如+-*/(加减乘除),这些运算符都用于我们的内置类型,比如int+int,int-int,doule+doule,double*double。这些都是系统都已经处理好的。但是比如果像我们上面写的Date日期类,如果我们想要日期加一个整数,那就是几天过后,这样就不能直接相加了。而且日期每月的天数不一样,还存在平年闰年等等一系列需要考虑的问题。

        运算符重载是具有独特名字的函数,它的名字是由operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回的类型和参数列表以及函数体。

        如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,我们前面讲过,成员函数都存在隐式指针,不知道的同学可以看一线前面那一节内容。因此运算符重载作为成员函数的时候,参数比运算少一个。

        需要注意的是,不能通过连接语法中没有的符号来创建新的操作符,如operator@。

        注意,.*   ::  sizeof   ?:   .  这五个运算符是不能够重载的。

        还有就是重载运算符必须要有意义,比如拿日期类来说,我们重载operator+就意味着两个日期类相加,而日期加日期就没有任何意义,而日期加天数,就可以算出几天后是几月几号,这样才有意义。还比如,日期减日期就能算出中间差多少天,这也是意义的。

注意:我们以下举列全部使用日期类来举例。

operator==

赋值符号,意味着我们现在可以用一个日期d1去给另一个日期d2赋值了。

	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

这样一个==的运算符就写好了,参数列表其实有两个第一个是*this。前面我们已经讲过了。其实上面这个运算符重载也是比较简单的。

接下来还有>,>=,<,<=,!=等一系列运算符。

operator>

我们判断日期大小的时候,首先比较的是年份,如果年份相等,再比较月份,月份相等再比较天数,这个逻辑还是很简单的。

// >运算符重载
bool Date:: operator>(const Date& d)
{
	if (this->_year < d._year)
	{
		return false;
	}
	else  if (this->_year == d._year)
	{
		if (this->_month < d._month)
		{
			return false;
		}
		else if (this->_month == d._month)
		{
			if (this->_day <= d._day)
			{
				return false;
			}
			else // (this->_day> d._day)
			{
				return true;
			}
		}
		else  // (this->_month > d._month)
		{
			return true;
		}
	}
	else   //(this->_year >d._year)
	{
		return true;
	}	
}

上面的代码逻辑已经很清晰了。

接下来我们写>=,<= 这些运算符重载就简单的多了,我们写>=只需要将=和>的运算符重载结合起来就行了。

operator>=

// >=运算符重载
bool Date::operator >= (const Date& d)
{
	return this->operator>(d) ||this->operator==(d);
}

这就是>=的逻辑,后面的很多运算符重载都只需要复用前面写过的代码就行了。

operator<=

// <=运算符重载
bool Date::operator <= (const Date& d)
{
	return !(this->operator>(d));
}

 operator!=

// !=运算符重载
bool Date::operator != (const Date& d)
{
	return !(this->operator==(d));
}

 operator++(前置++)

C++规定重载运算符++的时候,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加一个int形参,跟前置++构成运算符重载,方便区分。

// 前置++
Date& Date::operator++()
{
	*this += 1;

	return *this;
}

operator++(int)

// 后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;

	return tmp;
}

operator--

然后前置--和后置--和前面的逻辑一样。

// 后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this-= 1;
	return	tmp;
}

operator--(int)

// 前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}

operator<

// <运算符重载
bool Date::operator < (const Date& d)
{
	return !(this->operator>=(d));
}

对于<的运算符重载,我们直接将>=取反就行了。所以后面写<=也可以直接将>取反就行。

operator<=

// <=运算符重载
bool Date::operator <= (const Date& d)
{
	return !(this->operator>(d));
}

operator+=

现在我们需要写一个稍微难一点的运算符重载,日期加天数,日期加天数可以得到几天后是几月几日,这还是比较有用的。

// 日期+=天数
Date& Date::operator+=(int day)
{	
    //如果日期小于零,则需要调用-=运算符重载,后面我们会讲到
	if (day < 0)
	{
		day = -day;
		return *this -= day;
	}
    //现在我们将天数相加,如果超过了当月的天数
    //则月份加一,我们将当月的天数减掉,一直循环
	_day += day;
	while (_day >GetMonthDay(_year, _month))
	{	
	_day -= GetMonthDay(_year, _month);
	_month++;
        //当月份等于13时,就需要将年进1
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

逻辑还是很简单的,我们还需要写一个得到当年当月的天数的函数如下。

	// 获取某年某月的天数
	//因为该函数会被频繁调用,所以写在类里里面,就是内联函数
	int GetMonthDay(int year, int month)
	{	
		assert(month > 0 && month < 13);
		static	int arr[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		//如果是闰年,并且月份是2月就返回29天
		if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
		{
			return 29;
		}
			return arr[month];
		
	}

这样就完成了。

operator+

这样来说+就很好写了,我们调用一下就行了,只不过我们日期本身不改变就行了。

// 日期+天数
Date Date::operator+(int day)
{
	Date tmp = *this;

	tmp += day;

	return tmp;
}

operator-=

同样,现在我们来看一下-=,也是比较复杂的。

// 日期-=天数
Date& Date::operator-=(int day)
{	
    //同样,如果日期小于零,负负得正,我们只需要调用+=就行了
	if (day < 0)
	{
		day = -day;
		return *this += day;
	}
    //我们先让我们的日期相减,如果小于0,则就需要去向上一个月去借天数
	_day -= day;
	//2024 8 30 
	//			40
	while (_day <= 0)
	{
		_month--;
        //如果月份小于零,则就需要向上一年借了
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
        //再加上,知道天数变为正
		_day += GetMonthDay(_year,_month);
	}
	return *this;
}

operator-

-我们只需要复用-=就行了。

// 日期-天数
Date Date::operator-(int day)
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}

总结:

以上几个概念,还是比较复杂的,特别是运算符重载,这里我们只以日期类来举列子,以后遇到其它的类情况,也像这样处理就行。接下来,就需要同学们自己动手写代码才能理解这些概念了。

                ​​​​​​​        ​​​​​​​        ​​​​​​​        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值