【C++类和对象】拷贝构造与赋值运算符重载(上):https://developer.aliyun.com/article/1496868
2.赋值运算符重载
2.1运算符重载
在学习赋值运算符重载之前我们先来学习以下运算符重载;
首先运算符是一种特殊的符号,用于表示特定的操作或运算。在C++中,运算符可以分为以下几类:
1.算术运算符:用于执行基本的数学运算,包括加法 (+)、减法 (-)、乘法 (*)、除法 (/)、取余 (%)等。
2.关系运算符:用于比较两个值的关系,包括等于 (==)、不等于 (!=)、大于 (>)、小于 (<)、大于等于 (>=)、小于等于 (<=)等。
3..逻辑运算符:用于对布尔值进行操作,包括逻辑与 (&&)、逻辑或 (||)、逻辑非 (!)等。
4.赋值运算符:用于将右操作数的值赋给左操作数,包括赋值 (=)、加等于 (+=)、减等于 (-=)等。
5.位运算符:用于对二进制位进行操作,包括按位与 (&)、按位或 (|)、按位取反 (~)、按位异或 (^)等。
6.条件运算符:也称为三元运算符,用于根据条件选择不同的值,形式为 条件 ? 值1 : 值2。
7.成员运算符:用于访问类和结构体的成员,包括成员访问符 (.)和成员指针访问符 (->)。
8.索引运算符:用于访问数组、容器等集合类型的元素,形式为 数组名[索引]。
9.函数调用运算符:用于调用重载了函数调用运算符的类对象的函数,形式为 对象名()。
10.类型转换运算符:用于将一个类型转换为另一个类型,包括显式转换运算符和隐式转换运算符。
以上的运算符都是针对自定义类型所进行的操作比如:int、double等类型,在C++中,我们可以重载赋值运算符(类似于自己重新定义运算符,当然自己定义的运算符只针对自定义类型),使其适应自定义的数据类型。比如进行日期类的+ -,判断是否相等==;
class Date { private: int _year; int _month; int _day; }; int main() { Date d1; Date d2; d1 == d2;//我们想要进行日期类的对象进行判断是否相等就需要对运算符进行重载 return 0; }
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
返回值类型 operator<运算符号> (const 类型& 变量名)
例如:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } private: int _year; int _month; int _day; };
但是我们发现上面的代码编译不通过:
函数的参数太多?这是因为运算符重载函数作为类成员函数重载时,其形参要看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this,所以上面的函数只需要给一个参数即可:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //bool operator==(const Date& d1, const Date& d2) bool operator==(const Date& d)//隐藏的this指向d1,d是d2的引用 { return this->_year == d._year && this->_month == d._month && this->_day == d._day; } private: int _year; int _month; int _day; };
注意:
- 运算符重载函数需要定义为类的成员函数或者全局函数,具体的重载方式取决于运算符的操作数类型。
- 不能通过连接其他(不是运算符的)符号来创建新的操作符:比如operator@ 。
- 重载操作符必须有一个类类型参数(不能全是内置类型,不然就不是重载运算符了)。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
- ( .* )( :: )( sizeof )( ?:)( .) 注意以上5个运算符不能重载。
2.2赋值运算符重载
赋值运算符重载属于运算符重载的一种
1.赋值运算符重载格式:
返回类型 operator=(const 类型名& 右操作数) { // 赋值操作的实现 // 将右操作数的值赋给左操作数 // 返回左操作数的引用 return *this; }
参数类型:const 类型名&,传引用传参可以提高传参效率
返回值类型:类型名&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要符合赋值的含义
例如:
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; };
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.默认生成的赋值运算符重载
- 在C++类和对象中用户没有显式实现赋值运算符重载时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝;
- 注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。这和之前学的默认构造函数与默认生成的析构函数类似;
例如:
class Time //自定义类型Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time& operator=(const Time& t)//赋值运算符重载 { cout << "Time& operator=" << endl; 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 = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2; d1 = d2; return 0; }
结果如下:
从上述例子中可以发现在Date类中我们没有显式实现赋值运算符重载,它默认生成了一个赋值运算符重载,对于内置类型直接以字节的方式进行浅拷贝,对于自定义类型Time会去调用它的赋值运算符重载;
对于赋值运算符重载既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,那还需要自己实现吗?
这和我们上面学习的拷贝构造函数类似,像日期类这样的类是没必要的自己显式实现。那么下面的栈类呢?
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。 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; }
结果如下:
我们发现这和我们之前学习的拷贝构造函数非常相似,这里程序崩溃的原因也在于浅拷贝;
- s2与s1指向了同一块空间,在s1和s2生命周期结束时都会自动调用析构函数销毁空间,就相当于一块空间被释放了两次,程序当然会崩溃;
- 此外赋值运算符重载还有当s2创建时调用构造函数开辟了空间,当s1赋值给s2,s2原来的空间就会丢失造成内存泄漏;
图示如下:
所以和拷贝构造类似如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要自主实现赋值运算符。
3.结语
对于C++类和对象的拷贝构造函数与运算符重载它们一个是在创建对象时使用另一个创建好的对象来进行赋值(拷贝构造),另一个则是在两个已经创建好的对象之间进行赋值(赋值运算符重载);
此外它们两个如果没有在类中显式实现编译器都会默认生成对应的函数,而此时默认生成的函数对于内置类型会进行浅拷贝,对于自定义类型则会调用它的拷贝构造函数或赋值运算符重载;
所以如果是简单的日期类,类中未涉及到资源管理,就可以使用编译器默认生成的函数,对于类含有指针或动态分配的资源比如栈类就不能依靠编译器要自己显式实现对应的函数;以上就是C++类和对象拷贝构造与赋值运算符重载所有的内容啦~ 完结撒花 ~🥳🎉🎉