0%

Cpp基础(9)运算符重载

1.基本概念

  • 运算符:C++预定义表示对数据的运算
    • +, - , *, /, %, ^, &, ~, !, |, =, <<, >>, != …….
    • 只能用于基本的数据类型:整型,实型,字符型,逻辑型,……
  • C++提供了数据抽象的手段:用户自己定义数据类型——类
    • 调用类的成员函数 $\to$ 操作它的对象,有时会不方便
    • 比如在数学上,两个复数可以直接进行+, -等运算;而在C++中,直接将+,-作用于复数(我们定义的类)是不允许的
  • 运算符重载:对抽象数据类型也能够直接使用C++提供的运算符

    • 程序更简洁
    • 代码更容易理解
  • 运算符重对已有的运算符赋予多重的含义

    • 在使用同一运算符作用于不同类型的数据时 $\to$ 不同类型的行为
    • 目的是扩展C++中提供的运算符的适用范围,以用于类所表示的抽象数据类型
  • 运算符重载的实质是函数重载

​ 返回值类型 operator 运算符(形参表) { …… }

  • 在程序编译时:
    • 把含运算符的表达式 $\to$ 对运算符函数的调用
    • 把运算符的操作数 $\to$ 运算符函数的参数
    • 运算符被多次重载时,根据实参的类型决定调用哪个运算符函数
    • 运算符可以被重载为普通函数,也可以被重载为成员函数
  • 运算符重载为普通函数时,参数个数为运算符目数(如+为2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Complex {
public:
Complex(double r = 0.0, double i = 0.0) {
real = r;
imaginary = i;
}
double real;
double imaginary;
};
Complex operator+(const Complex &a, const Complex &b) {
return Complex(a.real+b.real,a.imaginary+b.imaginary)
}
int main() {
Complex a(1, 2), b(2, 3), c;
c = a + b; //相当于operator+(a,b),会传递两个参数a,b
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
class Complex {
public:
Complex(double r = 0.0, double i = 0.0):real(r),imaginary(i) { } //constructor
Complex operator+(const Complex &); //addition
Complex operator-(const Complex &); //subtraction
private:
double real;
double imaginary;
};
//Overload addition operator
Complex Complex::operator+(const Complex &operand2) {
return Complex(real + operand2.real, imaginary + operand2.imaginary)
}
//Overload subtraction operator
Complex Complex::operator-(const Complex &operand2) {
return Complex(real - operand2.real, imaginary - operand2.imaginary)
}

int main() {
Complex x(4.3,8.2),y(3.3,1.1),z;
z = x + y; //相当于z = x.operator(y),即只传入一个参数
z = x - y; //相当于z = x.operator(y)
return 0;
}

2.赋值运算符’=’重载

2.1.基本实现

当类和对象这个新概念对引入的时候,原先一些传统的运算符并不能直接作用在我们自己定义的类型的对象上,但是唯有赋值运算符’=’是可以直接使用的,它会会按对象的数据成员一一完成赋值。当我们对’=’有更多的要求时,比如两边类型可以不匹配,或者除了完成普通的赋值外还要实现其他功能,就需要重载运算符’=’。

  • 赋值运算符’=’只能重载为成员函数

例子:编写一个长度可变的字符串类String,包含一个char *类型的成员变量 $\to$ 指向动态分配的存储空间,该存储空间用于存放'\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
#include<iostream>
using namespace std;
class String {
private:
char *str;
public:
String():str(NULL){} //构造函数,初始化str为NULL
const char * c_str() { return str; }
char * operator=(const char * s);
~String();
};

char * String::operator=(const char *s) {
if (str) delete[] str;
if (s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
}
else
str = NULL;
return str;
}

String::~String() {
if (str) delete[] str;
}

int main() {
String s;
s = "Good Luck"; //相当于s.operator = "Good Luck";
cout << s.c_str() << endl;
//String s2 = "hello!"; 这条语句是错误的,我们并没有定义这样的构造函数
s = "C++";
cout << s.c_str << endl;
return 0;
}

2.2.重载赋值运算符的意义—浅复制和深复制

  • 浅复制/浅拷贝:执行逐个字节的复制工作

比如利用我们上节定义的String类,它有两个对象S1S2,利用我们已经重载的复制运算符可以实现直接将一个字符串赋值给一个String对象,如果想进一步将S2直接赋值给S1,在语法上也是没问题的,它会实现浅复制,即将S2对象中的内容逐字节地复制给S1,实际上就是S1.str = S2.str两个指针就指向了同一块地址,但这会引发一些问题。

1
2
3
4
String S1, S2;
S1 = "this";
S2 = "that";
S1 = S2;

当执行了S1 = S2 后,S1.strS2.str两个指针指向了同一块地址,这引发了两个问题:第一个是存放”this“字符串的内存没有任何指针来对它进行控制,成为了一个内存垃圾;当S1S2同时消亡的时候,存放”that“的内存会被释放两次,这会导致严重的内存错误。

  • 深复制/深拷贝:将一个对象中指针变量指向的内容 $\to$ 复制到另一个对象中指针成员对象指向的地方

1
2
3
4
5
6
String & operator=(const String &s) {
if (str) delete[] str;
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
return *this; //注意这里返回的是this指针
}

通过上面的赋值运算符深拷贝的实现,我们是否已经完全实现了String对象的赋值呢?仔细考虑一下,还会一点小疏漏,就是当执行s = s,即把当前对象赋值给其自身,那么在刚才的重载函数中就会出现一些小问题,当执行strcpy(str, s.str)时会发现我们已经把原来s.str所指的内存空间中的内容删掉了,所以我们要在原来代码基础上加一条if语句。

1
2
3
4
5
6
7
String & operator=(const String &s) {
if (str == s.str) return *this;
if (str) delete[] str;
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
return *this;
}

那么上面定义的String类还有其他问题嘛?

  • 需要注意的是:为String类编写复制构造函数时,会面临和’=’同样的问题,如果采用浅拷贝或者调用默认的复制构造函数,就会出现问题,为此我们也要采用深拷贝的方式
1
2
3
4
5
6
7
8
String(String &s) {
if (s.str) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
else
str = NULL;
}

3.运算符重载为友元函数

  • 通常,将运算符重载为类的成员函数
  • 重载为友元函数的情况:
    • 成员函数不能满足要求
    • 普通函数又不能访问类的私有成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Complex {
public:
Complex(double r,double i):real(r),imag(i) {}
Complex operator+(double r);
private:
double real, imag;
};
Complex Complex::operator+(double r) {
return Complex(real + r, imag);
}
int main(){
Complex c;
c = c + 5;
return 0;
}

上面的例子中,能实现c = c + 5,相当于c = c.operator +(5),但不能实现c = 5 + c,如果要实现后者,就需要将’+’重载为普通函数,且要能访问Complex类的私有成员real,因此要将’+’重载为友元函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Complex {
public:
Complex(double r,double i):real(r),imag(i) {}
Complex operator+(double r);
friend Complex operator+(double r, const Complex &c);
private:
double real, imag;
};
Complex Complex::operator+(double r) {
return Complex(real + r, imag);
}
Complex operator+(double r, const Complex &c) {
return Complex(c.real + r, c.imag);
}

4.实例-长度可变的整型数组类

C++中的数组的大小(size)是固定的,不能按存放元素的多少自动调整容量,为此我们想自己定义一个长度可变的整型数组类,可以实现下面的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {   //要编写可变长度的整型数组类,使用如下功能
CArray a; //开始里的数值是空的
for (int i = 0; i < 5; ++i)
a.push_back(i);
CArray a2, a3;
for (int i = 0; i < a.length(); ++i)
cout << a[i] << " ";
cout << endl;
a2 = a3; //a2这时为空
for (int i = 0; i < a2.length(); ++i) //a2.length()为0,所以没有输出
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a); //复制构造
for (int i = 0; i < a4.length(); i++)
cout << a4[i] << " ";
cout << endl;
return 0;
}

程序的输出结果是:

分析一下需要实现的功能:

  • 要用动态分配的内存来存放数组元素,需要一个指针成员变量
  • 要重载’=’
  • 要重载’[]’,即实现a2[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
class CArray {
int size; //数组元素的个数
int *ptr; //指向动态分配的数组
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray &a);
~CArray();
void push_back(int v); //用于在数组尾部添加一个元素v
int length() { return size; } //返回数组元素个数
CArray & operator=(const CArray & a); //用于数组对象间的赋值
int & operator[](int i) {
//用于支持根据下标访问数组元素,如n = a[i] 和 a[i] = 4
//若返回类型为int,则不能实现a[i] = 4,因为非引用的函数返回值不能作为左值
return ptr[i];
}
};

CArray::CArray(int s) :size(s) {
if (s == 0)
ptr = NULL;
else
ptr = new int[s];
}

CArray::CArray(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;
size = 0;
return *this;
}
if (size < a.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;
}

5.流插入和流提取运算符的重载

  • cout是在iostream中定义的,ostream类的对象。
  • “<<” 能用在cout上是因为在iostream里对”<<”进行了重载

  • cin是在iostream中定义的,istream类的对象。

  • “>>” 能用在cin上是因为在istream里对”>>”进行了重载

例子:假设c是Complex复数类的对象,实现cout<<c; 能输出”a+bi”形式,cin>>c 能从键盘接受”a+bi”形式的输入。

为此我们需要把<<和>>重载成全局函数,因为它们已经是ostreamistream的成员函数了;又因为它们需要访问Complex类的私有成员,因此要声明成Complex类的友元函数。

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
#include<iostream>
#include<string>
#include<cstdlib>
using namespace std;
class Complex {
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"; //以“a+bi”的形式输出
return os;
}

istream & operator>>(istream & is, Complex &c) {
string s;
is >> s; //将"a+bi"作为字符串读入,"a+bi"中间不能有空格
int pos = s.find("+", 0);
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); //分离出代表虚部的字符串
c.imag = atof(sTmp.c_str());
return is;
}

int main() {
Complex c;
int n;
cin >> c >> n;
cout << c << "," << n;
return 0;
}

运行结果示例:

6.自加/自减运算符的重载

  • 自加++/自减—运算符有前置/后置之分:
    • 前置++(++a):先加1,再执行语句
    • 后置++(a++):先执行语句,再加1
  • 前置运算符作为一元运算符重载:

    • 重载为成员函数:T operator++();T operator--();
    • 重载为全局函数:T operator++(T);T operator--(T);
    • ++objobj.operator++()operator++(obj)都是调用上述前置运算符
  • 后置运算符作为二元运算符重载:(多写的参数只是标记重载的运算符为后置,并无具体意见)

    • 重载为成员函数:T operator++(int);T operator--(int);
    • 重载为全局函数:T operator++(T, int);T operator--(T, int);
    • obj++obj.operator++(0)operator++(obj, 0)都是调用上述前置运算符

例子:我们希望设计一个CDemo对象来实现下面的功能:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
CDemo d(5);
cout << (d++) << ","; //等价于d.operator++(0);
cout << d << ",";
cout << (++d) << ","; //等价于d.operator++();
cout << d << endl;
cout << (d--) << ","; //等价于operator--(d,0);
cout << d << ",";
cout << (--d) << ","; //等价于operator--(0);
cout << d << endl;
return 0;
}

程序运行结果:

我们首先需要设计CDemo对象的自加自减运算符,再注意cout<<d语句它会将CDemo对象直接输出为整型数,而cout并没有这样的功能,它不支持任意自定义类型的输出,因此要设计一个强制类型转换符的一个运算符重载。

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
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); //后置--
};

CDemo CDemo::operator++() { //前置++
n++;
return *this;
}
CDemo CDemo::operator++(int k) { //后置++
CDemo tmp(*this);
n++;
return tmp; //返回修改前的对象
}
CDemo operator--(CDemo & d) { //前置++
d.n--;
return d;
}
CDemo operator--(CDemo &d,int k) { //后置++
CDemo tmp(d);
d.n--;
return tmp; //返回修改前的对象
}

注意语句operator int() { return n; }

  • 此时Int作为一个类型强制转换运算符被重载(而不是整型类型了)
1
2
CDemo s;
(int)s; //等效于s.int();
  • 类型强制转换运算符重载时:
    • 不能写返回值类型
    • 实际上返回值类型——类型强制转换运算符代表的类型

运算符重载的注意事项

  • C++不允许定义新的运算符
  • 重载后运算符的含义应该符合日常习惯
  • 运算符重载不改变运算符的优先级
  • 一下运算符不能被重载:’.‘,’.*‘,’::‘,’?:‘,sizeof
  • 重载运算符()[]->=时,重载函数必须声明为类的成员函数