第21章 运算符重载

运算符重载的基本概念

运算符重载是C++的一项强大特性,允许程序员为自定义类型定义运算符的行为。通过运算符重载,可以使自定义类型的使用更加直观和自然。

什么是运算符重载?

运算符重载是指重新定义运算符的行为,使其能够应用于自定义类型。例如,我们可以为一个Complex(复数)类重载+运算符,使其能够直接进行复数加法运算。

运算符重载的语法

运算符重载的语法与函数定义类似,只是函数名由operator关键字和要重载的运算符组成:

1
2
3
返回类型 operator运算符(参数列表) {
// 实现运算符的行为
}

可重载的运算符

C++中大部分运算符都可以重载,但有一些例外。以下是可重载和不可重载的运算符:

可重载的运算符

  • 算术运算符:+, -, *, /, %, ++, --
  • 关系运算符:==, !=, <, >, <=, >=
  • 逻辑运算符:&&, ||, !
  • 位运算符:&, |, ^, ~, <<, >>
  • 赋值运算符:=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
  • 其他运算符:[], (), ->, ->*, new, new[], delete, delete[]

不可重载的运算符

  • .(成员访问运算符)
  • .*(成员指针访问运算符)
  • ::(作用域解析运算符)
  • ?:(条件运算符)
  • sizeof(大小运算符)
  • typeid(类型信息运算符)
  • const_cast, dynamic_cast, reinterpret_cast, static_cast(类型转换运算符)

成员运算符与非成员运算符

运算符重载可以作为类的成员函数实现,也可以作为非成员函数实现。

成员运算符

成员运算符是作为类的成员函数实现的运算符重载。对于二元运算符,左侧操作数是调用对象,右侧操作数是函数参数。

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
class Complex {
private:
double real;
double imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 成员运算符重载:+ 运算符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}

// 成员运算符重载:- 运算符
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}

// 成员运算符重载:* 运算符
Complex operator*(const Complex& other) const {
return Complex(
real * other.real - imag * other.imag,
real * other.imag + imag * other.real
);
}

// 成员运算符重载:+= 运算符
Complex& operator+=(const Complex& other) {
real += other.real;
imag += other.imag;
return *this;
}

void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};

int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);

Complex c3 = c1 + c2; // 调用 operator+
Complex c4 = c1 - c2; // 调用 operator-
Complex c5 = c1 * c2; // 调用 operator*

c1 += c2; // 调用 operator+=

std::cout << "c1: "; c1.display();
std::cout << "c2: "; c2.display();
std::cout << "c1 + c2: "; c3.display();
std::cout << "c1 - c2: "; c4.display();
std::cout << "c1 * c2: "; c5.display();

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Complex {
private:
double real;
double imag;

public:
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

// 声明友元函数
friend Complex operator+(const Complex& c1, const Complex& c2);
friend Complex operator-(const Complex& c1, const Complex& c2);
friend Complex operator*(const Complex& c1, const Complex& c2);
friend std::ostream& operator<<(std::ostream& os, const Complex& c);

void display() const {
std::cout << real << " + " << imag << "i" << std::endl;
}
};

// 非成员运算符重载:+ 运算符
Complex operator+(const Complex& c1, const Complex& c2) {
return Complex(c1.real + c2.real, c1.imag + c2.imag);
}

// 非成员运算符重载:- 运算符
Complex operator-(const Complex& c1, const Complex& c2) {
return Complex(c1.real - c2.real, c1.imag - c2.imag);
}

// 非成员运算符重载:* 运算符
Complex operator*(const Complex& c1, const Complex& c2) {
return Complex(
c1.real * c2.real - c1.imag * c2.imag,
c1.real * c2.imag + c1.imag * c2.real
);
}

// 非成员运算符重载:<< 运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}

int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);

Complex c3 = c1 + c2; // 调用非成员 operator+
Complex c4 = c1 - c2; // 调用非成员 operator-
Complex c5 = c1 * c2; // 调用非成员 operator*

std::cout << "c1: " << c1 << std::endl;
std::cout << "c2: " << c2 << std::endl;
std::cout << "c1 + c2: " << c3 << std::endl;
std::cout << "c1 - c2: " << c4 << std::endl;
std::cout << "c1 * c2: " << c5 << std::endl;

return 0;
}

成员运算符与非成员运算符的选择

  • 成员运算符:适用于一元运算符(如++, --)和左侧操作数是类对象的二元运算符(如+=, -=)。
  • 非成员运算符:适用于需要支持左侧操作数为内置类型的二元运算符(如10 + Complex(1, 2))和流插入/提取运算符(如<<, >>)。

特殊运算符的重载

赋值运算符

赋值运算符=是一个特殊的运算符,默认情况下,类会生成一个默认的赋值运算符,执行成员级别的复制。但是,当类包含指针或其他需要深度复制的成员时,需要自定义赋值运算符。

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
class String {
private:
char* data;
size_t length;

public:
String(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}

// 复制构造函数
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}

// 赋值运算符重载
String& operator=(const String& other) {
if (this != &other) { // 避免自赋值
delete[] data; // 释放原有内存
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
}
return *this;
}

// 移动赋值运算符(C++11+)
String& operator=(String&& other) noexcept {
if (this != &other) { // 避免自赋值
delete[] data; // 释放原有内存
data = other.data; // 窃取资源
length = other.length;
other.data = nullptr; // 置空源对象
other.length = 0;
}
return *this;
}

~String() {
delete[] data;
}

const char* c_str() const {
return data;
}
};

下标运算符

下标运算符[]用于访问类的元素,通常用于容器类。

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
class Array {
private:
int* data;
size_t size;

public:
Array(size_t s) : size(s) {
data = new int[size];
}

~Array() {
delete[] data;
}

// 下标运算符重载(非const版本)
int& operator[](size_t index) {
if (index >= size) {
throw std::out_of_range("Index out of range");
}
return data[index];
}

// 下标运算符重载(const版本)
const int& operator[](size_t index) const {
if (index >= size) {
throw std::out_of_range("Index out of range");
}
return data[index];
}

size_t getSize() const {
return size;
}
};

int main() {
Array arr(5);

// 使用下标运算符赋值
for (size_t i = 0; i < arr.getSize(); ++i) {
arr[i] = i * 10;
}

// 使用下标运算符访问
for (size_t i = 0; i < arr.getSize(); ++i) {
std::cout << "arr[" << i << "] = " << arr[i] << std::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
39
40
41
42
43
44
45
46
47
class Add {
private:
int value;

public:
Add(int v) : value(v) {}

// 函数调用运算符重载
int operator()(int x) const {
return x + value;
}
};

class Multiply {
private:
int factor;

public:
Multiply(int f) : factor(f) {}

// 函数调用运算符重载
int operator()(int x) const {
return x * factor;
}
};

int main() {
Add add5(5);
int result1 = add5(10); // 调用operator()
std::cout << "10 + 5 = " << result1 << std::endl;

Multiply multiply3(3);
int result2 = multiply3(10); // 调用operator()
std::cout << "10 * 3 = " << result2 << std::endl;

// 函数对象作为参数
std::vector<int> vec = {1, 2, 3, 4, 5};
std::transform(vec.begin(), vec.end(), vec.begin(), add5);

std::cout << "After adding 5: " << std::endl;
for (int num : vec) {
std::cout << num << " ";
}
std::cout << std::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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Counter {
private:
int value;

public:
Counter(int v = 0) : value(v) {}

// 前缀递增运算符
Counter& operator++() {
++value;
return *this;
}

// 后缀递增运算符
Counter operator++(int) {
Counter temp = *this;
++value;
return temp;
}

// 前缀递减运算符
Counter& operator--() {
--value;
return *this;
}

// 后缀递减运算符
Counter operator--(int) {
Counter temp = *this;
--value;
return temp;
}

int getValue() const {
return value;
}
};

int main() {
Counter c(5);

std::cout << "Initial value: " << c.getValue() << std::endl;

// 前缀递增
++c;
std::cout << "After prefix ++: " << c.getValue() << std::endl;

// 后缀递增
c++;
std::cout << "After postfix ++: " << c.getValue() << std::endl;

// 前缀递减
--c;
std::cout << "After prefix --: " << c.getValue() << std::endl;

// 后缀递减
c--;
std::cout << "After postfix --: " << c.getValue() << std::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
39
40
41
42
43
44
45
46
47
48
49
50
51
class Temperature {
private:
double celsius;

public:
Temperature(double c) : celsius(c) {}

double getCelsius() const {
return celsius;
}

// 类型转换运算符:转换为double(华氏度)
operator double() const {
return celsius * 9.0 / 5.0 + 32.0;
}

// 类型转换运算符:转换为bool
explicit operator bool() const {
return celsius > 0;
}
};

int main() {
Temperature t(25.0);
std::cout << "Celsius: " << t.getCelsius() << std::endl;

// 隐式转换为double(华氏度)
double fahrenheit = t;
std::cout << "Fahrenheit: " << fahrenheit << std::endl;

// 显式转换为bool
if (static_cast<bool>(t)) {
std::cout << "Temperature is above freezing" << std::endl;
} else {
std::cout << "Temperature is at or below freezing" << std::endl;
}

// 测试低于冰点的温度
Temperature t2(-5.0);
std::cout << "\nCelsius: " << t2.getCelsius() << std::endl;
fahrenheit = t2;
std::cout << "Fahrenheit: " << fahrenheit << std::endl;

if (static_cast<bool>(t2)) {
std::cout << "Temperature is above freezing" << std::endl;
} else {
std::cout << "Temperature is at or below freezing" << std::endl;
}

return 0;
}

运算符重载的最佳实践

1. 保持运算符的语义

重载的运算符应该保持其原始语义,例如,+运算符应该执行加法操作,而不是其他操作。

2. 返回类型的选择

  • 一元运算符:返回修改后的对象(如++, --)。
  • 二元运算符:返回一个新的对象(如+, -, *)。
  • 赋值运算符:返回*this的引用(如=, +=, -=)。

3. 处理自赋值

在重载赋值运算符时,应该检查自赋值情况,避免不必要的操作和潜在的错误。

4. 使用const

对于不修改对象状态的运算符重载,应该使用const修饰符。

5. 避免过度重载

不要为了重载而重载运算符,只有当运算符重载能够使代码更清晰、更直观时才使用。

6. 考虑异常安全

在运算符重载中,应该考虑异常安全,确保在发生异常时,对象的状态保持一致。

运算符重载的应用示例

实现一个简单的向量类

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class Vector {
private:
double x;
double y;
double z;

public:
Vector(double x = 0.0, double y = 0.0, double z = 0.0) : x(x), y(y), z(z) {}

// 成员运算符重载
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y, z + other.z);
}

Vector operator-(const Vector& other) const {
return Vector(x - other.x, y - other.y, z - other.z);
}

Vector operator*(double scalar) const {
return Vector(x * scalar, y * scalar, z * scalar);
}

Vector& operator+=(const Vector& other) {
x += other.x;
y += other.y;
z += other.z;
return *this;
}

Vector& operator-=(const Vector& other) {
x -= other.x;
y -= other.y;
z -= other.z;
return *this;
}

Vector& operator*=(double scalar) {
x *= scalar;
y *= scalar;
z *= scalar;
return *this;
}

// 计算向量的模长
double magnitude() const {
return sqrt(x * x + y * y + z * z);
}

// 计算单位向量
Vector normalize() const {
double mag = magnitude();
if (mag == 0) {
return Vector();
}
return *this * (1.0 / mag);
}

// 计算点积
double dot(const Vector& other) const {
return x * other.x + y * other.y + z * other.z;
}

// 计算叉积
Vector cross(const Vector& other) const {
return Vector(
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x
);
}

// 友元函数
friend Vector operator*(double scalar, const Vector& vec);
friend std::ostream& operator<<(std::ostream& os, const Vector& vec);
};

// 非成员运算符重载:标量左乘
Vector operator*(double scalar, const Vector& vec) {
return Vector(vec.x * scalar, vec.y * scalar, vec.z * scalar);
}

// 非成员运算符重载:流插入
std::ostream& operator<<(std::ostream& os, const Vector& vec) {
os << "(" << vec.x << ", " << vec.y << ", " << vec.z << ")";
return os;
}

int main() {
Vector v1(1.0, 2.0, 3.0);
Vector v2(4.0, 5.0, 6.0);

std::cout << "v1: " << v1 << std::endl;
std::cout << "v2: " << v2 << std::endl;

Vector v3 = v1 + v2;
std::cout << "v1 + v2: " << v3 << std::endl;

Vector v4 = v1 - v2;
std::cout << "v1 - v2: " << v4 << std::endl;

Vector v5 = v1 * 2.0;
std::cout << "v1 * 2.0: " << v5 << std::endl;

Vector v6 = 3.0 * v1;
std::cout << "3.0 * v1: " << v6 << std::endl;

double dotProduct = v1.dot(v2);
std::cout << "v1 · v2: " << dotProduct << std::endl;

Vector crossProduct = v1.cross(v2);
std::cout << "v1 × v2: " << crossProduct << std::endl;

double mag = v1.magnitude();
std::cout << "|v1|: " << mag << std::endl;

Vector unit = v1.normalize();
std::cout << "v1 normalized: " << unit << std::endl;
std::cout << "|v1 normalized|: " << unit.magnitude() << std::endl;

return 0;
}

总结

运算符重载是C++的一项强大特性,允许程序员为自定义类型定义运算符的行为,使自定义类型的使用更加直观和自然。本章介绍了:

  1. 运算符重载的基本概念:什么是运算符重载,运算符重载的语法。
  2. 可重载的运算符:哪些运算符可以重载,哪些不能重载。
  3. 成员运算符与非成员运算符:如何实现成员运算符和非成员运算符,以及如何选择。
  4. 特殊运算符的重载:赋值运算符、下标运算符、函数调用运算符、递增/递减运算符、类型转换运算符。
  5. 运算符重载的最佳实践:保持运算符的语义、返回类型的选择、处理自赋值、使用const、避免过度重载、考虑异常安全。
  6. 运算符重载的应用示例:实现一个简单的向量类。

通过合理使用运算符重载,可以使C++代码更加简洁、直观和易于理解。然而,运算符重载也需要谨慎使用,只有当它能够真正提高代码质量时才应该使用。