运算符重载

运算符重载
flowwalker运算符重载
对已有的运算符赋予多重含义
使得同一运算符作用于不同数据类型时产生不同类型的行为
实质是函数重载,例如
operator+(a,b)和a.operator+(b)运算符可以被重载为普通函数和类的成员函数(better)
重载为普通函数时,参数个数就是实际运算符目数;
重载为成员函数时,参数个数为运算符数-1(由于
类名.函数相当于已包含自己,更具体的如,a.operator-(b))一个成为函数作用的对象,其余成为函数的实参
编译

⚠️
*既可以作为乘法号(两个参数),也可以作为指针符号一个参数
目录
范式
1 | 返回值类型 operator 运算符 (形参表) |
运算符的操作数称为函数调用的实参,运算的结果就是函数的返回值。
对于强制类型运算符,返回值类型不必写,因为运算符就说明了返回值类型
约定
- 重载后的运算符含义应该符合原有的用法习惯
- 尽量保持原有的特性
- C++规定,运算符重载不改变运算符优先级
- ⚠️并非所有都可重载,不能被重载的运算符:
.::?:sizeof.*(以及原来不存在的) - ⚠️只能重载为成员函数:
=()[]->和强制类型转换运算符
普通运算符重载实例
1 | // 注意以下暂时将变量置于public以方便访问,正常情况下参见 friend 接口访问 |
实现[]的重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 此处用到了引用作为返回值即为return的值(实参)
Complex operator-(const Complex &c);
double& operator[](int index) { return index == 0 ? real : imag; }
const double& operator[](int index) const { return index == 0 ? real : imag; }
// 此处重载了两次,前者用于非const对象的只读和写入,后者用于const对象的只读(const 只能用于此)--当然也可以传入非const对象只读
};
Complex operator+(const Complex &a, const Complex &b)
{
return Complex(a.real + b.real, a.imag + b.imag);
}
Complex Complex::operator-(const Complex &c)
{
return Complex(real - c.real, imag - c.imag);
}
int main()
{
Complex a(4,4), b(1,1), c;
c = a + b;
cout << c[0] << " , " << c[1] << endl; // 使用 [] 访问
a[0] = 10; // 通过 [] 修改实部
a[1] = 20; // 修改虚部
cout << (a-b)[0] << " , " << (a-b)[1] << endl;
return 0;
}此外,
[]是双目运算符,需要两个操作数:对象本身(左操作数)和下标索引(右操作数),重载为成员函数(只能)仅有一个参数,因为第一个操作数被this 指针隐式获取了实现二维数组
方法 1:代理类(实现
a[i][j]语法)
operator[]只能接受一个参数,所以第一次a[i]返回一个代理对象,再由代理对象的operator[]取列:cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 class Array2D {
int* data;
int rows, cols;
public:
// 代理类:代表某一行
class RowProxy {
int* row_start;
public:
RowProxy(int* p) : row_start(p) {}
int& operator[](int j) { return row_start[j]; }
};
Array2D(int r, int c) : rows(r), cols(c) {
data = new int[r * c](); // 一维连续内存
}
~Array2D() { delete[] data; }
RowProxy operator[](int i) {
return RowProxy(data + i * cols);
}
};
// 使用
Array2D a(3, 4);
a[1][2] = 100; // 先 a[1] 返回 RowProxy,再 [2] 定位元素好处:符合直觉的
[][]写法,且底层是一维连续内存方法 2:直接重载
operator()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 class Array2D {
int* data;
int rows, cols;
public:
Array2D(int r, int c) : rows(r), cols(c) {
data = new int[r * c]();
}
~Array2D() { delete[] data; }
int& operator()(int i, int j) {
return data[i * cols + j];
}
int& operator()(int i, int j) const {
return data[i * cols + j];
}
};
// 使用
Array2D a(3, 4);
a(1, 2) = 100; // 直接二维索引,无需代理类方法3:最直接了当的办法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 class Array2D {
int** data;
int rows, cols;
public:
Array2D(int r, int c) : rows(r), cols(c) {
data = new int*[rows];
for (int i = 0; i < rows; ++i)
data[i] = new int[c]();
}
int* operator[](int i) { return data[i]; } // 返回 int*,第二次 [j] 用内建指针下标
~Array2D() {
for (int i = 0; i < rows; ++i) delete[] data[i];
delete[] data;
}
};
// 使用
Array2D a(3, 4);
a[1][2] = 100; // a[1] 返回 int*,然后 [2] 是原生指针运算
运算符重载为友元
- 一般情况下,运算符重载为成员函数
- 重载为成员函数不能满足需求,重载为全局函数
- 重载为全局函数不能访问类的私有成员,所以需要重载为友元
例如:重载为成员函数
c+5 //ok,5+c //error!,需重载为全局函数,为使其能够访问私有应声明为友元示范:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 class Complex{
private:
double real,imag;
public:
Complex(double r,double i):real(r),imag(i){}
Complex operator+(double r);
friend Complex operator+(double r,const Complex& c);
};
Complex Complex::operator+(double r){
return Complex(real+r,img);
}
//解释 c+5
Complex operator+(double r,const Complex &c){
return Complex(r+c.real,c.imag);
}
//解释 5+c
// 对于全局函数+,第一个参数为左值,第二个参数为右值事实上:
更加简单的写法(但可能略微开销)是:
1
2
3
4
5
6
7 //构造函数改为:
Complex(double r,double i=0):real(r),imag(i){}
//采用全局友函数+自动隐式转换的方式
friend Complex(const Complex &c1,const Complex &c2){
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
// 这样5→Complex(5)可以实现正常加减
下标运算符的高级重载
目标
重载为长度可变的整型数组类
希望
- 数组元素个数可在初始化该对象时指定
- 可以往动态数组中添加元素
- 使用该类时不用操心动态分配、释放问题
- 能够像使用数组那样使用动态数组类对象,如可以下标访问元素
⚠️ “[]”是双目运算符,两个操作数,一里一外,“a[i]”等价于a.operator[](i)。按照原有“[]”的特性,“a[i]”可作为左值使用(等号左侧,具有明确的内存地址),因此应该返回引用
实现
1 |
|
Tips:注意到push_back函数每次尾部添加一个元素都要重新分配内存并复制原有内容,效率低下,因此可以采用“翻倍扩容增长策略”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 // 成员变量增加 capacity
void CArray::push_back(int v){
if(size==capacity){
// 空间爆满再扩容
int newCapacity=(capacity==0?)1:2*capacity;
int *tmpPtr=new int[newCapacity];
if(ptr){
memcpy(tmpPtr,ptr,sizeof(int)*size);
delete []ptr;
}
ptr=tmpPtr;
capacity=newCapacity;
}
ptr[size++]=v;
}如此将扩容次数从 n 降到 log(n)
1
2 int* a = new int[5]; // a[0] 可能是 0,也可能是 114514,不可预期
int* b = new int[5](); // b[0] ~ b[4] 保证全是 0
赋值运算符的重载
赋值运算符“=”只能重载为成员函数!(与该类强关联必须重载为成员函数)
标准操作
- ⚠️
- 检查自我赋值
- 检查是否为空
- 释放旧内存→分配新内存→复制数据
- 让目标指向新内存
以下为<string>的可能实现参考,掌握以下则几乎可以掌握“=”的重载
前置:浅拷贝和深拷贝
浅拷贝(浅复制)
执行逐个字节的复制工作
- 对于指针,(拷贝指针的值即地址)将导致指向同一个地方
- 若1对象消亡,但另一指针2仍然指向该地址,对象2消亡释放同一块内存,程序崩溃
- 若将1指针改指向"others",消亡原本对象,2指针成为悬空指针

深拷贝
将一个对象中指针变量指向的内容→复制到另一个对象中指针成员变量指向的地方

🤨
目标
实现 string 类,长度可变
实现
1 |
|
关于42行误判:
关于 operator 返回值的讨论
🤨 加上 const 使得 const char* 也可以使用
流运算符的重载
流插入运算符重载
<<本没有输出功能,只因被 ostream类重载了多次(cout是ostream类的对象)
cout<<object→两个参数1
2
3void operator<<(ostream &o,int n){
output(n);
}cout<<5<<" hello"→`operator<<(operator<<(cout,5)," hello")→为了链式调用,必须返回std::ostream类的引用1
2
3
4ostream & operator<<(ostream &o,int n){
output(n);
return o;
}
重载为全局函数:
1 | //实际上操作 |
重载为成员函数
标准库实际做的,但是对于自定义对象⚠️我们不能改变标准库!!!
1 | class ostream{ |
调用时则例如相当于(cout.operator<<(n++)).operator<<(n)
流提取运算符重载
cin 是istream类的对象,在头文件iostream声明,** istream 将>>重载为成员函数,与cin组合用于输入数据 **
实际重载
由于无法修改 ostream 类和 istream 类,只能重载为全局函数,且声明为友元
1 |
|
强制类型转换运算符重载
在c++语言中,类型的名字(包括类本身的名字)本身也是一种运算符,即强制类型转换运算符
⚠️ 不需要指定返回值类型,因为运算符就代表返回值类型
单目运算符
只能重载为成员函数
经过适当重载后等价为
对象.operator 类型名()
实现(以double重载为例)
1 |
|
🌊有了对 double 运算符的重载,本该出现 double 类型的变量或常量的地方,出现的Complex类型对象会被自动调用operator double成员函数,然后取其返回值使用
重载自增、自减运算符
⚠️ 此处重载应有前置和后置之分,前置为返回操作后的值,后置返回操作前的值(再操作),因此:
C++规定,⚠️ 在重载自增、自减运算符时,允许多编写一个没用的 int 类型形参→
- 处理前置表达式,调用参数个数正常的重载函数
- ⚠️处理后置表达式,调用多出一个参数的重载函数
实现
1 |
|
对比底层可以发现,后置效率更低













