运算符重载
概念概述
- 当运算符被用于类类型的对象时,C++ 语言允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,则会编译报错。
- 运算符重载是具有特殊名字的函数,他的名字是由 operator 和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型和参数列表以及函数体。
- 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
- 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 this 指针,因此运算符重载作为成员函数时,参数比运算对象少一个。
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
- 不能通过连接语法中没有的符号来创建新的操作符:比如 operator@。
- .* :: sizeof ?: . 注意以上 5 个运算符不能重载。(选择题里面常考,大家要记一下)
- 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)。
- 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如 Date 类重载 operator - 就有意义,但是重载 operator * 就没有意义(因为:日期之间的乘法运算在现实生活中没有一个直观且被广泛接受的含义。不像减法运算可以表示两个日期之间的时间间隔,乘法运算很难找到一个符合日期概念的自然解释)。
- 重载 ++ 运算符时,有前置 ++ 和后置 ++,运算符重载函数名都是 operator++,无法很好的区分。C++ 规定,后置 ++ 重载时,增加一个 int 形参,跟前置 ++ 构成函数重载,方便区分。
- 重载 <<(流插入) 和 >>(流提取) 时,需要重载为全局函数,因为重载为成员函数,this 指针默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了对象 <<cout,不符合使用习惯和可读性。重载为全局函数把 ostream/istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。
一元二元三元运算符:
一、一元运算符
- 算术运算符:
- 正号(+):例如
+a
,一般对数值类型起作用,通常意义不大,因为数值默认就是正的。- 负号(-):如
-b
,用于取负值。- 逻辑运算符:
- 逻辑非(!):例如
!c
,对布尔值进行取反操作,如果操作数为true
,则结果为false
;如果操作数为false
,则结果为true
。- 自增自减运算符:
- 前置自增(++a):先将变量的值加 1,然后再使用变量的值。
- 后置自增(a++):先使用变量的值,然后再将变量的值加 1。
- 前置自减(--a):先将变量的值减 1,然后再使用变量的值。
- 后置自减(a--):先使用变量的值,然后再将变量的值减 1。
- 地址运算符:
- 取地址符(&):例如
&d
,用于获取变量的内存地址。- 间接寻址运算符:
- 解引用符(*):如果有一个指针变量
p
,*p
用于访问指针所指向的对象。二、二元运算符
- 算术运算符:
- 加法(+):如
a + b
,用于数值相加或字符串连接(对于 C++ 中的std::string
对象)。- 减法(-):如
a - b
,用于数值相减。- 乘法(*):如
a * b
,用于数值相乘。- 除法(/):如
a / b
,用于数值相除。- 取余(%):如
a % b
,用于求整数除法的余数。- 位运算符:
- 按位与(&):如
a & b
,对两个操作数的每一位进行与操作。- 按位或(|):如
a | b
,对两个操作数的每一位进行或操作。- 按位异或(^):如
a ^ b
,对两个操作数的每一位进行异或操作。- 左移(<<):如
a << b
,将a
的二进制表示向左移动b
位。- 右移(>>):如
a >> b
,将a
的二进制表示向右移动b
位。- 关系运算符:
- 等于(==):如
a == b
,判断两个操作数是否相等。- 不等于(!=):如
a!= b
,判断两个操作数是否不相等。- 大于(>):如
a > b
,判断a
是否大于b
。- 小于(<):如
a < b
,判断a
是否小于b
。- 大于等于(>=):如
a >= b
,判断a
是否大于等于b
。- 小于等于(<=):如
a <= b
,判断a
是否小于等于b
。- 逻辑运算符:
- 逻辑与(&&):如
a && b
,当且仅当两个操作数都为true
时,结果为true
。- 逻辑或(||):如
a || b
,当至少一个操作数为true
时,结果为true
。- 赋值运算符:
- 简单赋值(=):如
a = b
,将b
的值赋给a
。- 复合赋值(如 +=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=):例如
a += b
相当于a = a + b
。三、三元运算符(条件运算符)
条件运算符(?:),例如
a? b : c
,如果a
为true
,则结果为b
;如果a
为false
,则结果为c
。
内置类型和自定义类型
内置类型(简单类型):
一般情况下系统对于内置类型,直接就有对应的指令,会自动识别进行比较,核心就是比较,这里就是i和j直接进行比较 存到ret里面返回
自定义类型(按照你自己的比较方式,按照自己的需求,进行比较):
1,自定义类型不能直接转换成指令,就不能直接进行比较。尤其是像日期类里面的,年 月 日的比较,更是无法直接进行比较,你是需要返回更大的日期,还是更小的,你是需要加减,多少进位一次,这些都不是编译器决定的,是右你自己决定的。编译器不能直接进行识别。
2,内置类型比较简单,可以直接转换识别,但是自定义类型就不是
3,为了可以自己对自定义类型进行比较,语法结构就是:operator ➕运算符
自定义类型运算符重载的使用
原因:
日期类的比较日期,这里会存在一个问题,就是类外面无法访问私有变量,所以这里我们有三种解决办法
- get函数
- 重载为类的成员函数
- 友元函数
这里我们以重载为成员函数为例子进行举例,至于经常用到的友元函数,类的第三章我们会讲到:
这里还有一个关键点,我们直接上代码(这里发现报错)
![]()
原因:可以放到类里面,重载为成员函数,但是直接拷贝放到类里面,会导致报错,因为隐藏了this指针,隐藏的this指针会指向第一个参数
![]()
解决:
但是此时调用,从全局调用,变成了成员函数的调用,调用方式发生了改变
此时d1传给了this,d2传给了d
试验一下 发现没有问题
//.h文件 //运算符重载,比较大小的实现,不在类里面实现 class Date { public: Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给 Date(Date& d); void print(); bool operator<(const Date& d); //比较日期大小(重载为成员函数) private: int _year; int _month; int _day; }; //.cpp实现文件 #include"类和对象的通篇实现.h" //构造函数的实现(全缺省构造函数的实现) Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //拷贝构造的实现 Date::Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } //打印函数的实现 void Date::print() { cout << _year << "/" << _month << "/" << _day << endl; } //日期类的比较日期的大小,this-> < d.year 此时返回 bool Date::operator<(const Date& d) { //比较年 if (this->_year < d._year) { return true; } else { if (this->_year == d._year && this->_month < d._month)//比较月 { return true; } else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日 { return true; } } return false; } //.cpp测试文件 //运算符重载,比较大小 int main() { //构造和拷贝构造函数的使用 Date d1(1000, 2, 1); d1.print(); Date d2 = d1; d2.print(); //比较年月日 Date d3(999, 1, 1); bool ret1 = d1.operator<(d3); cout << ret1 << endl; Date d4(1100, 1, 1); bool ret2 = d1.operator<(d4); cout << ret2 << endl; return 0; }
重载成员函数至少有一个类类型的形参
前置++和后置++的区分
在C++里面,d++和++d,在汇编层,其实都是一样的,都是,operater++,所以是无法是分辨的
所以我们需要在书写的时候做出分辨
这里重载的函数,函数名是一样的
前置++,和后置++,运算符的复用
前置++,直接进行复用
后置++
1,保存之前的数值(拷贝下来)
2,然后进行++
3,最后返回++之前的数值
总结
注意,
在 C++ 中,后置
++
运算符重载函数的参数必须是int
类型,这是 C++ 语言的规定,不能使用double
或其他类型。这样规定的原因主要是为了保持语言的一致性和可识别性。编译器通过参数为
int
来区分前置++
和后置++
的重载版本。如果允许使用其他类型,会使编译器难以确定到底是哪种++
操作,增加了语言的复杂性和不确定性。传递
int
类型参数通常只是作为一个占位符,实际在函数实现中一般不会用到这个参数的值,它的存在仅仅是为了满足语法要求以区分前置和后置自增操作。所以一般情况下,都是默认传递int
类型参数,而不是0
或者1
这样的特定值,并且也不能传递double
等其他类型。代码的实现:
这里声明一下,上面图片的讲解的代码,是实现+=的,所以可以直接使用this+=1,这里并没有实现+=的函数重载,所以这里我们就会跑到对象里面实现day的+1
//.h文件 //运算符重载,比较大小的实现,不在类里面实现 class Date { public: Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给 Date(Date& d); void print(); bool operator<(const Date& d); Date& operator++(); Date& operator++(int); //比较日期大小(重载为成员函数) private: int _year; int _month; int _day; }; //.cpp实现文件 #include"类和对象的通篇实现.h" //构造函数的实现(全缺省构造函数的实现) Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //拷贝构造的实现 Date::Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } //打印函数的实现 void Date::print() { cout << _year << "/" << _month << "/" << _day << endl; } //日期类的比较日期的大小,this-> < d.year 此时返回 bool Date::operator<(const Date& d) { //比较年 if (this->_year < d._year) { return true; } else { if (this->_year == d._year && this->_month < d._month)//比较月 { return true; } else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日 { return true; } } return false; } //前置++ Date& Date::operator++() { this->_day += 1; return *this; } //后置++ Date& Date::operator++(int) { Date tmp = *this; this->_day += 1; return tmp; } //.cpp测试文件 //运算符重载,比较大小 int main() { //构造和拷贝构造函数的使用 Date d1(1000, 2, 1); d1.print(); Date d2 = d1; d2.print(); //比较年月日 Date d3(999, 1, 1); bool ret1 = d1.operator<(d3); cout << ret1 << endl; Date d4(1100, 1, 1); bool ret2 = d1.operator<(d4); cout << ret2 << endl; //前置++和后置++ cout << "前置++和后置++" << endl; d4.print();//1100 ,1,1 d4.operator++(); d4.print(); Date ret = d4.operator++(1); ret.print(); d4.print(); return 0; }
赋值运算符重载
概念概述
赋值运算符重载的特点:
- 成员函数:赋值运算符重载必须定义为类的成员函数。
- 参数:建议将参数声明为
const
类型的类引用,以避免不必要的拷贝。- 返回值:应有返回值,且建议为当前类类型的引用,这样可以支持连续赋值操作,并提高效率。
编译器自动生成的赋值运算符:
- 如果没有显式实现赋值运算符重载,编译器会提供一个默认实现。
- 默认赋值运算符对内置类型成员变量执行值拷贝或浅拷贝。
- 对自定义类型成员变量,会调用其赋值运算符重载函数。
特定情况下的赋值运算符重载:
- 对于像
Date
这样只有内置类型成员的类,编译器自动生成的赋值运算符通常足够使用。- 对于像
Stack
这样包含指向资源的成员的类,需要自定义赋值运算符以实现深拷贝。- 对于像
MyQueue
这样包含自定义类型成员的类,如果这些成员的赋值运算符已经正确实现,通常不需要为MyQueue
显式实现赋值运算符重载。额外技巧:
- 如果一个类显式实现了析构函数并释放资源,通常也需要显式实现赋值运算符重载,以确保资源被正确管理。
赋值运算符重载的使用以及注意事项
1,有返回值,建议写成const类类型的引用。因为C++规定类类型的传值传参,会调用拷贝构造,传递引用会减少拷贝(提高效率)
2,连续赋值的返回值
d3=d1;
返回值是d3,所以我们写代码的时候,要注意返回值,很可能是this
第一次赋值
第二次赋值
3,当返回的节点是d2,如何实现返回d2(用this,this本身指向的就是返回值)(这里注意:有返回值就支持连续赋值,但是返回的时候,需要注意)
注意:这里返回是可以用引用返回的,因为赋值运算符重载是两个已有的对象。出去作用域是依旧存在的(这里代码是传值返回,会增加拷贝)
4,这里还有一个问题,就是d1可以复制給給d1,所以为了防止自己給自己赋值 ,会进行判断
代码实现
//.h文件 //运算符重载,比较大小的实现,不在类里面实现 class Date { public: Date(int year = 1000, int month = 1, int day = 1);//声明给,定义不给 Date(Date& d); void print(); bool operator<(const Date& d); //比较日期大小(重载为成员函数) Date& operator++(); Date& operator++(int); //赋值运算符重载(这里是可以加上const的,因为这里改变的是this不是d) Date& operator=(const Date& d); private: int _year; int _month; int _day; }; //.cpp实现文件 #include"类和对象的通篇实现.h" //构造函数的实现(全缺省构造函数的实现) Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //拷贝构造的实现 Date::Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } //打印函数的实现 void Date::print() { cout << _year << "/" << _month << "/" << _day << endl; } //日期类的比较日期的大小,this-> < d.year 此时返回 bool Date::operator<(const Date& d) { //比较年 if (this->_year < d._year) { return true; } else { if (this->_year == d._year && this->_month < d._month)//比较月 { return true; } else if (this->_year == d._year && this->_month < d._month && this->_day < d._day)//比较日 { return true; } } return false; } //前置++ Date& Date::operator++() { this->_day += 1; return *this; } //后置++ Date& Date::operator++(int) { Date tmp = *this; this->_day += 1; return tmp; } //赋值运算符重载(这里是可以加上const的,因为这里改变的是this不是d) Date& Date::operator=(const Date& d) { //地址不一样才进行赋值 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } //.cpp测试文件 //运算符重载,比较大小 int main() { //构造和拷贝构造函数的使用 Date d1(1000, 2, 1); d1.print(); Date d2 = d1; d2.print(); //比较年月日 Date d3(999, 1, 1); bool ret1 = d1.operator<(d3); cout << ret1 << endl; Date d4(1100, 1, 1); bool ret2 = d1.operator<(d4); cout << ret2 << endl; //前置++和后置++ cout << "前置++和后置++" << endl; d4.print();//1100 ,1,1 d4.operator++(); d4.print(); Date ret = d4.operator++(1); ret.print(); d4.print(); //赋值运算符重载 cout << "赋值运算符重载" << endl; Date d5(1, 1, 1); d5.print(); d5 = d4;//这样写也是对的 //d5.operator=(d4);//这样写也是对的 d5.print(); return 0; }
赋值运算符重载什么时候需要自己实现
注意:默认的赋值运算符重载也会完成浅拷贝(有资源,就需要自己完成赋值运算符重载)
1,内置类型不指向什么资源,不需要自己实现,编译器自动生成的就可以实现
2,栈和深拷贝,都需要自己写,和拷贝构造非常相似
3,如果写了析构函数,那么赋值运算符重载就需要自己写
几个函数之间进行对比
1、构造一般都需要自己写,自己传参定义初始化
2、析构,构造时有资源申请(如malloc或者fopen)等,就需要显示写析构函数
3、拷贝构造和赋值重载,显示写了析构,内部管理资源,就需要显示实现深拷贝