• 机械转码日记【14】C++运算符重载的应用——实现一个日期类计算器


    目录

    前言

    1.运算符重载

    2.赋值重载函数:operator=

    2.1不写赋值重载函数,编译器会默认生成

    3.实现日期计算器

    3.1日期类的构造函数

    3.2函数复用定义">",">=","<","<=","==","!="

    3.2.1复用"<","=="去实现"<="

    3.2.2复用"<=",去实现">"

    3.2.3复用"<",去实现">="

     3.2.4复用"==",去实现"!="

    3.3"+"和"+="

    3.3.1"+"

    3.3.2分清楚拷贝构造函数和赋值重载函数

    3.3.3为什么赋值重载和拷贝构造函数里面的参数都建议用const修饰

    3.3.4"+="

    3.3.5+和+=互相复用的优劣

    3.4"-"和"-="

    3.5前置++,--和后置++,--

    3.6"-"的另外一种重载形式,日期对象-日期对象

    3.7const修饰成员


    前言

    这篇博客主要是讲了C++的运算符重载,在一个类中,我们不显式写出赋值重载函数,编译器会自动生成一个浅拷贝的赋值重载函数;同时写出一个日期计算器能够加深我们前面所学知识的印象。新人创作者,欢迎大佬们提出你们宝贵的意见和建议!本篇博客的代码已经上传到我的码云了,欢迎有需要的朋友们自取!日期类计算器代码

    1.运算符重载

    我们前面写的日期类,再某种情况下可能要进行比较,比如比较一个日期谁大谁小,但是可以直接用>和<去比较大小吗?显然是不行的,对于内置类型(int,double,float......)我们完全可以直接用>和<去比较大小;对于自定义类型,C++引入了运算符重载的功能,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。以<为例,其定义方式为:

    1. bool operator<(const Date& d)
    2. {
    3. if (_year < d._year
    4. || (_year == d._year && _month < d._month)
    5. || (_year == d._year && _month == d._month && _day < d._day))
    6. {
    7. return true;
    8. }
    9. else
    10. {
    11. return false;
    12. }
    13. }
    可以看到我们写的<运算符重载,它是一个函数的形式,它的返回值是bool类型的,函数的参数其实有两个,一个是this指针,一个是常引用日期类型d;运算符的重载函数一般写在类里面的公有成员函数内。
    我们要调用这个运算符是如何调用呢:

    可以看到有两种方式:

    • 以我们调用对象的成员函数的形式调用:d1.operator<(d2)
    • 直接写成d1<d2,在这里编译器会自动给我们处理

    .*::sizeof?:.    注意以上5个运算符不能重载!

    2.赋值重载函数:operator=

    如果我们要把一个日期类的值赋值给另一个日期类,比如Date d1(2022,5,23),Date d2,d2 = d1;就需要赋值重载,你可能会说,这还不容易吗?你可能会写成这样:

    1. class Date
    2. {
    3. public:
    4. //普通构造
    5. Date(int year = 1, int month = 1, int day = 1)
    6. {
    7. _year = year;
    8. _month = month;
    9. _day = day;
    10. }
    11. //赋值重载
    12. void operator=(const Date& d)
    13. {
    14. _year = d._year;
    15. _month = d._month;
    16. _day - d._day;
    17. }
    18. private:
    19. int _year; // 年
    20. int _month; // 月
    21. int _day; // 日
    22. };

    但是这种情况是不能使用连等的:

    原因就在于我们的返回类型是void,void是不能赋给别的值的,因此我们应该把返回类型改成Date:

    1. //赋值重载
    2. Date operator=(const Date& d)
    3. {
    4. _year = d._year;
    5. _month = d._month;
    6. _day - d._day;
    7. return *this;
    8. }

    更优化的写法是把Date返回改成引用返回,因为如果是值返回,会调用一次拷贝构造函数,会有内存的消耗,而引用返回不是,引用返回直接返回这个变量的别名,且这里出了函数的作用域,*this的内容还在,用引用返回是最优解!

    1. //赋值重载
    2. Date& operator=(const Date& d)
    3. {
    4. _year = d._year;
    5. _month = d._month;
    6. _day - d._day;
    7. return *this;
    8. }

    别以为这就完了,我们还有最优化的写法,假如我们写错了,写成了自己赋值给自己,比如Date d1(2022,5,23);d1 = d1;这种情况其实如果调用我们上面写的是会浪费内存时间的,那我们就再做一层改进:

    1. //赋值重载
    2. Date& operator=(const Date& d)
    3. {
    4. if (this != &d)
    5. {
    6. _year = d._year;
    7. _month = d._month;
    8. _day - d._day;
    9. }
    10. return *this;
    11. }

    2.1不写赋值重载函数,编译器会默认生成

    其实我们不写赋值重载函数,编译器会默认生成一个:

    这个时候其实默认生成的赋值重载函数是浅拷贝,对于日期类这样的我们可以不写,但是像我们上篇博客中所提到的栈类,我们不能不写。

    3.实现日期计算器

    其实我们在项目中,类的声明和定义经常是分离的,所以我们写日期类计算器也进行声明变量分离,如下图定义一个Date.h用来声明日期类的成员变量和成员函数,在Date.cpp中用来定义日期类。

    接着我们在Date.h中声明我们的日期类:

    1. #pragma once
    2. #include<iostream>
    3. #include<assert.h>
    4. //项目里面尽量不要全展开,防止命名冲突
    5. using std::cout;
    6. using std::cin;
    7. using std::endl;
    8. class Date
    9. {
    10. public:
    11. //四年一闰,百年不闰,四百年一闰
    12. bool isLeapYear(int year)
    13. {
    14. return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    15. }
    16. int GetMonthDay(int year, int month);
    17. Date(int year = 1, int month = 1, int day = 1);
    18. //拷贝构造和赋值不需要写,因为浅拷贝足够了
    19. //Date(const Date& d);
    20. //Date& operator=(const Date& d);
    21. void Print()
    22. {
    23. cout << _year << "-" << _month << "-" << _day << endl;
    24. }
    25. Date operator+(int day);
    26. Date& operator+=(int day);
    27. Date operator-(int day);
    28. Date& operator-=(int day);
    29. // ++d1
    30. Date& operator++();// 前置
    31. // d1++
    32. Date operator++(int);// 后置
    33. Date& operator--();// 前置
    34. Date operator--(int);// 后置
    35. // d1 - d2
    36. int operator-(const Date& d);
    37. bool operator==(const Date& d);
    38. bool operator<(const Date& d);
    39. bool operator>(const Date& d);
    40. bool operator>=(const Date& d);
    41. bool operator!=(const Date& d);
    42. // d1 <= d2
    43. bool operator<=(const Date& d);
    44. private:
    45. int _year;
    46. int _month;
    47. int _day;
    48. };

    在这里我们不全展开命名空间(实际中在项目里也是一样,防止我们定义的变量与库里面的发生命名冲突);我们不自己写析构函数,因为我们并不需要特殊的功能,变量除了作用域直接销毁就行;拷贝构造函数和赋值拷贝函数我们也不需要写,因为对于日期类来说,浅拷贝已经足够了,我们直接使用编译器默认生成的就行。

    3.1日期类的构造函数

    我们先来实现日期类的构造函数,因为一年不同的月有不同的天数,年也有平年和闰年之分,所以我们在初始化日期类的对象时,要判断他合不合法,因此日期类的构造函数需要调用两个函数,一个是获取当前月的天数判断合法不合法,另一个是判断当前的年是否是闰年(闰年的2月为29天)。

    1. //判断闰年的函数:四年一闰,百年不闰,四百年一闰
    2. bool isLeapYear(int year)
    3. {
    4. return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    5. }
    6. //获取当前月的天数的函数:
    7. int Date::GetMonthDay(int year, int month)
    8. {
    9. assert(year >= 0 && month > 0 && month < 13);
    10. //static,因为它频繁调用,所以加上static就可以节约内存
    11. //多线程读取数据是没问题的
    12. const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
    13. if (month == 2 && isLeapYear(year))
    14. {
    15. return 29;
    16. }
    17. else
    18. {
    19. return monthDayArray[month];
    20. }
    21. }

    以上两个函数,实现方法我相信大家都已经很明白了,但是这里有个细节需要大家注意一下,可以看到我在monthDayArray数组前加了const和static修饰;因为GetMonthDay函数我们需要频繁调用,那么如果我们不加static,每次调用都会生成一个数组,出了函数作用域又被销毁了,这样其实会很影响程序运行的效率,因此加上static我们只会在第一次调用GetMonthDay函数时会创建这个数组,此时数组被存放在静态区,当main函数销毁时才会销毁数组,这样提高了程序的运行效率;加const是为了不让这个数组被修改,也为了线程安全,因为多线程读取数据是不会影响线程安全的,而写数据会。

    1. //构造函数,声明定义分离
    2. //声明给了缺省,定义就不用给了
    3. Date::Date(int year, int month, int day)
    4. {
    5. if (year>0 && month <= 12 && day <= GetMonthDay(year,month) && month > 0 && day > 0)
    6. {
    7. _year = year;
    8. _month = month;
    9. _day = day;
    10. }
    11. else
    12. {
    13. cout << "构造失败" << endl;
    14. }
    15. }

    上述是我们的构造函数,定义构造函数,因为我们是声明和定义分离,从上面的代码我们已经知道构造函数的声明是带了缺省值的,那么我们定义构造函数时,就不能再带缺省值了(原因请看机械转码日记【7】缺省参数部分)。

    3.2函数复用定义">",">=","<","<=","==","!="

    其实我们在实际项目过程中,要尽可能的去复用我们已经定义的函数,这样不仅可以缩短代码的篇幅长度,也可以减小出错的概率,在这里我们就定义"<"和"==",然后复用这两个运算符重载函数去定义其他的函数。

    1. //能复用的情况尽可能复用
    2. bool Date:: operator<(const Date& d)
    3. {
    4. if( (_year == d._year && _month == d._month && _day < d._day)
    5. || (_year == d._year && _month < d._month)
    6. || (_year < d._year))
    7. {
    8. return true;
    9. }
    10. else
    11. {
    12. return false;
    13. }
    14. }
    15. bool Date:: operator==(const Date& d)
    16. {
    17. return _year == d._year
    18. && _month == d._month
    19. && _day == d._day;
    20. }

    3.2.1复用"<","=="去实现"<="

    1. //复用"<","=="去实现"<="
    2. bool operator<=(const Date& d)
    3. {
    4. return *this < d || *this == d;
    5. }

    3.2.2复用"<=",去实现">"

    1. //复用"<=",去实现">"
    2. bool operator>(const Date& d)
    3. {
    4. return !(*this <= d);
    5. }

    3.2.3复用"<",去实现">="

    1. //复用"<",去实现">="
    2. bool operator>=(const Date& d)
    3. {
    4. return !(*this < d);
    5. }

     3.2.4复用"==",去实现"!="

    1. //复用"==",去实现"!="
    2. bool operator!=(const Date& d)
    3. {
    4. return !(*this == d);
    5. }

    3.3"+"和"+="

    3.3.1"+"

    1. Date Date::operator+(int day)
    2. {
    3. Date ret(*this);//需要返回临时变量,防止原来的值被修改
    4. ret._day += day;
    5. while (ret._day > GetMonthDay(ret._year, ret._month))
    6. {
    7. ret._day -= GetMonthDay(ret._year, ret._month);
    8. ret._month++;
    9. if (ret._month == 13)
    10. {
    11. ++ret._year;
    12. ret._month = 1;
    13. }
    14. }
    15. return ret;
    16. }

    上述是我们实现"+"的写法,有两个地方需要注意,一个是我们需要返回一个临时变量,防止原来的值被修改,如图:

    另一个需要注意的地方是我们在这里不能使用引用返回,因为在这里我们是返回一个临时变量,这个临时变量出了作用域就被销毁了,引用返回会造成内存的非法访问!

    3.3.2分清楚拷贝构造函数和赋值重载函数

    请看下面这段代码:

    1. void test2()
    2. {
    3. Date d1(2022, 5, 28);
    4. cout << endl;
    5. Date d2 = d1+100;//这里是拷贝还是赋值呢?
    6. cout << endl;
    7. Date d3;
    8. cout << endl;
    9. d3 = d1;//这里是拷贝还是赋值呢?
    10. }

    请问Date d2 = d1+100和d3 = d1这两句语句是调用了拷贝构造函数还是赋值重载函数呢?因为我们前面没手动写出赋值重载函数和拷贝构造函数,所以我们验证不了,所以现在我们为了验证,手动把这两个函数写出来:

    接下来我们开始验证: 

    可以看到Date d2 = d1+100是调用了拷贝构造(+调用了一次,拷贝给d2调用了一次),而d3 = d1是调用的赋值重载函数,我们总结一下:拷贝构造是指用一个对象去初始化另一个同类型的对象,而赋值是两个已经存在的对象去进行操作。

    3.3.3为什么赋值重载和拷贝构造函数里面的参数都建议用const修饰

    我们把我们刚刚自己写的赋值重载和拷贝构造里的参数里的const去掉,再次运行一下,看看会发生什么:

     程序报错了,为什么呢?我们来分析一下原因:

     因为在实现d1+100时,会调用operator+函数,返回ret时,由于他是一个类对象,返回时会生成一个临时对象,而临时对象具有常性,当d1+100作为右值赋值给d2时,调用拷贝构造函数,但是此时的拷贝构造函数的参数时Date&类型,不是const Date&,这相当于权限的放大,自然就会报错!

    3.3.4"+="

    +=和+的逻辑是一样的,但是+=之后原来的值会变,所以不需要返回临时变量,出了作用域this指向的内容也还在,这样我们用引用返回就可以了:

    1. Date& Date::operator+=(int day)
    2. {
    3. if (day < 0)
    4. day = -day;
    5. _day += day;
    6. while (_day > GetMonthDay(_year, _month))
    7. {
    8. _day -= GetMonthDay(_year, _month);
    9. _month++;
    10. if (_month == 13)
    11. {
    12. ++_year;
    13. _month = 1;
    14. }
    15. }
    16. return *this;
    17. }

    在这里还有另一个需要注意的地方,就是我们的day如果是负值,是会报错的,比如:

    可以看到我们的日期时非法的,一个月是没有-72日的,因此我们必须加上如果day是负数的的处理程序。

    3.3.5+和+=互相复用的优劣

    其实我们实现+,可以复用+=;同样的,我们实现+=,也可以复用+;代码如下:

    1. /* +复用+= */
    2. Date Date::operator+(int day)
    3. {
    4. Date ret(*this);
    5. ret += day;
    6. return ret;
    7. }
    8. Date& Date::operator+=(int day)
    9. {
    10. if (day < 0)
    11. return *this -= -day;
    12. _day += day;
    13. while (_day > GetMonthDay(_year, _month))
    14. {
    15. _day -= GetMonthDay(_year, _month);
    16. _month++;
    17. if (_month == 13)
    18. {
    19. ++_year;
    20. _month = 1;
    21. }
    22. }
    23. return *this;
    24. }
    25. /* +=复用+ */
    26. Date Date::operator+(int day)
    27. {
    28. Date ret(*this);
    29. ret._day += day;
    30. while (ret._day > GetMonthDay(ret._year, ret._month))
    31. {
    32. ret._day -= GetMonthDay(ret._year, ret._month);
    33. ret._month++;
    34. if (ret._month == 13)
    35. {
    36. ++ret._year;
    37. ret._month = 1;
    38. }
    39. }
    40. return ret;
    41. }
    42. Date& Date::operator+=(int day)
    43. {
    44. *this = *this + day;
    45. return *this;
    46. }

    那么这两种方式哪一种效率更高呢?

    答案是+复用+=效率高一下,因为+会调用两次拷贝构造,如果+=复用+,单独写+=是不用调用拷贝构造的,但是复用了+之后,又增加了两次拷贝构造,很划不来。 

    3.4"-"和"-="

     写完了+和+=,想必-和-=也很好些吧!代码如下:

    1. /* -复用-= */
    2. Date Date::operator-(int day) const
    3. {
    4. Date ret(*this);
    5. ret -= day;
    6. return ret;
    7. }
    8. Date& Date::operator-=(int day)
    9. {
    10. if (day < 0)
    11. day = -day;
    12. _day -= day;
    13. while (_day <= 0)
    14. {
    15. --_month;
    16. if (_month == 0)
    17. {
    18. _month = 12;
    19. --_year;
    20. }
    21. _day += GetMonthDay(_year, _month);
    22. }
    23. return *this;
    24. }

    3.5前置++,--和后置++,--

    如何区分前置++和后置++或者前置--和后置--呢?因为它们的符号都一样,其实C++规定了一种方式,就是用参数区分,如果是后置++或者--,运算符重载的参数里会带一个int值:

     那么我们如何实现前置++和后置++呢?首先我们要搞清楚,前置++是返回++后的值,后置++是返回++前的值。其代码如下:

    1. // ++d1
    2. Date& operator++() // 前置
    3. {
    4. *this += 1;
    5. return *this;
    6. }
    7. // d1++
    8. Date operator++(int) // 后置
    9. {
    10. Date tmp(*this);
    11. *this += 1;
    12. return tmp;
    13. }

    同理前置--和后置--的值也如下:

    1. Date& operator--() // 前置
    2. {
    3. *this -= 1;
    4. return *this;
    5. }
    6. Date operator--(int) // 后置
    7. {
    8. Date tmp(*this);
    9. *this -= 1;
    10. return tmp;
    11. }

    3.6"-"的另外一种重载形式,日期对象-日期对象

    我们再来看看-的另外一种重载形式,日期对象-日期对象,这种情况是算出两个日期相差多少天。我们来实现一下:

    1. int Date:: operator-(const Date& d) const
    2. {
    3. int sum = 0;
    4. int flag = 1;
    5. Date min = d;
    6. Date max = *this;
    7. if (min > max)
    8. {
    9. flag = -1;
    10. min = *this;
    11. max = d;
    12. }
    13. while (min != max)
    14. {
    15. max--;
    16. sum++;
    17. }
    18. return sum * flag;
    19. }

    我们来算一下今天距离武汉第一例新冠肺炎(2019,12,8)已经多少天了(期望疫情早日结束),再用网页上的日期计算器来验证一下我们写的结果!

     结果是对的上的,我们写的代码没有错误。

    3.7const修饰成员

    我们先来看一下下面这段代码和他的运行结果:

    是不是感觉非常奇怪,为什么d1.print不会报错,而d.print就报错了,我们来分析一下: 

    首先我们print()函数的参数是Date*类型的,而d1.print传的参数也是Date*类型的,因此不会报错,但是Func函数里面的d.print函数所传的参数是const Date*类型,const Date*类型的参数传给Date*类型是属于权限的放大,是会报错的。那么应该如何修改呢?C++发明了const成员这样一个方法,以print成员函数为例,其使用方法如下:

    我们在定义成员函数时,在他的后面加上const,它实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。即修改成const Date*的类型。实际上在我们的日期类计算器中,如果我们不需要对this所指向的内容进行修改,就都可以加上const修饰更加安全。我们的">","<","<=",">=","==","!=","+","-"都可以用const来修饰。

  • 相关阅读:
    马尔可夫链
    Gateway 简介
    远程debug调试
    Linux下Couchbase的安装&升级&维护最佳指南
    Fragment中使用ViewPager滑动浏览页面
    Node.js(6)-node的web编程
    [题] 跳房子 #dp #二分答案 #单调队列优化
    使用 DM binary 部署 DM 集群
    代码随想录-Day25
    云耀服务器L实例部署Nextcloud企业云盘系统|华为云云耀云服务器L实例评测使用体验
  • 原文地址:https://blog.csdn.net/qq_52378490/article/details/124929225