四、赋值运算符重载(默认成员函数)
1、引入
我们首先来看一个使用场景,我们想要把一个已经初始化的自定义类型的数据赋值给另一个已经初始化的自定义类型(不是对象初始化时赋值,对象初始化时赋值用的是拷贝构造)该怎么办?
看看下面的代码:
//赋值重载 #include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "年" << _month << "月" << _month << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2 = d1;//或者Date d2(d1) 会调用默认生成的拷贝构造,对象初始化时赋值用的是拷贝构造 Date d3; d3 = d1;//我们没有实现Date类的运算符 = 的赋值重载,所以会调用默认生成的赋值重载 //最后d3里面的数据与d1一样 }
2、特性
1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值(=)运算符重载完成赋值。
实例代码:
如上面的代码
2. 赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要符合连续赋值的含义
#include<iostream> using namespace std; class Date { public: Date(int year = 1900, 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; } //自己写的 赋值重载 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; }; int main() { Date d1(2023,2,12); Date d2; d2 = d1; return 0; }
3. 赋值运算符只能重载成类的成员函数不能重载成全局函数
#include<iostream> using namespace std; 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 =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。
此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
4. 如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
和拷贝构造函数一样我们继续思考:既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,对于内置类型还需要自己实现吗?
和拷贝构造一样,特性4也是我们写与不写复制重载函数的判断条件!
例如:
// 这里会发现下面的程序会崩溃掉,编译器生成的是浅拷贝,导致我们析构了两次空间, //这里就需要我们以后讲的深拷贝去解决。 #include<iostream> using namespace std; 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; }
到这里我们就把六个默认成员函数中的第四个:复制重载给讲完了。
复制重载其实是运算符重载的一部分!
五、取地址及const取地址操作符重载
1、取地址操作符重载(默认成员函数)
我们还是先看代码再思考:
#include<iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; cout << &d1 << endl; }
结果符合我们的预期,你可能觉得没有什么值得思考的点。
但是我们说过:对于自定义类型我们不能对他们像对内置类型那样使用运算符,但是我们对Date类的对象 d1 使用了取地址运算符&
,而我们并没有实现&
的运算符重载,结果我们却可以使用&
,而且结果很对。为什么呢?
这是因为第五个默认成员函数:取地址操作符重载,即我们不写,编译器会帮我们自动生成,它的作用就是帮我们实现自定义类型对象的取地址。
取地址重载的手动实现
通常情况下我们一般自己不写此函数,让编译器自动生成。那假设我们自己实现此函数该怎么办呢?
实现代码如下:
//取地址重载函数 #include<iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //取地址重载 Date* operator&() { return this; } private: int _year; int _month; int _day; }; int main() { Date d1; cout << &d1 << endl; }
2、const取地址操作符重载(默认成员函数)
我们定义对象时一般都不会加const
,那我们如果给对象加const
会发生什么?
那我们再看一段代码:
#include<iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2022, 1, 13); d1.Print(); const Date d2(2022, 1, 13); d2.Print(); return 0; }
我们发现无法编译通过
为什么给对象加了const
后我们调用函数就失败了呢?按照加const
报错的常见原因,不难想应该是权限被放大了。
还记得this
指针的类型是什么吗?答案是:* const
类型。这里应该是Date * const
我们用const
修饰的对象取地址后应该是什么类型?答案是:const *
。这里应该是const Date*
两个类型不匹配,const
修饰对象后内容不能被更改,所以我们的this
指针要改变类型,在*
前加一个const
。
但是呢 this
指针是编译器传递的,我们无法加const
,这该怎么办呢?
这里C++编译器又做了特殊化处理我们需要加const
在函数括号后面,才能对this
指针进行修饰
正确代码:
#include<iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() const { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1(2022, 1, 13); d1.Print(); const Date d2(2022, 1, 13); d2.Print(); return 0; }
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
答案:不可以,传递this
指针时权限会放大 - 非const对象可以调用const成员函数吗?
答案:可以,传递this
指针时权限缩小 - const成员函数内可以调用其它的非const成员函数吗?
答案:不可以,传递this
指针时权限会放大 - 非const成员函数内可以调用其它的
const
成员函数吗?
答案:可以,传递this
指针时权限缩小
const取地址重载手动实现
同理在前面的代码中我们取const
类型的地址时没有对&
进行重载,但我们却可以使用,同样是因为编译器自动帮我们实现了const取地址重载。
注意两个不太一样,两个函数构成函数重载!
取地址操作符重载
Date* operator&() //对非 const 对象取地址
const取地址重载
const Date* operator&()const //对 const 对象取地址
手动实现:
//const取地址重载函数 #include<iostream> using namespace std; class Date { public: Date(int year=0, int month=0, int day=0) { _year = year; _month = month; _day = day; } void Print() const { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } const Date* operator&()const //返回值const Date * 是为了与this 指针保持一致 { return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { const Date d1; cout << &d1 << endl; return 0; }
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!