文章目录
- 前言
- 一、赋值运算符重载
- 运算符重载
- 赋值运算符重载
- 赋值运算符不可重载为全局函数
- 前置++和后置++的重载
- 二、const修饰成员函数
- 三、取地址及const取地址操作符重载
- 四、日期类的实现
- 构造函数
- 日期 += 天数
- 日期 + 天数
- 日期 -= 天数
- 日期 - 天数
- 日期类的大小比较
- 日期类 > 日期类
- 日期类 == 日期类
- 日期类 >= 日期类
- 日期类 < 日期类
- 日期类 <= 日期类
- 日期类 != 日期类
- 日期类 - 日期类
- 五、流插入流提取运算符重载
- 总结
前言
继承上节的内容,本节内容依旧量大管饱!!
一、赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。
请记住这个目的!我们的接下来的一切都是围绕这个来展开
运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。
运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@。(需要是C/C++语法中存在)
- 重载操作符必须有一个类类型或枚举类型的操作数。(不能去重载运算符改变内置类型的行为) -> 其实要真那么搞也可以,但是很无聊,没什么实用的
- 用于内置类型的操作符,重载后其含义不能改变。-> 例如内置的整型+,不能改变其含义
- 作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
- sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。
- 并不是运算符都是需要重载的,需要看是否有存在的意义,参数部分需要对应顺序
// 来个实际例子
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
// 注意此时该函数的第一个形参默认为this指针
bool operator==(const Date& d)// 运算符重载函数
{
return _year == d._year
&&_month == d._month
&&_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 22);
Date d2 = d1;
// 注意优先级
cout << (d1 == d2) << endl; // 1
cout << d1.operator==(d2) << endl; // 与上一条语句等价
return 0;
}
显然第一种 d1 == d2 这种形式相当明了,就好像Date真的是我们的内置类型一样,其实说白了,还是编译器帮我们化成了第二种形式,不信我们可以看下汇编形式:
还是那句话,当你觉得轻松的时候,总是有人为你负重前行
赋值运算符重载
先上代码:
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1) // 构造函数
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d) // 赋值运算符重载函数
{
if (this != &d) // 防止会有 d1 = d1 这样自己赋值给自己
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()// 打印函数
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
我们要注意以下几点:
- 参数类型设置为引用,并用const进行修饰 -> 由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。并且我们也不改变它,那就用 const 来修饰
- 函数的返回值使用引用返回 -> 本质上是为了支持连续赋值,所以必须要有个返回值,而且很显然是返回左操作数 d3 = d2 = d1; 所以我们在这里返回 this 指针,且因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回,又省了拷贝多余的资源消耗
- 一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝 -> 没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy
// 请注意区分
Date d1(2024,9,22);
Date d2 = d1; // 拷贝构造
Date d3;
d3 = d1; // 赋值
// 拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象
// 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象
赋值运算符不可重载为全局函数
赋值运算符重载跟拷贝构造类似,如果不显式实现,编译器会生成一个默认的赋值运算符重载,此时用户再类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突,故而赋值运算符只能是类的成员函数(其他运算符函数可以重载为全局函数)
《C++ Primer》第5版P500有言:
默认生成赋值运算符重载对于内置类型与自定义类型处理方式:
- 内置类型成员变量直接赋值的
- 自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
同样的,我们有个深拷贝和浅拷贝的问题,方式还是跟拷贝构造方式一样,当涉及到动态资源申请的时候深拷贝,否则浅拷贝即可
前置++和后置++的重载
这两个妙就妙在一样就只有一个操作符,一个操作符在前一个在后
C++给出了它的解决方案:
//++d1
Date& operator++()
{
_day += 1;
return *this;
}
//d1++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
请注意,后置++重载函数中的参数int没有实际作用,只是为了与前置++构成函数重载,以便区分
二、const修饰成员函数
将const修饰的 “成员函数” 称之为 const 成员函数,const修饰类成员函数,实际修饰改成员隐含的 this 指针,表明在该成员函数中不能对类的任何成员进行修改,如图:
在讲解具体的实用场景前,我们还可以先来看看这四个问题,答案和解释我都会相应给出:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其他的非const成员函数吗?
- 非const成员函数内可以调用其他的const成员函数吗
答案是:不可以、可以、不可以、可以
解释:
5. 非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败
6. const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功
7. 在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败
8. 在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功
权限可以平移,也可以缩小,但是不可以放大
好,我们接下来来看以下代码:
// Print()成员函数没有被const修饰
int main()
{
Date d1(2024, 9, 22);
d1.Print(); // right,权限平移
const Date d2(2024, 9, 22);
d2.Print(); // err,权限放大
return 0;
}
但是并非所有函数都需要加上 const 修饰的!如果对成员变量进行读写访问的函数,不能加上 const ,另外,const修饰成员函数是修饰this指针的,那么 流插入>> 与 流提取<< 不是在类中实现,没有隐含的this指针,不能使用 const 修饰
三、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会默认生成的,无需你多虑
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
四、日期类的实现
学完了这6个默认成员函数,我们来写个日期类巩固一下吧:
class Date
{
public:
// 构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 打印函数
void Print() const;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 日期-天数
Date operator-(int day) const;
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
// 日期的大小关系比较
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
// 日期-日期
int operator-(const Date& d) const;
// 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可
private:
int _year;
int _month;
int _day;
};
构造函数
// 获取某年某月的天数
inline int GetMonthDay(int year, int month)
{
// 数组存储平年每个月的天数
static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int day = dayArray[month];
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
//闰年2月的天数
day = 29;
}
return day;
}
// 构造函数
Date::Date(int year, int month, int day)
{
// 检查日期的合法性
if (year >= 0
&& month >= 1 && month <= 12
&& day >= 1 && day <= GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
// 严格来说抛异常更好
cout << "非法日期" << endl;
cout << year << "年" << month << "月" << day << "日" << endl;
}
}
其中,我们注意一下这个GetMonthDay函数:
- 该函数可能被多次调用,所以我们最好将其设置为内联函数
- 函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组
- 逻辑与应该先判断 month == 2 是否为真,因为当不是2月的时候我们不必判断是不是闰年,短路判断
日期 += 天数
因为出了作用域对象还存在,于是我们用引用返回
逻辑如下:
1.若日已满,则日减去当前月的天数,月加一。
2.若月已满,则将年加一,月置为1
// 日期 += 天数
Date& Date::operator+=(int day)
{
if (day<0)
{
// 复用operator-=
*this -= -day;
}
else
{
_day += day;
// 日期不合法,通过不断调整,直到最后日期合法为止
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12)
{
_year++;
_month = 1;
}
}
}
return *this;
}
日期 + 天数
因为返回对象的值不变,但我们要返回变化后的值,于是就可以通过复用+=、创建临时变量tmp来解决,因为tmp出了作用域就销毁,于是我们用传值返回
// 日期 + 天数
Date Date::operator+(int day) const
{
Date tmp(*this);// 拷贝构造tmp,用于返回
// 复用operator+=
tmp += day;
return tmp;
}
日期 -= 天数
逻辑如下:
1.若日为负数,则月减一。
2.若月为0,则年减一,月置为12。
3.日加上当前月的天数
// 日期 -= 天数
Date& Date::operator-=(int day)
{
if (day < 0)
{
// 复用operator+=
*this += -day;
}
else
{
_day -= day;
// 日期不合法,通过不断调整,直到最后日期合法为止
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
}
return *this;
}
日期 - 天数
跟日期 + 天数类似,也是复用
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);// 拷贝构造tmp,用于返回
// 复用operator-=
tmp -= day;
return tmp;
}
日期类的大小比较
这里,我们将充分发挥复用的智慧!
日期类 > 日期类
逻辑:年大则大,月大则大,年月相等比日
bool Date::operator>(const Date& d) const
{
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 Date::operator==(const Date& d) const
{
return _year == d._year
&&_month == d._month
&&_day == d._day;
}
日期类 >= 日期类
逻辑:复用前面两个
bool Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
}
日期类 < 日期类
逻辑:>= 的反面就是 <
bool Date::operator<(const Date& d) const
{
return !(*this >= d);
}
日期类 <= 日期类
逻辑:> 的反面就是 <=
bool Date::operator<=(const Date& d) const
{
return !(*this > d);
}
日期类 != 日期类
逻辑:== 的反面就是 !=
bool Date::operator!=(const Date& d) const
{
return !(*this == d);
}
日期类 - 日期类
其实也就是算两个日期之差,它还是有点意义的
试想一下,假如你未来结婚,结婚到一半,新娘子问你我们几年几月几日遇见的?
你早有准备,一下就回答上来
突然,新娘子还不死心,问你遇见当天离今天一共经历多少天?
你就可以说:“不急,我先跑个程序”
这就是日期类 - 日期类的一个实际场景运用
有两种思路,请听我细细道来:
方法一:所谓日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值,这很容易,设置一个判别变量 flag 即可
// 日期-日期
int Date::operator-(const Date& d) const
{
Date max = *this;// 假设第一个日期较大
Date min = d;// 假设第二个日期较小
int flag = 1;// 此时结果应该为正值
if (*this < d)
{
// 假设错误,更正
max = d;
min = *this;
flag = -1;// 此时结果应该为负值
}
int n = 0;// 记录所加的总天数
while (min != max)
{
min++;// 较小的日期++
n++;// 总天数++
}
return n*flag;
}
方法二:所谓“年、月、日”,不过也是一种进位的方式,那我们可以全把数堆给“日”这个位,先将年等同,再将月等同,最后直接将两个日期的“日”位相减即可,只需要注意返回的是正值还是负值
int Date::operator-(const Date& d)
{
Date greD = (*this) > d ? *this : d;
Date smlD = (*this) < d ? *this : d;
while (greD._year > smlD._year) {
if (isLeapYear(greD._year - 1)) greD._day += 366;
else greD._day += 365;
greD._year -= 1;
}
while (greD._month > smlD._month) {
greD._month -= 1;
greD._day += GetMonthDay(greD._year, greD._month);
}
while (greD._month < smlD._month) {
greD._month += 1;
greD._day -= GetMonthDay(greD._year, greD._month);
}
int ret = greD._day - smlD._day;
return *this > d ? ret : -ret;
}
五、流插入流提取运算符重载
这个比较困难,有一些不懂的概念可以暂且放一下
其实,我们平时说cout、cin能自动识别类型,本质上就是因为重载,cout属于ostream类,cin属于istream类
假如我们 重载运算符<< 为成员函数,隐含的 this 指针占用第一个参数的位置,Date必须是左操作数,使用时候d1<<cout 是不符合我们的习惯的
于是,我们要将其重载为全局函数,并且把ostream& out放在第一个位置!但是又引出了另一个问题:类外不能访问类中的私有成员,如果将私有权限放开,就缺乏安全性,对此C++中有友元,接下来我们会涉及到,这里就使用下,虽然这个全局函数不在类中,但是可以访问私有成员函数
//友元,告诉该类这两个全局函数是我们的朋友,允许使用私有成员(在类中)
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "" << d._month << "" << d._day << "" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
总结
哈哈,这个中篇终于结束了,想不到吧,学Cpp的第一个大内容就如此困难!
还有个下篇呢,继续加油吧!