运算符重载

运算符重载

  • 已有的运算符赋予多重含义

  • 使得同一运算符作用于不同数据类型时产生不同类型的行为

  • 实质是函数重载,例如operator+(a,b)a.operator+(a,b)

  • 运算符可以被重载为普通函数和类的成员函数(better)

    • 重载为普通函数时,参数个数就是实际运算符目数;

    • 重载为成员函数时,参数个数为运算符数-1(由于类名.函数相当于已包含自己,更具体的如,a.operator-(b)

      一个成为函数作用的对象,其余成为函数的实参

  • 编译

    image-20260313220945023

目录

范式

1
2
3
4
返回值类型 operator 运算符 (形参表)
{
...
}

运算符的操作数称为函数调用的实参,运算的结果就是函数的返回值。

约定

  • 重载后的运算符含义应该符合原有的用法习惯
  • 尽量保持原有的特性
  • C++规定,运算符重载不改变运算符优先级
  • 不能被重载的运算符:. :: ?: sizeof .*
  • 只能重载为成员函数: = () [] ->

普通运算符重载实例

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
// 注意以下暂时将变量置于public以方便访问,正常情况下参见 friend 接口访问
// 此处的+重载为普通函数,但一般情况下还是更推荐重载为成员函数

#include <iostream>
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
Complex operator-(const Complex &c);
};
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.real << " , " << c.imag << endl;
cout << (a-b).real << " , " << (a-b).imag << endl;
return 0;
}

实现[]的重载

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
#include <iostream>
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 指针隐式获取了

运算符重载为友元

  • 一般情况下,运算符重载为成员函数
  • 重载为成员函数不能满足需求,重载为全局函数
  • 重载为全局函数不能访问类的私有成员,所以需要重载为友元

例如:重载为成员函数 c+5 //ok5+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
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <iostream>
using namespace std;
class CArray{
private:
int size; //数组元素个数
int *ptr; //指向动态分配的数组
public:
CArray(int s=0);
CArray(const CArray &a);
~CArray();
void push_back(int v); // 欲实现 a.push_back(...)

// 更进一步的优化,应当是提供一个const版本
CArray & operator=(const CArray &a); // 用于数组对象之间的赋值
// 优化版本,const关键字防止修改
int length () const {return size;}

//此处返回的是 int & 相当于是对下表的那块地址的引用
int & operator[](int i){
// 防越界
assert(i>=0 && i<size); // 条件不成立程序直接终止并报错
// 用于支持根据下标访问数组元素,如 a[i]=4 和 n=a[i]的语句
return *(ptr+i); // 或者等价地写为 ptr[i]
}
}; // !
CArray::CArray(int s):size(s){
if(s==0)
ptr=NULL;
else
ptr=new int[s];
}
CArray::CArray(const CArray &a){
if(!a.ptr){
ptr=NULL;
size=0;
return;
}
ptr=new int[a.size];
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size=a.size;
}
CArray::~CArray(){
if(ptr) delete []ptr;
}
CArray & CArray::operator=(const CArray & a){
//赋值号作用:“=”左边对象中存放的数组,大小和内容都和右边对象一样
// 防自己,防空
if(ptr==a.ptr) return *this; // 防止自等
if(a.ptr==NULL){
if(ptr) delete []ptr;
ptr=NULL; // 清空地址后为悬浮指针,仍然指向原来地址,必须令为NULL保证安全
size=0;
return *this;
}
if(size<a.size){ // 只有不够才多申请,不用担心push,因为size也对已经改了
if(ptr)
delete []ptr;
ptr=new int[a.size];
}
memcpy(ptr,a.ptr,sizeof(int)*a.size);
size=a.size;
return *this;
}
void CArray::push_back(int v){
if(ptr){
int *tmpPtr=new int[size+1]; // 重新分配空间
memcpy(tmpPtr,ptr,sizeof(int)*size); // 复制原数组内容
delete []ptr;
ptr=tmpPtr;
}
else
ptr=new int[1];
ptr[size++]=v; // 加入新的数组元素
}
int main(){
CArray a;
for(int i=0;i<5;++i)
a.push_back(i);
CArray a2,a3;
a2=a;
for(int i=0;i<a2.length();++i)
cout<<a2[i]<<" ";
cout<<endl;
a[3]=100;
CArray a4(a);
for(int i=0;i<a4.length;++i)
cout<<a4[i]<<" ";
return 0;
}

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)

赋值运算符的重载

赋值运算符“=”只能重载为成员函数!(与该类强关联必须重载为成员函数)

标准操作

  • 检查自我赋值
  • 检查是否为空
  • 释放旧内存→分配新内存→复制数据
  • 让目标指向新内存

以下为<string>的可能实现参考,掌握以下则几乎可以掌握“=”的重载

前置:浅拷贝和深拷贝

浅拷贝(浅复制)

执行逐个字节的复制工作

  • 对于指针,(拷贝指针的值即地址)将导致指向同一个地方
  • 若1对象消亡,但另一指针2仍然指向该地址,对象2消亡释放同一块内存,程序崩溃
  • 若将1指针改指向"others",消亡原本对象,2指针成为悬空指针

image-20260315154951918

深拷贝

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

image-20260315160722752

🤨

image-20260315160532693

目标

实现 string 类,长度可变

实现

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *str;
public:
String():str(NULL){}
//定义转换构造函数(方便强制类型转换)
String(const char* s):str(NULL){
if(s){
str=new char[strlen(s)+1];
strcpy(str,s);
}
}
//必须补充复制构造函数以使得String s=s2 生效,深拷贝!
String(const String &s):str(NULL){
if(s.str){
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
}
}
const char *c_str() const { return str;} // 加上前const防止对象内部被轻易修改,加上外const推广范围,使得const String 也可以调用
String & operator=(const char*s);
~String(){ delete []str;} // 根据生成来确定,此外,C++ 标准规定 delete 或 delete[] 一个空指针是安全的(不会产生任何效果)无需检查
};
// 防自己,防空
// 浅拷贝(针对 const char*)
String & String::operator=(const char *s){
if(str==s) return *this; // 少发生
delete []str;
if(s){
str=new char[strlen(s)+1];
strcpy(str,s);
}
else
str=NULL;
return *this;
}
// 深拷贝
String &String::operator=(const String &s){
//if(str==s.str) return *this; // 可能出现误判!
if(this==&s) return *this; // 直接判断是否为同一个对象,与成员具体的值无关,最可靠!
delete []str;
if(s.str){
str=new char[strlen(s.str)+1];
strcpy(str,s.str);
}
else
str=NULL;
return *this;
}
int main(){
String s;
s="Good Luck";
cout<<s.c_str()<<endl;
// String s2="hello!"; //在没有转换构造函数之前这句话是错的
s="Shenzhou!";
cout<<s.c_str()<<endl;
return 0;
}

关于42行误判:

image-20260315171404785

关于 operator 返回值的讨论

image-20260315171540412

🤨 加上 const 使得 const char* 也可以使用

image-20260315163549393

🤨 有必要自己能够完美写出来吗?

流运算符的重载

流插入运算符重载

<<本没有输出功能,只因被 ostream类重载了多次(cout是ostream类的对象)

  • cout<<object→两个参数

    1
    2
    3
    void operator<<(ostream &o,int n){
    output(n);
    }
  • cout<<5<<" hello"→`operator<<(operator<<(cout,5)," hello")→为了链式调用,必须返回std::ostream类的引用

    1
    2
    3
    4
    ostream & operator<<(ostream &o,int n){
    output(n);
    return o;
    }

重载为全局函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//实际上操作
class A{
public:
int n;
};
ostream & operator<<(ostream & o,const A &a){
o<<a.n;
return o;
}
int main(){
A a;
a.n=5;
cout<<a<<"hello";
return 0;
}

重载为成员函数(实际)

1
2
3
4
5
6
class ostream{
ostream & operator<<(int n){
output(n);
return *this;
}
};

调用时则例如相当于(cout.operator<<(n++)).operator<<(n)

流提取运算符重载

cin 是istream类的对象,在头文件iostream声明,** istream 将>>重载为成员函数,与cin组合用于输入数据 **

实际重载

由于无法修改 ostream 类和 istream 类,只能重载为全局函数,且声明为友元

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
39
40
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex{
private:
double real,imag;
public:
Complex(double r=0,double i=0):real(r),imag(i){}
friend ostream & operator<<(ostream &os,const Complex &c);
friend istream & operator>>(istream &is,Complex &c);
};
ostream & operator <<(ostream & os,const Complex & c)
{
os<<c.real<<"+"<<c.imag<<"i";
return os;
}
istream & operator >>(istream &is,Complex &c){
string s;
is>>s;
int pos=s.find('+',0); // 默认“a+bi”不能有空格
// 此处有个小小的坑:‘-’则不可处理

// 分离实部和虚部
string sTmp=s.substr(0,pos);
c.real=atof(sTmp.c_str()); // atof 将const char*指针指向内容转换为float
sTmp=s.substr(pos+1,s.length()-pos-2);
// 前面pos指的是实际位置-1=长度,取pos可;此处pos指的是+号的pos位置,+1即下一个位置,-2减掉加号和i号
c.imag=atof(sTmp.c_str());
return is;

//实际上更推荐现成的 std::stod(sTmp)。它直接支持 std::string,且能处理转换失败的异常,不需要手动调用 .c_str()
}
int main(){
Complex c;
int n;
cin>>c>>n;
cout<<c<<","<<n;
return 0;
}

强制类型转换运算符重载

在c++语言中,类型的名字(包括类本身的名字)本身也是一种运算符,即强制类型转换运算符

  • ⚠️ 不需要指定返回值类型,因为运算符就代表返回值类型

  • 目运算符

  • 能重载为成员函数

  • 经过适当重载后等价为 对象.operator 类型名()

实现(以double重载为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
class Complex{
private:
double real,imag;
public:
Complex(double r=0,double i=0):real(r),imag(i){}
// ☀️
operator double(){ return real;} // 不需要指定返回类型!
};
int main(){
Complex c(1.2,3.4);
cout<<(double)c<<endl;
double n=2+c; // 等价于 double n=2+c.operator()
cout<<n;
return 0;
}

有了对 double 运算符的重载,本该出现 double 类型的变量或常量的地方,出现的Complex类型对象会被自动调用operator double成员函数,然后取其返回值使用

重载自增、自减运算符

⚠️ 此处重载应有前置和后置之分,前置为返回操作后的值,后置返回操作前的值(再操作),因此:

C++规定,⚠️ 在重载自增、自减运算符时,允许多编写一个没用的 int 类型形参

  • 处理前置表达式,调用参数个数正常的重载函数
  • 处理后置表达式,调用多出一个参数的重载函数

实现

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
39
40
41
#include <iostream>
using namespace std;
class CDemo{
private:
int n;
public:
CDemo(int i=0):n(i){}
CDemo& operator++(); // 前置形式
CDemo operator++(int); // 后置形式
operator int(){ return n;}
friend CDemo operator--(CDemo &);
friend CDemo operator--(CDemo &,int);
};
// 此处 int 是约定不用接变量;其余则是类内声明只需类型,不一定接变量
// 采用CDemo&可以避免一次临时对象的拷贝,且支持 ++++a 这种连续操作
CDemo& CDemo::operator++(){
// 前置 ++
n++;
return *this;
}

// 注意以下需要先存放原来,操作后返回
CDemo CDemo::operator++(int k){
// 后置++
CDemo tmp(*this); //记录修改前的对象
n++;
return tmp;
}
CDemo operator--(CDemo & c){
c.n--;
return c;
}
CDemo operator--(CDemo & c,int k){
CDemo tmp(c);
c.n--;
return tmp;
}
int main(){
...
}
// 此处--重载为全局函数为示范,重载为成员函数更佳

对比底层可以发现,后置效率更低