【C++】类和对象(中)(1)https://developer.aliyun.com/article/1514564?spm=a2c6h.13148508.setting.19.4b904f0ejdbHoA
2、赋值运算符重载
(1)赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率。
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
- 检测是否自己给自己赋值。
- 返回 *this :要复合连续赋值的含义。
class Date { public : Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date (const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; // 支持连续赋值 } Date& operator=(const Date& d) // 传引用返回 { if(this != &d) // 检测是否自己给自己赋值 { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year ; int _month ; int _day ; }; void TestDate() { Date d1(2023, 9, 13); Date d2(d1); Date d3(2023, 11, 28); d2 = d1 = d3; }
(2)赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; }; // 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数 Date& operator=(Date& left, const Date& right) { if (&left != &right) { left._year = right._year; left._month = right._month; left._day = right._day; } return left; }
编译失败:error C2801: “operator =”必须是非静态成员
原因 :赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是 类的成员函数 。
(3)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
注意 :内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time& operator=(const Time& t) { if (this != &t) { _hour = t._hour; _minute = t._minute; _second = t._second; } return *this; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; return 0; }
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?
像日期类这样的类是没必要的。但有些需要深拷贝的类,其内部往往是很复杂的,是需要用户显式定义赋值运算符重载函数来完成深拷贝的。
// 下面这个程序会崩溃,这里需要用深拷贝去解决。 typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType *_array; size_t _size; size_t _capacity; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; s2 = s1; return 0; }
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
3、前置++和后置++重载
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator++() // 前置++:返回+1之后的结果。 { _day += 1; return *this; // this 指向的对象函数结束后不会销毁,故以引用的方式返回提高效率。 } Date operator++(int) // 后置++是先使用后+1,因此需要返回+1之前的旧值 { Date temp(*this); // 在实现时需要先将this保存一份,然后给this+1 _day += 1; return temp; // 因为temp是临时对象,因此只能以值的方式返回,不能返回引用 } private: int _year; int _month; int _day; }; int main() { Date d; Date d1(2023, 9, 13); d = d1++; // d: 2023,9,13 d1:2023,9,14 d = ++d1; // d: 2023,9,14 d1:2023,9,14 return 0; }
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++ 规定:后置++重载时多增加一个 int 类型的参数,但调用函数时该参数不用传递,编译器自动传递。前置返回的是引用,后置返回的是值。
六、日期类的实现
1、Date.h
// Date.h #pragma once #include<iostream> #include<assert> #include<stdbool.h> using namespace std; class Date { public: // 获取某年某月的天数 int GetMonthDay(int year, int month) { assert(month >= 1 && month <= 12); // 每月的天数(这个函数会被频繁调用,每次进来都要重新定义数组,所以将其定义为静态的) // 默认是平年 static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int day = days[month]; if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0))) // 把month == 2写在前面可以直接筛选出更少的内容 { day += 1; // 闰年的二月是29天 } return day; } Date(int year = 1, int month = 1, int day = 1) // 全缺省的构造函数 { _year = year; _month = month; _day = day; //判断日期是否合法 if (_year < 0 || _month <= 0 || _month >= 13 || _day <= 0 || _day > GetMonthDay(_year, _month)) { cout << _year << "/" << _month << "/" << _day << "->"; cout << "非法日期" << endl; } } // 打印日期 void Print() { cout << _year << "/" << _month << "/" << _day << endl; } // 拷贝构造、赋值运算符、析构函数用编译器自动生成的就可以了(因为Date类是浅拷贝) // 日期 += 天数 --> d1 += 100 Date& operator+=(int day); // 日期 + 天数 --> d1 + 100 Date operator+(int day); // 日期 -= 天数 --> d1 -= 100 Date& operator-=(int day); // 日期 - 天数 --> d1 - 100 Date operator-(int day); // 前置++ Date& operator++(); // 编译器会解释为:Date& operator++(Date* const this); // 后置++ Date operator++(int); // 编译器会解释为:Date& operator++(Date* const this, int); // 前置-- Date& operator--(); // 后置-- Date operator--(int); // >运算符重载 bool operator>(const Date& d) { if (_year > d._year) return true; else if (_year == d._year && _month > d._month) return true; else if (_year == d._year && _month == d._month && _day > d._day) return true; else 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; // 复用operator>、operator== } // <运算符重载 bool operator<(const Date& d) { return !(*this >= d); // 复用operator>=,再取反 } // <=运算符重载 bool operator<=(const Date& d) { return !(*this > d); // 复用operator>,再取反 } // !=运算符重载 bool operator!=(const Date& d) { return !(*this == d); // 复用operator==,再取反 } // 日期 - 日期(返回相差天数) --> d1 - d2 int operator-(const Date& d); private: int _year; int _month; int _day; };
2、Date.cpp
(1)日期 += 天数(返回累加天数后的日期)
比如:d1 += 100
注意:d1本身要被更改,天数累加到 d1 上面去。
Date& Date::operator+=(int day) { if (day < 0) // 如果day是负数,就向前计算,相当于 -= { return *this -= -day; // 调用-=运算符重载函数 } _day += day; // 累加天数 // 日期不合法,需要进位 while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了 { _day -= GetDays(_year, _month); // 减去当前月的天数 _month++; // 月进位 if (_month == 13) // 判断当前月份是否合法 { _year++; // 年进位 _month = 1; // 更新为1月 } } return *this; /* 写法二:复用+运算符重载函数的代码 *this = *this + day; // d1等价于*this,对d1进行+天数操作,再赋值给d1 return *this; // 返回d1 */ }
(2)日期 + 天数(返回累加天数后的日期)
比如 :d1 + 100
注意:d1本身不能被更改,天数累加到一个临时对象上面去。
// 写法一: Date Date::operator+(int day) { Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改 tmp._day += day; // 累加天数 while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了 { tmp._day -= GetDays(tmp._year, tmp._month); // 减去当前月的天数 tmp._month++; // 月进位 if (tmp._month == 13) // 判断当前月份是否合法 { tmp._year++; // 年进位 tmp._month = 1; // 更新为1月 } } return tmp; // 返回临时对象 } // 写法二: Date Date::operator+(int day) { /* 复用 += 运算符重载函数的代码 */ Date tmp(*this); // 拷贝构造一份临时对象 tmp += day; // 对临时对象进行 += 天数操作 return tmp; // 返回临时对象 }
(3)日期 -= 天数(返回累减天数后的日期)
比如:d1 -= 100
Date& Date::operator-=(int day) { if (day < 0) // 如果day小于0,就往后计算,相当于 += { return *this += -day; // 调用+=运算符重载函数 } _day -= day; // 累减天数 while (_day <= 0) // 说明天数不够减了,需要向上一个月去借 { _month--; // 月份-1 if (_month == 0) { _year--; _month = 12; } _day += GetDays(_year, _month); // 借上一个月的天数 } return *this; }
(4)日期 - 天数(返回累减天数后的日期)
比如:d1 - 100
Date Date::operator-(int day) { // 复用 -= 运算符重载函数的代码 Date tmp(*this); // 拷贝构造一份临时对象 tmp -= day; // 对临时对象进行 -= 天数操作 return tmp; // 返回临时对象 }
(5)前置++ 和 后置++
注意:按正常的运算符重载规则,无法区分 前置++ 和 后置++,为了区分,这里做了一个特殊处理,给 后置++ 增加了一个 int 参数,这个参数仅仅是为了区分,使 前置++ 和 后置++ 构成重载。
// 前置++ // ++d1 Date& Date::operator++() { // 复用 += 运算符重载函数的代码 *this += 1; return *this; } // 后置++ // d1++ Date Date::operator++(int) { Date tmp(*this); // 保存当前对象自减前的值 *this += 1; // 复用 += 运算符重载函数的代码 return tmp; // 返回当前对象自减前的值 }
(6)前置-- 和 后置–
// 前置-- // --d1 Date& Date::operator--() { // 复用 -= 运算符重载函数的代码 *this -= 1; return *this; } // 后置-- // d1-- Date Date::operator--(int) { Date tmp(*this); // 保存当前对象自减前的值 *this -= 1; // 复用 -= 运算符重载函数的代码 return tmp; // 返回当前对象自减前的值 }
(7)日期 - 日期(返回相差的天数,有正负之分)
比如:d1 - d2
思路:让小的日期不断往后++,直到等于大的日期,统计加了多少次,就相差多少天。
- 大的日期 - 小的日期 = 正的天数
- 小的日期 - 大的日期 = 负的天数
int Date::operator-(const Date& d) { // 判断出大的日期和小的日期 Date max = *this; Date min = d; int flag = 1; // 加一个flag变量来控制天数的正负 if (max < min) { max = d; min = *this; flag = -1; } // 让小的日期累加天数,加了多少次,说明就相差了多少天 int count = 0; while (min != max) { ++min; ++count; } return flag * count; }
【总结】
// Date.cpp #include "Date.h" // 日期 += 天数 Date& Date::operator+=(int day) { if (day < 0) // 如果day是负数,就向前计算,相当于 -= { return *this -= -day; // 调用-=运算符重载函数 } _day += day; // 累加天数 // 日期不合法,需要进位 while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了 { _day -= GetDays(_year, _month); // 减去当前月的天数 _month++; // 月进位 if (_month == 13) // 判断当前月份是否合法 { _year++; // 年进位 _month = 1; // 更新为1月 } } return *this; } // 日期 + 天数 Date Date::operator+(int day) { Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改 tmp._day += day; // 累加天数 while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了 { tmp._day -= GetDays(tmp._year, tmp._month); // 减去当前月的天数 tmp._month++; // 月进位 if (tmp._month == 13) // 判断当前月份是否合法 { tmp._year++; // 年进位 tmp._month = 1; // 更新为1月 } } return tmp; // 返回临时对象 } // 日期 -= 天数 Date& Date::operator-=(int day) { if (day < 0) // 如果day小于0,就往后计算,相当于 += { return *this += -day; // 调用+=运算符重载函数 } _day -= day; // 累减天数 while (_day <= 0) // 说明天数不够减了,需要向上一个月去借 { _month--; // 月份-1 if (_month == 0) { _year--; _month = 12; } _day += GetDays(_year, _month); // 借上一个月的天数 } return *this; } // 日期 -= 天数 Date Date::operator-(int day) { // 复用 -= 运算符重载函数的代码 Date tmp(*this); // 拷贝构造一份临时对象 tmp -= day; // 对临时对象进行 -= 天数操作 return tmp; // 返回临时对象 } // 前置++ Date& Date::operator++() { // 复用 += 运算符重载函数的代码 *this += 1; return *this; } // 后置++ Date Date::operator++(int) { Date tmp(*this); // 保存当前对象自减前的值 *this += 1; // 复用 += 运算符重载函数的代码 return tmp; // 返回当前对象自减前的值 } // 前置-- Date& Date::operator--() { // 复用 -= 运算符重载函数的代码 *this -= 1; return *this; } // 后置-- Date Date::operator--(int) { Date tmp(*this); // 保存当前对象自减前的值 *this -= 1; // 复用 -= 运算符重载函数的代码 return tmp; // 返回当前对象自减前的值 } // 日期 - 日期 int Date::operator-(const Date& d) { // 判断出大的日期和小的日期 Date max = *this; Date min = d; int flag = 1; // 加一个flag变量来控制天数的正负 if (max < min) { max = d; min = *this; flag = -1; } // 让小的日期累加天数,加了多少次,说明就相差了多少天 int count = 0; while (min != max) { ++min; ++count; } return flag * count; }
七、const 成员
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数,const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中不能对类的任何成员进行修改。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() // void Print(Date* const this) { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } void Print() const // void Print(const Date* const this) { cout << "Print() const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; void Test() { Date d1(2022,1,1); d1.Print(); const Date d2(2023,1,1); // const修饰 d2.Print(); // d2.Print(&d2); // &d2的类型是const Date*,只能读不能写 // 传给第一个Print会导致权限放大为可读可写 }
- const 修饰成员函数是有好处的,这样 const 对象可以调用,非 const 对象也可以调用。
- 但并不是说所有的成员函数都要加 const ,具体得看成员函数的功能,如果成员函数是修改型(比如:operrato+=、Push),那就不能加;如果是只读型(比如:Print、operator+),那就最好加上 const。
- const 成员(只读)函数内不可以调用其它的非 const 成员(可读可写)函数(权限放大),非 const 成员(可读可写)函数内可以调用其它的 const 成员(只读)函数(权限缩小)。
八、取地址及const取地址操作符重载
下面这两个是默认成员函数,一般不用重新定义,不写编译器默认会自动生成。
class Date { public: Date* operator&()// Date* operator&(Date* const this) { return this; } const Date* operator&()const // const Date* operator&(const Date* const this) { return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到这个类型对象的地址。
class Date { public: Date* operator&() { return nullptr; } const Date* operator&() const { return nullptr; } private: int _year; int _month; int _day; };