(1) 运算符重载是什么?
(2) 运算符重载有什么作用?(3) 运算符重载怎么实现?
00 引入
C++中的运算符(+,-,*,/,=,==,!= ...)是可以直接作用于内置类型(int/double/short ...)和标准库类型(string:只支持少数运算符)等,但是在默认情况下,运算符无法对自定义的类类型生效,但是在实际开发中,对于许多用户自定义的类型,也需要使用类似的运算操作
如:定义一个复数类,想要实现复数的加法
这个时候,就必须在C++中重新定义这些运算符,赋予已有运算符新的功能,使得运算符能够作用于特定类型(自定义类型)的特定操作(你自己添加功能的运算符),这种重新定义这些运算符的方式叫做运算符重载!
运算符重载:给已经存在的运算符,添加新的功能,使它能够作用于特定的自定义类型(让自定义类型也能够实现运算操作),但是我们不能改变原有语义,操作数个数,结合性和优先级,当然你也不能创造新的运算符
C++中所有的运算符都是使用函数实现的,所以,运算符重载的本质,就是函数重载
true false
所有运算符重载函数定义的一般格式如下:返回值类型 operator<运算符号>(参数列表) {
函数体;
}
返回值类型:运算符本身的结果类型(不要修改运算符的语义)运算本身:你要添加功能的运算符(你要重载的运算符)
参数列表:运算符的操作数
例子:实现复数的相加// 运算符重载函数,增加了“+”的作用,使用“+”能够处理Complex和Complex类型的相加
// x1和x2是引用,是实参的别名,不会调用拷贝构造函数生成形参对象
// x1和x2是非const,不能成为const对象的别名
Complex operator+(Complex &x1, Complex &x2) {
cout << "operator+(Complex &x1, Complex &x2)" << endl;
double r = x1.m_real + x2.m_real;
double i = x1.m_img + x2_img;
return Complex{r, i}; // 返回临时对象
}
// 在实际开发中,一般只会提供这一种,在 + 操作中,不会被加数进行修改的Complex operator+(const Complex &x1, const Complex &x2) {
cout << "const operator + (Complex &x1, Complex &x2) << endl";
double r = x1.m_real + x2.m_real;
double i = x1.m_img + x2.m_img;
return Complex{r, i}; // 返回临时对象
}
有const修饰的参数和没有const修饰的参数是两个不同的类型,所以上面两个版本的函数是重载重载 friend Complex operator+(Complex &x1, Complex &x2); friend Complex operator+(const Complex &x1, const Complex &x2); 二义性 friend Complex operator+(Complex x1, Complex x2); friend Complex operator+(const Complex &x1, const Complex &x2);
当你把运算符处理为运算符函数之后,编译器会根据上下文语义自动匹配相应的运算符函数
#include <iostream> using namespace std; // 自定义的复数类 class Complex { friend Complex operator+(Complex &x1, Complex &x2); friend Complex operator+(const Complex &x1, const Complex &x2); friend Complex operator+(const Complex &x1, double d); friend Complex operator+(double d, const Complex &x1); private: double _real; // 实部 double _img; // 虚部 public: Complex(double r, double i) : _real{r}, _img{i} { cout << "构造函数" << endl; } ~Complex() { cout << "析构函数" << endl; } Complex(const Complex &oth) { cout << "拷贝构造函数" << endl; this->_real = oth._real; this->_img = oth._img; } // void show(const Complex *const this); void show() const { cout << "const:Complex(" << _real << "," << _img << ")" << endl; } // void show(Complex *const this); void show() { cout << "Complex(" << _real << "," << _img << ")" << endl; } }; // 运算符重载函数,增加了"+"的作用,使得"+"能够处理Complex和Complex类型的相加 // x1和x2是引用,是实参的别名,不会调用拷贝构造函数生成形参对象 // x1和x2是非const,不能成为const对象的别名 Complex operator+(Complex &x1, Complex &x2) { cout << "operator+(Complex &x1, Complex &x2)" << endl; double r = x1._real + x2._real; double i = x1._img + x2._img; return Complex{r, i}; // 返回临时对象 } // 在实际开发中,一般只会提供这一种,在+操作中,不会被加数进行修改的 Complex operator+(const Complex &x1, const Complex &x2) { cout << "operator+(const Complex &x1, const Complex &x2)" << endl; double r = x1._real + x2._real; double i = x1._img + x2._img; return Complex{r, i}; // 返回临时对象 } Complex operator+(const Complex &x1, double d) { cout << "operator+(const Complex &x1, double d)" << endl; double r = x1._real + d; double i = x1._img; return Complex{r,i}; // 返回临时对象 } Complex operator+(double d, const Complex &x1) { cout << "operator+(double d, const Complex &x1)" << endl; double r = x1._real + d; double i = x1._img; return Complex{r, i}; // 返回临时对象 } int main() { Complex c1{1.1, 1.2}; Complex c2{2.4, 3.6}; Complex c3 = c1 + c2; // 调用运算符函数 // Complex c3 = operator+(c1, c2); // 运算符函数也可以像普通函数一样,正常调用 c3.show(); Complex c4 = c1 + 10; // 调用运算符函数 // Complex c4 = operator+(c1, 10); // 运算符函数也可以像普通函数一样,正常调用 c4.show(); Complex c5 = 10 + c1; // 调用运算符函数 // Complex c5 = operator+(10, c1); // 运算符函数也可以像普通函数一样,正常调用 c5.show(); return 0; }
练习:
实现两个复数能够相减
#include <iostream> using namespace std; // 自定义的复数类 class Complex { friend Complex operator-(const Complex &x1, const Complex &x2); friend Complex operator-(const Complex &x1, double d); friend Complex operator-(double d, const Complex &x1); private: double _real; // 实部 double _img; // 虚部 public: Complex(double r, double i) : _real{r}, _img{i} { cout << "构造函数" << endl; } ~Complex() { cout << "析构函数" << endl; } Complex(const Complex &oth) { cout << "拷贝构造函数" << endl; this->_real = oth._real; this->_img = oth._img; } // void show(const Complex *const this); void show() const { cout << "const:Complex(" << _real << "," << _img << ")" << endl; } // void show(Complex *const this); void show() { cout << "Complex(" << _real << "," << _img << ")" << endl; } }; Complex operator-(const Complex &x1, const Complex &x2) { cout << "operator-(const Complex &x1, const Complex &x2)" << endl; double r = x1._real - x2._real; double i = x1._img - x2._img; return Complex{r, i}; // 返回临时对象 } Complex operator-(const Complex &x1, double d) { cout << "operator-(const Complex &x1, double d)" << endl; double r = x1._real - d; double i = x1._img; return Complex{r, i}; // 返回临时对象 } Complex operator-(double d, const Complex &x1) { cout << "operator-(double d, const Complex &x1)" << endl; double r = d - x1._real; double i = x1._img; return Complex{r, i}; // 返回临时对象 } int main() { Complex c1{1.1, 1.2}; Complex c2{2.4, 3.6}; Complex c3 = c1 - c2; // 调用运算符函数 // Complex c3 = operator-(c1, c2); // 运算符函数也可以像普通函数一样,正常调用 c3.show(); Complex c4 = c1 - 10; // 调用运算符函数 // Complex c4 = operator-(c1, 10); // 运算符函数也可以像普通函数一样,正常调用 c4.show(); Complex c5 = 10 - c1; // 调用运算符函数 // Complex c5 = operator-(10, c1); // 运算符函数也可以像普通函数一样,正常调用 c5.show(); return 0; }
01 基本的双目运算符(全局(友元)函数,类的成员函数)
典型的双目运算符重载的方式类似,包括如下:
+ - * / % ......算术运算符
< <= > >= == != ......关系运算符
|| && 逻辑运算符
| & 位运算
这些运算符既可以重载为全局(友元)函数,又可以重载为类的成员函数
如果重载为全局的友元函数,则双目运算符有两个参数,编译器根据函数名和参数列表匹配相应的函数
如:Complex operator+(const Complex &c1, const Complex &x2) {
cout << "operator+(const Complex &c1, const Complex &x2)" << endl;
double r = c1._real + c2._real;
double i = c1._img + c2._img;
return Complex{r, i}; // 返回临时对象
}
典型的双目运算符重载为全局的函数方式和上面类似!!!
如果重载为类的非静态成员函数,则双目运算符只有一个参数,还有一个参数呢?类的非静态成员函数中,默认有一个 this 指针,可以表示一个参数
c1 + c2 ------> c1.operator+(c2) ------> operator+(&c1, c2)
c1调用自己的成员函数(operator+),参数为c2,同时把c1的地址作为this传递出去
因为类的非静态成员函数必须通过对象去调用,编译器默认使用运算符左侧的对象调用该运算符函数,隐式的通过this把左侧的操作数对象传递到函数内部,把运算符右侧的操作数作为函数的参数, 所以重载为类的非静态成员函数,则双目运算符只有一个参数
形式如下:
// 把运算符重载为成员函数的形式
// Complex operator+(Complex *const this, const Complex &x)
Complex operator+(const Complex &x) {
cout << "成员函数:operator+(const Complex &x)" << endl;
double r = this->_real + x._real;
double i = this->_img + x._img;
return Complex{r, i}; // 返回临时对象
}
Complex c3 = c1 + c2;
Complex c3 = c1.operator+(c2); // 也可以直接像普通的成员函数一样调用
既重载为成员函数,又重载为友元函数,会调用哪一个呢?会报错,有二义性错误!!!
const 对象只能调用 const 函数,但是非const 对象,既可以调用非const 函数又可以调用 const 函数
const 对象只能被const 指针/引用绑定,但是非const 对象,既可以被const 引用/指针绑定,又可以被const 指针/引用绑定
成员函数使用const修饰,实际上是修饰了成员函数的this指针
上面的运算符重载为友元函数好函数成员函数好?基本的双目运算符,最好重载为友元函数,因为类的成员函数默认会选择第一个操作数调用成员函数,第二个操作数作为参数(第一个操作数,必须是自定义的类类型)
所以,函数的参数会比友元函数少一个,无法实现下面的操作:只有全局函数可以实现
Complex c2 = 10.1 + c1;
Complex operator+(double x, const Complex & c) {
......
}
都可以实现:
Complex c2 = c1 + 10.1;
全局友元函数:
Complex operator+(const Complex &c, double x) {
......
}
成员函数:
Complex operator+(double x) {
......
}
练习:写一个圆类,可以实现两个圆的:+ - > < ==
02 输入输出运算符(<<,>>)(只能重载为全局的友元函数)
在C++中,左移运算符 << 可以和 cout (ostream类的一个全局对象)一起用于输出,也经常被称为"流插入运算符",或者"输出运算符",但是实际上,<< 本来没有这样的功能,之所以能够和 cout 一起使用,是因为 ostream类重载了 <<运算符,给<<赋予了很多新的功能(可以输出各种各样的基本类型)
cout 是 ostream类的对象,ostream类和cout都是在iostream中声明的,ostream类将<<重载为成员函数而且重载了很多次
如:int x;
cout << x;
编译器默认使用运算符左侧的对象调用该运算符函数(把cout的地址当做 this指针传入函数内部)
把运算符右侧的操作数当成函数的参数
======>
cout.operator<<(x); =====> operator<<(&cout, x);
为了同时能够让cout<<"hello"这样的语句能够成立ostream类需要将<<进行以下重载
ostream & ostream::operator<<(const char *s) {
输出s的具体代码
return *this;// *this就表示cout本身
}
为了同时能够让cout<<50这样的语句能够成立ostream类需要将<<进行以下重载
ostream & ostream::operator<<(int n) {
输出n的具体代码
return *this;// *this就表示cout本身
}
为什么需要返回值,而且是返回引用?
第一:为什么返回(*this)?
*this在成员函数中,表示的就是调用者对象本身!!!
cout << x << y;
(cout.operator<<(x)).operator<<(y);
如果没有返回值(void):void.operator<<(y); void有operator<<成员函数吗?
所以必须返回cout对象:cout.operator<<(y);
目的是让cout<<可以实现连续输出,使得cout << "hello" << 50 这样的代码能够成立!!!
第二:为什么返回引用?
引用目的是避免调用拷贝构造函数产生临时对象!
basic_ostream(const basic_ostream &) = delete;
IO对象无拷贝(拷贝构造函数被删除了),输入输出对象(cout/cin)作为函数参数和返回值的时候,必须是引用
说明:
一个类在默认情况下,会产生一些函数,如:构造函数,拷贝构造函数,析构函数等
如果不需要编译器默认生成某些函数,只需要在该函数的后面加上一个 =delete 即可
如:
ostream类不需要拷贝构造函数
basic_ostream(const basic_ostream&) = delete;
如果需要编译器自动生成某些函数,只需要在该函数的后面加上一个 =default 即可
如:
日期类只有一个带参数的构造函数
Date() = default; // 编译器就会生成默认的构造函数,函数体为空
注意:输入对象(cin)的成员函数(>>)和输出对象(cout)的成员函数(<<)是类似的
而且我们在重载(<<,>>)的时候,尽量不要在operator<<函数中添加无关的功能!!!
输入输出运算符只能重载为全局的友元函数,不能重载为类的成员函数Date d;
cout << d;
如果重载为成员函数,是谁的成员?
编译器默认使用运算符左侧的操作数对象调用这个运算符函数,把运算符右侧的操作数当成是函数的参数
cout.operator<<(d);
需要 cout 对象本身存在这个函数 operator<<(const Date &d); cout 是ostream类的对象
我们不能修改 ostream类和 istream类,所以我们只能将 << 和 >> 重载为全局函数的形式
由于这两个函数一般需要访问Date类的私有成员,因此我们需要在类的定义中声明这两个函数为友元函数
练习:假设d是Date类的对象,现在希望写如下的代码可以编译通过
1. cout << d 就能够以 Date(y, m, d)的方式输出日期
2. cin >> d 就能够直接从键盘接收三个整数y,m,d,使得:
d._year = y;
d._month = m;
d._day = d;
// 重载为全局函数,为了使cout << d << endl成立,返回值必须是一个ostream类的引用
// out 就是cout 的别名,等价于cout,cout 不能是值传递(std::ostream),因为 IO对象无拷贝(拷贝构造函数被删除了)
// 参数d也最好是常引用,可以避免产生临时对象,加上const,就可以接收const参数
std::ostream & operator<<(std::ostream &out,const Date &d) {
out << "Date(" << d._year << "-" << d._mon << "-" << d._day << ")";
return out;
}
// IO对象无拷贝,返回值和istream参数必须是引用
// 此处的d不能是const,且必须是引用,因为需要在函数内部去修改d对象
istream & operator>>(std::istream &in,Date &d) {
int year, mon, day;
in >> year >> mon >> day;
if (in) { // 如果接收参数成功
d._year = year;
d._mon = mon;
d._day = day;
} else {
throw std::runtime_error{"cin Date error~"};
}
return in;
}
03 赋值运算符重载(拷贝赋值)(只能重载为类的非静态成员函数)
赋值?
Date d1{2024, 11, 14}; // 调用构造函数
Date d2 = d1; // 不是赋值,调用拷贝构造函数
--------------------------------------------------------------
Date d1{2024, 11, 14}; // 调用构造函数
Date d2; // 会调用无参构造函数
d2 = d1; // 赋值操作,赋值前,d2就已经存在了
自己写的Date类中没有重载=,也是可以赋值的
当程序中没有提供一个以本类对象或者本类对象引用为参数的赋值运算符重载时,编译器会自动的生成一个默认的赋值运算符重载函数,执行浅拷贝工作(逐成员的赋值)
因为浅拷贝很危险(造成多个对象共享同一份资源),所以,当成员变量中有指针或者开辟了资源的时候,一定需要自己实现赋值运算符重载函数
大概形式:类名 & operator=(const 类名 &oth) {
......
}
s2 = s1;
s2.operator=(s1); =====> operator=(&s2, s1);
注意:函数返回值类型 必须是引用(类名 &),不能是类名:
1. 提高效率
2. (c1 = c2) = c3; // 函数调用做左值,它的返回类型必须是左值引用,临时对象只能做右值,不能做左值
C++规定,赋值运算符重载函数,只能重载为类的非静态成员函数,不能是静态函数,也不能是友元函数
为什么不能是静态成员函数?
因为静态成员函数没有this指针
静态成员函数只能操作静态成员,不能操作非静态成员(一个类不可能只有静态成员)
为什么不能是友元函数?
当程序中没有提供一个以本类对象或者本类对象引用为参数的赋值运算符重载时,编译器会自动的生成一个默认的赋值运算符重载函数,执行浅拷贝工作
如果C++把赋值运算符重载为友元函数,而且以本类的引用为参数
Student & operator=(Student &s1, const Student &s2);
因为友元函数不属于这个类,所以编译器同样会自动生成一个默认的赋值运算符重载函数
当执行赋值操作的时候,编译器就会产生二义性错误
为了避免这样的二义性,C++规定,赋值运算符重载函数只能重载为类的非静态成员函数
赋值运算符重载的实现步骤:1. 防止自赋值(自己给自己赋值) —— 一般使用比较地址是否相同来判断是否为自己给自己赋值
a. 如果类成员对象中含有指针,就会发生不可预知的后果(会先释放被赋值对象拥有的资源,防止资源泄漏)
b. 为了效率,自己给自己赋值是毫无意义的
2. 释放被赋值对象拥有的资源,防止资源泄漏
3. 分配新资源被赋值对象应该拥有自己的资源
4. 拷贝内容
两个对象拥有的资源中的内容应该“相同”
5. 返回自引用
可以避免函数返回时产生的临时对象,提高效率
(c1 = c2) = c3; // 函数调用做左值,它的返回类型必须是左值引用,临时对象只能做右值,不能做左值
可以实现连续赋值
赋值运算符重载函数,不是必须要自己实现的,当对象中没有任何指针和资源的时候
浅拷贝就已经可以完成功能了
关于赋值运算符的参数:本类类型的 常 引用(两个条件都不是强制的)
const 的原因:
我们本来就不希望在这个函数中对用来赋值的"赋值"对象进行改动
假设设置为const,对于const 和非const 的实参,函数都可以接受
如果不设置const,函数只能接受非const 的实参("赋值"对象只能是非const)
引用的原因:
可以避免函数调用时对实参进行拷贝构造,提高效率
赋值运算符重载函数,不可以使用const 修饰吗Student & operator=(const Student & s) const; // 错误
======>
Student & operator=(const Student * const this, const Student &s);
ERROR,因为我们必须拥有被赋值对象的修改权限!!!
练习:尝试自己为student 类写一个赋值运算符重载函数,让代码正确运行
// 赋值运算符重载函数 // oth就是用来被复制的对象的别名 // s = s1 = s2 ====> s = (s1 = s2) Student & operator=(const Student &oth) { // 不能用const修饰 cout << "赋值运算符重载!" << endl; // 防止自赋值 if (this == &oth) { // 比较地址是否相等 return *this; // 返回本身 } m_age = oth.m_age; // 释放被赋值对象的空间 delete [] m_name; // 开辟新空间 m_name = new char[strlen(oth.m_name) + 1]; // 把赋值对象的数据赋值给当前对象 strcpy(m_name, oth.m_name); // 返回自引用 return *this; }
04 单目运算符(全局(友元)函数,类的成员函数)
++,--
单目运算符分为前置和后置,在运算符重载的时候,为了区分前置和后置
Date d; // 2024-11-14Date d1 = d++; // d1:2024-11-14 d:2024-11-15
Date d2 = ++d; // d2:2024-11-15 d:2024-11-15
如何区分前置和后置呢?
使用了一个int 的占位参数(只占用一个参数位置,起到一个标识作用,没有其他的实际意义)
后置++操作符需要一个额外的 int类型 占位操作
前置++操作数不需要额外的操作
可以重载为友元函数,也可以重载为成员函数(还是属于算术运算符)友元函数: 返回值类型 & operator++(类型 &); // 没有占位参数,是前置++ 返回值类型 & operator++(类型 &, int); // 后置++,int仅仅是一个占位参数 成员函数: 返回值类型 operator++(); // 没有占位参数,是前置++ 返回值类型 operator++(int); // 后置++,int仅仅是一个占位参数
在使用类类型的自增/自减操作时,应该使用前置还是后置,为什么?
使用前置++,--,效率比较高,因为不产生临时对象!!!
练习:通过一个时间类验证单目运算符的重载
05 下标运算符[ ](只能重载为类的非静态成员函数,一般会重载两个版本)
必须以成员函数的形式进行重载,下标运算符只和当前类有关系
该函数在类中的重载形式如下:返回值类型 & operator[](参数);
and
const 返回值类型 & operator[](参数) const;
在实际开发中,我们应该同时提供上面两种重载方式,是为了适应const对象
第一种方式,[]不仅仅可以访问元素,还可以修改元素
string s1 = "hello";
cout << s1[0] << endl;
s1[0] = 'H';第二种方式,[]仅仅可以访问元素,不可以修改元素
const string s1 = "hello";
cout << s1[0] << endl;
如果不提供第二种方式的重载,那么const对象就不能通过[]访问成员
参数:int类型的数据,表示你要访问哪一个元素(index)
06 函数调用运算符()(类的成员函数)
如果一个类重载了函数调用运算符,这种类型的对象,可称之为 函数对象(仿函数)
函数对象的行为 类似于 函数,可以调用
例:class Demo {
public:
void operator()() {
// ......
}
void operator()(int x) {
// ......
}
};
Demo d1; // d1就可称为 函数对象
d1(); // 调用d1对象,就是在调用 operator()()
d1(100); // 调用d1对象,就是在调用 operator()(int)
#include <iostream> using namespace std; class Print { private: char sep; public: Print(char c = ' ') : sep(c) { cout << "构造函数" << endl; } Print(const Print &p) : sep(p.sep) { cout << "拷贝构造函数" << endl; } void setSep(char c) { this->sep = c; } void operator()(int x) const { cout << dec << x << sep; // 十进制 } }; void p1(int x) { cout << x << ' '; } void p2(int x) { cout << hex << x << ','; // 十六进制 } void p3(int x) { cout << oct << x << '/'; // 八进制 } void print_array(int a[], int n, void (*print)(int)) { for (int i = 0; i < n; i++) { print(a[i]); } cout << endl; } void print_array(int a[], int n, Print print) { for (int i = 0; i < n; i++) { print(a[i]); } cout << endl; } // 自定义的条件,用于判断给定的值是否为3的倍数 bool f1(int x) { return x % 3 == 0; } int find_array(int *a, int n, bool (*print)(int)) { for (int i = 0; i < n; i++) { if (print(a[i])) { return i; } } return -1; } class Demo { public: bool operator()(int x) { return x % 3 == 0; } }; int find_array(int *a, int n, Demo d) { for (int i = 0; i < n; i++) { if (d(a[i])) { return i; } } return -1; } int main(int argc, char *argv[]) { int a[5] = {100, 101, 102, 103, 104}; print_array(a, 5, p1); print_array(a, 5, p2); print_array(a, 5, p3); cout << "=====================" << endl; Print print; print_array(a, 5, print); print.setSep('*'); print_array(a, 5, print); cout << "=====================" << endl; // 临时对象作实参传递 print_array(a, 5, Print('-')); print_array(a, 5, Print('%')); cout << "=====================" << endl; // 从数组中查找满足条件的第一个元素,找到返回其下标,否则返回-1 int index = find_array(a, 5, f1); if (index != -1) { cout << "index = " << index << endl; } index = find_array(a, 5, Demo{}); cout << "index = " << index << endl; return 0; }
07 运算符小结
运算符重载的格式:
返回值类型 operator<运算符>(函数参数) {
函数体;
}
既可以重载为成员函数,又可以重载为友元函数
重载为全局的友元函数:参数个数个运算符的操作数个数相同
重载为成员函数:参数个数比运算符的操作数个数少一个
1. 运算符重载的本质,就是函数重载,目的是让自己定义的类型也能够使用运算符操作
2. 只能重载大多数运算符,不能创造新的运算符
3. 可以重载大多数运算符,但是以下运算符不能重载
(1) 取成员运算符 "."(2) 成员对象指针 ".*" "->*"
(3) 作用域运算符 "::"
(4) 条件运算符 "?:"
(5) sizeof
4. 运算符函数由编译器自动调用,但是也可以像普通函数一样去调用它
5. 重载为成员函数还是友元函数
= () [] -> 必须重载为成员函数<< >> 必须重载为友元函数
6. 只有当操作数中至少有一个是自定义类型的对象的时候,才需要运算符重载