前言
自从进入C++之后,语言就从面向过程改为了面向对象。本篇博客就继续带大家认识类和对象,和上一篇《初识C++(序)》不同。这次我们会借用举例一个类,方便大家认识类的使用,成员变量以及函数。
本次举例会比较简单,举例一个日期类。不包含指针在类中,所以不需要深层拷贝,之后碰到栈和队列之后会继续深化这一块。将拷贝构造改为深拷贝——及拷贝指针指向的资源。那么让我们开始博客的内容吧。
一、成员变量
首先我们需要定义出类,根据习惯将成员变量作为保护变量,不让外界访问它的资源。
class Date
{
// 成员声明
private:
int _year;
int _month;
int _day;
};
日期之中包含年月日,所以在类里面设置了如上三个变量。
二、构造函数
构造函数一般使用初始化列表初始化成员变量,方法如下:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
构造函数这里增加了缺省值,这样就能够有默认构造。初始化列表就类似于“; , ,”这样的结构,初识化的方式是调用该成员变量的构造函数。如果我们类里面有其他类,比如说我们这里增加一个“TIME”自定义类型,我们就可以初始化它。
class TIME
{
public:
TIME(int h = 1)
:_h(h)
{}
private:
int _h;
};
// 增加TIME的时候别忘了定义_time作为成员变量
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1, int h = 1)
:_year(year)
,_month(month)
,_day(day)
,_time(h)
{}
这样也能够初始化自定义的类。
另一方面,如果初始化但不赋值给成员变量,那么自定义变量会调用自己的默认构造,而内置类型会是随机值,当然这是由编译器决定的。
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
,_month(month)
{}
由此_day可能是随机值,也可能是0.
关于初始化列表有以下总结:
1.所有的成员变量都会通过初始化列表初始化,且只能初始化一次。
2.const对象作为成员不能赋值需要使用初始化列表初始化,引用只能初始化一次,故也需要走初始化列表。
3.如果没有在初始化列表里初始化,会自动调用默认构造。
4.C++11支持成员变量开始时给缺省值。
5.初始化列表的初始化顺序是成员变量声明的顺序,而不是列表顺序。
//4.
class Date
{
// 5.
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
:_year(year)
,_day(day)
,_month(month)
{} // 改变顺序后初始化顺序还是声明顺序
// 成员声明
private:
int _year = 1921;
int _month = 7;
int _day = 23;
};
三、析构函数
这里的成员变量都是内置类型,所以机器自动生成的析构函数就可以代劳了,但是想全部置为0,也可以自己写。
// 析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
}
四、拷贝构造
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
简单粗暴,别忘了加“&”,以及最好加上const修饰。
五、赋值重载
1、=的重载
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
和拷贝构造很像,别忘了返回*this,这里是有返回值的。
2、+=与+的重载
Date+=Date是没有意义的,所以这里重载的是Date+=(int)day。这种重载可以帮助计算第几天之后是什么日子。
// 日期+=天数
Date& operator+=(const int day)
{
// 如果输入负数,就转移到-=
if(day < 0)
{
return *this -= (-day);
}
_day += day;
// 如果日数大于本月最大日数数
while(_day > GetMonthDay(_year, _month))
{
// 减去最大日数
_day -= GetMonthDay(_year, _month);
// 月数进位
++_month;
// 年数进位
if(_month > 12)
{
_year++;
_month = 1;
}
}
return *this;
}
需要注意的点也写在代码的注释里在。(1)防止传入的数是负数,如果是负数传到“-=”的重载里。(2)如果日期大过每月的最大天数需要进位,月大过12年需要进位。
最后需要加上计算每月最大天数的函数,别忘了计算闰月:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int day[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int ret = day[month];
// 如果是二月闰年,就需要加1天
if((month == 2) && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)))
{
ret++;
}
return ret;
}
+的重载就套一层+=,创建临时变量来增加就行。因为是临时的所以返回值不能传引用。
// 日期+天数
Date operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
其实也可以反过来,将+嵌套在+=里,但是这样的话需要多几次拷贝构造,所以就写+=嵌套在+里对计算机更加友好。
这个道理在-和-=里是相同的意义。
3、-=与-的重载
// 日期-天数
Date operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
// 日期-=天数
Date& operator-=(int day)
{
// 如果day为负数,就转到+=
if(day < 0)
{
return *this += (-day);
}
_day -= day;
// _day如果非法
while(_day < 1)
{
// 向月借位
_month--;
// _month非法,就向_year借位
if(_month < 1)
{
_month = 12;
_year--;
}
// 将借来的天数加上
_day += GetMonthDay(_year, _month);
}
return *this;
}
-=的重载也需要判断传入的数是否为负数,如果是负数就传入到+=中。这里计算日期的时候就需要向月借日子,直到日期为正数。
和+不同-可以发生在Date-Date中。这样可以计算两个日期之间相距的天数,正数表示前者大,负数表示后者大。那么我们需要假定前者大,并将两个对象传给临时对象,计算中间相距的天数,并记录符号。
// 日期-日期 返回天数
int operator-(const Date& d)
{
int ret = 0;
int flag = 1;
Date min = d;
Date max = *this;
if(min > max)
{
min = *this;
max = d;
flag = -1;
}
while(min != max)
{
min++;
ret++;
}
return ret * flag;
}
如果你觉得上述方法过于简单,这是因为计算机计算的速度很快的,所以采用一个一个加的比较。也有其他的方法。先计算“x - y年1月1日”之间所差的天数,然后将月也转化为天数最后得到结果也是可以的,这里的代码就不写了,留个读者自行探索。
4、++与--的重载
// 前置++
Date& operator++()
{
return *this += 1;
}
// 后置++
Date operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
这里的重载可以嵌套之前写的+=和-=函数,同时函数参数为空的表示前置的一位操作符,函数参数中有“int”的表示后置的一位操作符。
5、比较大小的操作符重载
日期类比较大小可以看做是比较日期的前后。而且这里能够先写出“>”和“==”的重载,或者“<”和“==”的重载。这样其他的比较符号可以嵌套出来。比如“>=”可以表示为“<”的取反也可以表示为“>”和“==”的共同判断。实际情况如下:
// >运算符重载
bool operator>(const Date& d)
{
// 按照顺序比较年月日大小
if(_year > d._year)
{
return true;
}
else if(_year == d._year)
{
if(_month > d._month)
{
return true;
}
else if(_month == d._month)
{
return _day > d._day;
}
}
return false;
}
// ==运算符重载
bool operator==(const Date& d)
{
// 满足全部相等为真
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// >=运算符重载
bool operator >= (const Date& d)
{
return *this > d || *this == d;
}
// <运算符重载
bool operator < (const Date& d)
{
return !(*this >= d);
}
// <=运算符重载
bool operator <= (const Date& d)
{
return !(*this > d);
}
// !=运算符重载
bool operator != (const Date& d)
{
return !(*this == d);
}
直接就是抛转引玉了,如果在其他类中可以比较,也可以曹勇以上嵌套的方法。
当然这里最好也把this用const修饰一下,具体方法上次也说了,在函数后加const,例如:
// !=运算符重载
bool operator != (const Date& d) const
{
return !(*this == d);
}
六、友元函数
本来这里是想写“<<”和“>>”的重载的。如果这些符号的重载放到类里面的话,例如:
istream& operator>>(istream& in)
那么我们要调用的话就需要这样写:
Date d;
d >> cin;
这是不符合我们输入输出的习惯的,所以我们就把这个函数放到类的外面并修改为如下:
istream& operator>>(istream& in, Date& d);
这样就能够解决this指针的问题,因为类里面的函数会默认传入一个this指针,只不过在第一个并且影藏了显示。如果放到外面就行了,但是随之而来的又有另一个问题。
istream& operator>>(istream& in, Date& d)
{
cout << "请依次输入年月日:";
while(1)
{
in >> d._year >> d._month >> d._day;
if(0 < d._month && d._month < 13 && 0 < d._day && d._day < d.GetMonthDay(d._year, d._month))
{
break;
}
else
{
cout << "输入错误,请重新输入" << endl;
}
}
return in;
}
ostream& operator<<(ostream& out, Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
根据“>>”函数的重载,我们需要调用对象中的成员,但是由于重构函数不在类里面,无法调用被保护的对象。那么我们想得到参数有两种方法:(1)设置函数让他返回成员变量的引用。(2)让重载函数作为类的友元函数。
友元函数的写法如下:
class Date
{
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, Date& d);
private:
int _year;
int _month;
int _day;
};
这样重载函数就能正常使用了,注意这里的“istream”和“ostream”都不能用“const”修饰。
别问为什么,我只知道用了会报错。
七、其他
1、内部类
内部类就是在类里面定义一个其他的类,这个类只能在前一个类的里面使用。就例如这里提到的类“TIME”,可以作为“Date”的内部类:
class Date
{
class TIME
{
public:
TIME(int h)
:_h(h)
{}
private:
int _h;
};
private:
int _year;
int _month;
int _day;
TIME _time;
};
2、初始化顺序
class A
{
public:
A(int a = 1)
:_a1(a)
,_a2(_a1)
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
根据以上这个类初始化,因为“_a2”是先声明的,所以会先走“_a2(_a1)”,然后在是“_a1(a)”。如果调用:
A aa;
那么根据我电脑的结果会输出“1 0”,这也就证明了之前所说的初始化列表的初始化顺序只和成员变量的声明顺序有关。
3、类的声明
和函数定义和声明可以分开一样,类也可以提前声明,这样计算机就能找到所需要的类了。例如:
class B
{
C _c;
};
class C
{
int _n = 1;
};
C的定义在B的后面,但是B中就已经有了C,这个时候我们就需要声明C的存在。
class C;
class B
{
C _c;
};
class C
{
int _n = 1;
};
这样就没问题了。
作者结语
到这里,博客也就暂时告一段落了,其实类里面还有很多东西,就比如iterator等等。日常生活中所使用的程序还会包括各种嵌套包装,以及模版等等。想想就有些复杂,所以点到为止,免得脑子疼。
还有一些实用的函数没有说,这些是C++升级之后来的,很方便。下一期博客可能就会给大家这么一期内容,希望大家支持。