Cpp复习 Chapter 4 重载运算符、友元、类的转换函数


Cpp系列笔记目录

【Cpp筑基】一、内联函数、引用变量、函数重载、函数模板
【Cpp筑基】二、声明 vs 定义、头文件、存储持续性作用域和链接性、名称空间
【Cpp筑基】三、对象和类
【Cpp筑基】四、重载运算符、友元、类的转换函数
【Cpp筑基】五、类的继承、虚函数、抽象基类


1. 重载运算符

在C++中,运算符重载(operator overloading)允许开发者定义或重新定义标准运算符的行为,使其可以用于自定义类型(例如类)。通过运算符重载,可以让用户定义的类型与内置类型一样自然地进行运算操作,提高代码的可读性和易用性。

运算符重载是在C++中提供的一种特性,允许为用户定义的类型(例如类或结构体)定义新的运算符行为。几乎所有的运算符都可以被重载,但是有一些例外,如::域解析运算符), .(成员访问运算符)和*(成员指针访问运算符)不能被重载。

运算符重载通过在类中定义特定的成员函数或友元函数来实现。重载的函数名是operator后跟要重载的运算符,例如要重载+(加号运算符),我们就写成operator+。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) const {
// 这里访问 other.real 和 other.imag 是合法的,因为
// operator+ 是 Complex 类的成员函数
return Complex(real + other.real, imag + other.imag);
}
};
int main() {
Complex c1(1.0, 2.0);
Complex c2(2.5, 3.5);
Complex c3 = c1 + c2; // 使用重载运算符
return 0;
}

需要注意一点,类的成员函数可以访问同类的所有对象的私有和保护成员,这就是为什么这里我们可以直接访问other.realother.imag的原因。

为了方便使用std::cout进行输出,我们通常也需要重载<<运算符,我们应该返回一个std::ofstream&对象,提供给std::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
#include <iostream>
class Complex {
private:
double real, imag;
public:
// 默认构造函数
Complex() : real(0), imag(0) {}
// 带参数的构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) const {
// 这里访问 other.real 和 other.imag 是合法的,因为
// operator+ 是 Complex 类的成员函数
return Complex(real + other.real, imag + other.imag);
}
// 重载 << 运算符用于输出
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}
};
int main() {
Complex c1(1.0, 2.0);
Complex c2(2.5, 3.5);
Complex c3 = c1 + c2;
std::cout << "c1: " << c1 << std::endl;
std::cout << "c2: " << c2 << std::endl;
std::cout << "c3: " << c3 << std::endl;
return 0;
}

待会我们再解释这里为什么需要使用友元friend

2. 友元

2.1 为什么需要友元,一个简单的例子

友元(Friend)是C++中一种特殊的函数或类,它可以访问另一个类的私有和保护成员。友元关系是单向的,不是互相的,也不能继承。友元机制提供了一种灵活的访问控制方式,使得可以在不破坏类封装性的情况下,允许特定的函数或类访问类的私有数据。

友元分为三种:

  • 友元函数
  • 友元类
  • 友元成员函数

考虑我们以下的一个类

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
#include <iostream>
class Complex {
private:
double real, imag;
public:
// 默认构造函数
Complex() : real(0), imag(0) {}
// 带参数的构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) const {
// 这里访问 other.real 和 other.imag 是合法的,因为
// operator+ 是 Complex 类的成员函数
return Complex(real + other.real, imag + other.imag);
}
// 重载 * 运算符
Complex operator*(double time) const {
return Complex(real * time, imag * time);
}
// 重载 << 运算符用于输出
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}
};

int main() {
Complex A, C;
Complex B{ 3,4 };
A = B * 2.5;
// C = 2.5 * B; // 未定义的行为
std::cout << "A: " << A << std::endl;
}

结果应该输出:

1
A: (7.5, 10)

为什么A = B * 2.5就可以,而C = 2.5 * B就不行呢?这是因为A = B * 2.5将会被转化成A = B.operator*(2.5),而A = 2.5 * B不能找到对应的函数原型,所以这样是未定义的行为,但是我们重载乘法运算符*的目的就是为了能够同时使用这两种乘法的方式,那我们应该如何做到呢?这就需要引入友元friend的概念。

创建友元函数的第一步是将其函数原型放在类的声明中,并且函数原型的前面加上关键字friend,例如,在这里我们可以这样进行修改

1
friend Complex operator*(double time, const Complex& other);		// 友元函数原型放在类的声明中

==友元函数是被声明为另一个类的友元的函数。友元函数不是类的成员函数,但它可以访问该类的私有和保护成员==。在这个例子中:

  • 虽然operator*()函数是再类声明中声明的,但是它不是成员函数,因此不能使用成员函数运算符.来调用
  • 虽然operator*()函数不是成员函数,但是它与成员函数的访问权限相同。

了解了这一点之后,我们可以这样修改上述的成员函数

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>
class Complex {
private:
double real, imag;
public:
// 默认构造函数
Complex() : real(0), imag(0) {}
// 带参数的构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) const {
// 这里访问 other.real 和 other.imag 是合法的,因为
// operator+ 是 Complex 类的成员函数
return Complex(real + other.real, imag + other.imag);
}
// 重载 * 运算符
Complex operator*(double time) const {
return Complex(real * time, imag * time);
}
// 友元函数,不属于成员函数
friend Complex operator*(double time, const Complex& other) {
return Complex(other.real * time, other.imag * time);
}
// 重载 << 运算符用于输出
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}
};

int main() {
Complex A, C;
Complex B{ 3,4 };
A = B * 2.5;
C = 2.5 * B; // 未定义的行为
std::cout << "A: " << A << std::endl;
std::cout << "C: " << C << std::endl;
}

输出:

1
2
A: (7.5, 10)
C: (7.5, 10)

这样我们就成功解决了上述问题,另外,==友元函数是可以被重载的(前提是函数的特征标不同)==,因此,我们也可以这样来解决,例如:

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
#include <iostream>
class Complex {
private:
double real, imag;
public:
// 默认构造函数
Complex() : real(0), imag(0) {}
// 带参数的构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 重载 + 运算符
Complex operator+(const Complex& other) const {
// 这里访问 other.real 和 other.imag 是合法的,因为
// operator+ 是 Complex 类的成员函数
return Complex(real + other.real, imag + other.imag);
}
// 重载 * 运算符,不能同时提供两种解决方式
//Complex operator*(double time) const {
// return Complex(real * time, imag * time);
//}
// 友元函数,不属于成员函数
friend Complex operator*(double time, const Complex& other) {
return Complex(other.real * time, other.imag * time);
}
// 重载友元函数,不属于成员函数
friend Complex operator*(const Complex& other, double time) {
return Complex(other.real * time, other.imag * time);
}
// 重载 << 运算符用于输出
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}
};

int main() {
Complex A, C;
Complex B{ 3,4 };
A = B * 2.5;
C = 2.5 * B; // 未定义的行为
std::cout << "A: " << A << std::endl;
std::cout << "C: " << C << std::endl;
}

2.2 重载<<运算符

为什么需要重载<<运算符,很重要的是,我们重载了<<运算符之后可以使之与cout一起来显示对象的内容,但是这里有一个很重要的问题是,当我们使用cout << Complex的时候,实际调用的不是Complex.operator<<(cout),我们使用Complex << cout才调用Complex.operator<<(cout),这就导致了无法正确使用,所以这个时候我们就需要使用友元friend了。

1
2
3
4
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}

注意,返回类型是std::ostream &,这意味着该函数返回ostream对象的引用,具体地说,它将返回一个指向调用对象(这里是cout)的引用,因此,表达式cout << x本身就是ostream对象cout,从而可以用于<<运算符的左侧。

2.3 重载运算符:作为成员函数还是非成员函数

对于很多运算符来说,可以选择使用成员函数或非成员函数来实现运算符重载,一般来说,非成员函数应该是友元函数,这样它才能直接访问类的私有数据。

注意:

==非成员版本的重载运算符函数所需的形参数目与运算符使用的操作数数目相同;而成员版本所需的参数数目少一个,因为其中的一个操作数是被隐式地传递的调用对象==。

举个例子,T1 = T2 + T3将被转换成下面两个的任何一个:

1
2
T1 = T2.operator+(T3);			// member function
T1 = operator+(T2, T3); // nonmember function

3. 类的转换函数

在C++中,类的转换函数(conversion function)是指将类的对象转换为其他类型的函数。这种类型转换是通过定义一个特殊的成员函数实现的,该成员函数不带返回类型,但会返回一个指定的类型。这些函数通常是用来将自定义类类型转换为内置类型或其他用户定义类型。

首先,我们先复习一下Cpp是如何处理内置类型转换的。将一个标准类型变量的值赋给另一种标准类型的变量时,如果这两种类型兼容,则C++将自动完成将这个值转换成接受变量的类型。例如:

1
2
long count = 8;			// convert int value 8 to long type
double time = 11; // cocnvert int value 11 to double type

强制类型转换具有两种风格,一种是C风格,一种是C++风格:

1
2
3
int a = 10;
double b = (double)a; // C风格
double c = double(a); // C++风格

C++风格是为了让强制类型转换像函数调用一样。

3.1 转换函数

转换函数的语法是

1
operator type() const;
  • operator是关键字。
  • type是要转换到的目标类型。
  • const是可选的,表示该函数不会修改对象的状态。

==我们编写转换函数的目的就是让我们可以使用C++风格的方式来转换自定义类的类型==。举个例子:

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
#include <iostream>
class Complex {
private:
double real, imag;
public:
// 构造函数
Complex(double r, double i) : real(r), imag(i) {}
// 转换函数:将 Complex 对象转换为 double(返回实部)
operator double() const {
return real;
}
// 转换函数:将 Complex 对象转换为 bool(如果实部和虚部都为零,则返回 false,否则返回 true)
operator bool() const {
return real != 0 || imag != 0;
}
// 重载 << 运算符用于输出
friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << ")";
return os;
}
};

int main() {
Complex c1(3.0, 4.0);
Complex c2(0.0, 0.0);
// 使用转换函数
double realPart = c1; // 转换为double类型
bool isNonZero1 = c1; // 转换为bool类型
bool isNonZero2 = c2; // 转换为bool类型
std::cout << "c1: " << c1 << ", realPart: " << realPart << ", isNonZero1: " << std::boolalpha << isNonZero1 << std::endl;
std::cout << "c2: " << c2 << ", isNonZero2: " << std::boolalpha << isNonZero2 << std::endl;
return 0;
}

输出:

1
2
c1: (3, 4), realPart: 3, isNonZero1: true
c2: (0, 0), isNonZero2: false

3.2 explicit关键字

==关键字explicit用于指示构造函数或转换运算符是显式的,必须显示使用==。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
class MyClass {
public:
// 带一个参数的构造函数
MyClass(int x) : value(x) {
std::cout << "Constructor called with value: " << value << std::endl;
}
int getValue() const {
return value;
}
private:
int value;
};
void printValue(const MyClass& obj) {
std::cout << "Value: " << obj.getValue() << std::endl;
}
int main() {
MyClass obj1 = 42; // 隐式调用构造函数
printValue(50); // 隐式转换发生
return 0;
}

这个例子中,MyClass obj1 = 42;printValue(50);,相当于MyClass obj1 = MyClass(42);printValue(MyClass(50)), 都隐式调用了 MyClass 的构造函数。这可能会导致意外的类型转换和难以调试的问题。为了防止这种情况可以使用expilcit关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
class MyClass {
public:
// 使用 explicit 关键字
explicit MyClass(int x) : value(x) {
std::cout << "Constructor called with value: " << value << std::endl;
}
int getValue() const {
return value;
}
private:
int value;
};
void printValue(const MyClass& obj) {
std::cout << "Value: " << obj.getValue() << std::endl;
}
int main() {
MyClass obj1(42); // 显式调用构造函数
// printValue(50); // 编译错误:需要显式转换
printValue(MyClass(50)); // 显式转换
return 0;
}

Reference

《C++ Primer Plus》