一、运算符重载的基本概念
C++中的运算符重载允许用户对已有的运算符进行重新定义,使得用户自定义类型的对象能够像内置类型一样使用运算符进行运算。
1.运算符重载的定义
定义:运算符重载是对已有运算符的重新定义,使其能够处理自定义类型的数据。
本质:运算符重载实际上是通过定义一个特殊的函数来实现的,其本质为函数重载。
语法:返回值类型 operator操作符(参数列表) { }
2.运算符重载的位置
①运算符被重载为普通全局函数:
单目运算符重载时只有一个参数,它的操作数作为形参。
双目运算符重载时有两个参数,左边形参作为运算符的左操作数,右边形参作为右操作数。
②运算符被重载为类的成员函数:
单目运算符不需要写参数,双目运算符只需要写一个参数。
因为类的成员函数默认有一个this指针,它直接指向类本身,限定为第一个形参。
3.运算符重载的设计原则
在设计类时,应该首先考虑类提供哪些操作,然后决定是否需要重载运算符。
对于改变对象状态的运算符(如自增、自减、解引用等)通常作为成员函数重载;而算术、关系和逻辑运算符通常作为非成员函数重载。此外,= () [] -> 只能作为成员函数重载,输入/输出运算符一般作为非成员函数重载。
重载运算符的返回类型应与内置运算符的返回类型兼容,例如算术运算符返回新创建对象。
4.注意事项
- 重载后的运算符必须至少有一个操作数是用户定义的类型,防止用户为标准类型重载运算符。
- 不能创建新的运算符,只能对已有的运算符进行重载。
- 使用运算符时不能违反运算符原来的句法规则。
- 重载后运算符的功能应该与运算符的实际意义相符,不要滥用运算符重载。
- 不能修改运算符的优先级,重载后运算符的运算顺序不变。
- 不能重载这五个运算符:sizeof . .* :: ?:
二、运算符重载分类
1.基本算术运算符重载
可以重载基本算术运算符来定义对象之间的算术操作。
基本算术运算符:加(+)、减(-)、乘(*)、除(/)、取模(%)
返回值:通常返回一个新创建的对象,这允许支持链式操作。
①加号作为成员函数重载:
class Time {
private:
int hour;
int minute;
int second;
public:
Time(int h = 0, int m = 0, int s = 0)
:hour(h), minute(m), second(s) {}
Time operator+(const Time& t) {
int second = this->second + t.second;
int minute = this->minute + t.minute + second / 60;
int hour = this->hour + t.hour + minute / 60;
second %= 60;
minute %= 60;
hour %= 24;
return Time(hour, minute, second);
}
void printTime() const {
cout<< setw(2) << setfill('0') << hour << ":"
<< setw(2) << setfill('0') << minute << ":"
<< setw(2) << setfill('0') << second << endl;
}
};
Time t1(1, 20, 30);
Time t2(4, 15, 40);
Time ret = t1 + t2;
ret.printTime();
//输出05:36:10
②加号作为非成员函数重载:
class Time {
friend Time operator+(const Time& t1, const Time& t2);
private:
int hour;
int minute;
int second;
public:
Time(int h = 0, int m = 0, int s = 0)
:hour(h), minute(m), second(s) {}
void printTime() const {
cout<< setw(2) << setfill('0') << hour << ":"
<< setw(2) << setfill('0') << minute << ":"
<< setw(2) << setfill('0') << second << endl;
}
};
Time operator+(const Time& t1, const Time& t2) {
Time t;
t.second = t1.second + t2.second;
t.minute = t1.minute + t2.minute + t.second / 60;
t.hour = t1.hour + t2.hour + t.minute / 60;
t.second %= 60;
t.minute %= 60;
t.hour %= 24;
return t;
}
Time t1(10, 20, 33);
Time t2(14, 15, 40);
Time t3(2, 5, 12);
Time ret = t1 + t2 + t3;
ret.printTime();
//输出02:41:25
2.关系运算符重载
可以重载关系运算符让对象之间进行比较操作。
关系运算符: == != > < >= <=
返回值:通常是bool类型,以表示比较的结果。
①小于号作为成员函数重载:
class Rectangle {
private:
int width;
int height;
public:
Rectangle(int w = 0, int h = 0) :width(w), height(h) {}
int area() const {
return width * height;
}
bool operator<(const Rectangle& rectangle) {
return this->area() < rectangle.area();
}
};
②作为非成员函数重载形式:
bool operator<(const Rectangle& r1, const Rectangle& r2) {
return r1.area() < r2.area();
}
Rectangle r1(3, 4);
Rectangle r2(5, 5);
if (r1 < r2) {
cout << "r1面积小于r2" << endl;
}
//输出
r1面积小于r2
3.自增/自减运算符重载
可以重载前置或后置的自增/自减运算符来定义对象的自增/自减行为。为了区分,通常后置版本比前置版本多一个占位参数int。
前置版本:返回对象的引用,因为它修改了对象本身。
后置版本:返回对象的副本,因为它需要保留原始状态以供后续使用。
class Counter {
public:
int cnt;
Counter(int count = 0) :cnt(count) {}
//重载前置++运算符
Counter& operator++() {
cnt++;
return *this;
}
//重载后置++运算符
Counter operator++(int) {
Counter counter = *this;
cnt++;
return counter;
}
};
Counter c1;
cout << c1++.cnt << endl; //0
cout << c1.cnt << endl; //1
Counter c2;
cout << ++c2.cnt << endl; //1
cout << c2.cnt << endl; //1
4.输入/输出运算符重载
可以通过重载<<和>>运算符来定义类的输入和输出行为。
只能使用全局函数来重载<<运算符。因为如果使用成员函数重载,只能用"x<<cout;"的方式输出,无法让cout在左边,所以要作为友元函数实现。
重载输出运算符:
class Point {
private:
double P_x, P_y;
public:
Point(double x = 0, double y = 0) :P_x(x), P_y(y) {};
friend ostream& operator<<(ostream& os, const Point& p) {
os << "(" << p.P_x << "," << p.P_y << ")";
return os;
}
};
Point p1(3, 4);
Point p2(5, 5);
cout << p1 << p2 << endl;
//输出(3,4)(5,5)
输出运算符重载时,ostream对象不使用const引用是因为输出操作需要修改输出流的状态,而且输出流对象不支持复制,所以必须按引用传递。
返回引用使输出运算符支持链式操作,即可以连续对同一个输出流对象执行多次输出操作。此外,返回引用确保在一系列输出操作中,输出流对象的状态可以保持一致,直到所有操作完成。
5.赋值运算符重载
赋值运算符用于将一个已存在对象的状态赋值给另一个对象。
①拷贝构造函数和赋值运算符的区别:
拷贝构造函数用于对象的创建和初始化,而赋值运算符用于对象状态的更新。
②赋值运算符中的浅拷贝与深拷贝问题:
如果一个类没有显式地进行赋值运算符重载,编译器会自动生成一个默认的赋值运算符。
如果类中有指针成员并且指针指向堆区,那么在进行赋值操作时可能会出现浅拷贝问题。此时就需要重载赋值运算符,实现深拷贝。
举例:
class Myclass {
private:
int* value;
public:
Myclass(int value = 0) {
this->value = new int(value);
}
~Myclass() {
delete value;
}
Myclass& operator=(const Myclass& myc) {
//value = myc.value; //默认的浅拷贝操作
if (this != &myc) {
delete value;
value = new int(*myc.value);
}
return *this;
}
void print() {
cout << *value << endl;
}
};
Myclass m1(1);
Myclass m2(2);
Myclass m3(3);
m1 = m2 = m3;
m1.print(); //3
m2.print(); //3
m3.print(); //3
如果在赋值过程中先释放了资源(如删除动态分配的内存),然后又从同一个对象中尝试读取或使用资源,就可能导致未定义行为。重载复制运算符时,通过自赋值检查,可以避免潜在的错误。
在开辟新内存之前,需要释放当前对象持有的资源,以防止内存泄漏。此外,赋值运算符通常返回当前对象的引用,以便支持链式赋值。如果按值返回的话,则会调用拷贝构造函数。
6.函数调用运算符重载
C++中的仿函数是指那些重载了operator()的类的对象,它们可以像函数一样被调用。
①仿函数特点:
- 状态保存:仿函数可以包含成员变量,从而保持调用时的状态。
- 灵活性:通过重载函数调用运算符可以定义多种操作,且仿函数可以作为参数传递给其他函数,支持继承和多态。
- 上下文捕获:通过lambda表达式创建的仿函数可以捕获其所在作用域的变量,这使得它们可以在闭包中使用这些变量。
- 类型安全:仿函数通过类实现,其成员具有强类型属性,确保状态管理的安全性,且仿函数可以通过模板技术允许自动类型推导,确保调用时参数类型与预期相符。
②仿函数的应用:
仿函数在STL中的算法中扮演重要角色,例如在std::sort、std::for_each等算法中,可以通过传递仿函数来指定排序的准则或执行的操作。此外,仿函数还可以用于策略模式、回调函数等多种设计模式和编程技巧中。
class Myclass {
public:
void operator()(string s) {
cout << s << endl;
}
int operator()(int a, int b) {
return a + b;
}
};
Myclass m1;
m1("Hello World!");
cout << m1(2, 3) << endl;
//匿名函数对象,执行完立即被释放
cout << Myclass()(1, 2) << endl;
//输出
Hello World!
5
3