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