C++ 入门基础之六

大纲

友元函数

类的友元函数是定义在类的外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型在类的声明中出现过,但是友元函数并不是类的成员函数,而是普通函数(全局函数)。如果要声明函数为一个类的友元函数,则需要在类定义中的函数原型前面使用关键字 friend

友元函数的规则

C++ 利用 friend 修饰符,可以让一些设定的函数能够对类中的一些保护数据进行访问,避免把类的成员全部设置成 public,并且最大限度的保护数据成员的安全。同时友元函数可以实现类之间的数据共享,减少系统开销,提高效率。由于友元函数破环了封装机制,因此推荐尽量使用成员函数,除非不得已的情况下才使用友元函数。

  • 什么时候适合使用友元函数

    • 多个类要共享数据的时候
    • 运算符重载的某些场合需要使用友元函数
  • 友元函数的参数:因为友元函数没有 this 指针,所以友元函数的参数会有以下三种情况

    • (1) 要访问非 static 成员时,需要对象做参数
    • (2) 要访问 static 成员或全局变量时,则不需要对象做参数
    • (3) 如果做参数的对象是全局对象,则不需要对象做参数
  • 友元函数的调用

    • 可以直接调用友元函数,不需要通过对象或指针
    • 友元函数的调用与普通函数(全局函数)的调用方式和原理一致
  • 友元函数的位置

    • 因为友元函数是类外的普通函数(全局函数),所以它的声明可以放在类的私有段(private)或公有段(public),两者都是没有区别的
    • 一个函数可以是多个类的友元函数,只需要在各个类中分别声明即可

友元函数的使用

全局函数作为友元函数

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
#include <iostream>

using namespace std;

class Building {

public:
Building() {
this->m_SittingRom = "sitting rom";
this->m_BedRoom = "bed room";
}

// 声明友元函数
friend void visit(Building *building);

public :
string m_SittingRom;

private :
string m_BedRoom;

};

// 定义普通函数(全局函数)
void play(Building *building) {
cout << "好朋友正在访问" << building->m_SittingRom << endl;

// 错误写法,在普通函数(全局函数)内,类的私有成员不能在类外被访问
// cout << "好朋友正在访问" << building->m_BedRoom << endl;
}

// 定义友元函数(全局函数)
void visit(Building *building) {
cout << "Good friend visiting " << building->m_SittingRom << endl;

// 通过友元函数,在类外可以访问类的私有成员
cout << "Good friend visiting " << building->m_BedRoom << endl;
}

int main() {
Building *building = new Building();
// 调用友元函数
visit(building);
return 0;
}

程序运行的输出结果如下:

1
2
Good friend visiting sitting rom
Good friend visiting bed room

成员函数作为友元函数

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>

using namespace std;

class Building;

class GoodGay {

public:
GoodGay();

void visit();

void play();

private :
Building *building;

};

class Building {

// 声明只让 GoodGay 的成员函数 visit () 作为友元函数
friend void GoodGay::visit();

public:
Building();

public:
string m_SittingRoom;

private:
string m_BedRoom;
};

GoodGay::GoodGay() {
this->building = new Building();
}

void GoodGay::play() {
cout << "Good friend playing in " << this->building->m_SittingRoom << endl;
// 这里 GoodGay 类的成员函数 play() 不可以访问 Building 类的私有成员或者保护成员
// cout << "Good friend playing in " << this->building->m_BedRoom << endl;
}

void GoodGay::visit() {
cout << "Good friend visiting " << this->building->m_SittingRoom << endl;
// 这里 GoodGay 类的成员函数 visit() 可以访问 Building 类的私有成员或者保护成员
cout << "Good friend visiting " << this->building->m_BedRoom << endl;
}

Building::Building() {
this->m_SittingRoom = "sitting room";
this->m_BedRoom = "bed room";
}

int main() {
GoodGay goodGay;
goodGay.visit();
return 0;
}

程序运行的输出结果如下:

1
2
Good friend visiting sitting room
Good friend visiting bed room

友元类

友元类的所有成员函数都是另一个类的友元函数,即都可以访问另一个类中的私有(private)成员和保护(protected)成员。当希望一个类可以访问另一个类的保护数据时,可以将该类声明为另一类的友元类。定义友元类的语法格式为 friend class 类名;,其中类名必须是程序中的一个已定义过的类。值得一提的是,友元类通常设计为一种对数据操作或类之间传递消息的辅助类。

友元类的规则

  • 友元关系不能被继承
  • 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,则类 A 不一定是类 B 的友元,要看在类 B 中是否有相应的声明
  • 友元关系不具有传递性,若类 B 是类 A 的友元,类 C 是 类 B 的友元,则类 C 不一定是类 A 的友元,要看类 A 中是否有相应的声明

C++ 是纯面向对象的语言吗?

如果一个类被声明为 friend,这意味着它不是这个类的成员函数,却可以修改这个类的私有成员,而且必须列在类的定义中,因此它是一个特权函数。C++ 不是完全面向对象的语言,而只是一个混合产品,增加 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
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

using namespace std;

class Building {

// 声明友元类
friend class GoodGay;

public:
Building();

public:
string m_SittingRoom;

private:
string m_BedRoom;
};

Building::Building() {
this->m_SittingRoom = "sitting room";
this->m_BedRoom = "bed room";
}

class GoodGay {

public:
GoodGay();

void visit();

private :
Building *building;

};

GoodGay::GoodGay() {
this->building = new Building();
}

void GoodGay::visit() {
cout << "Good friend visiting " << this->building->m_SittingRoom << endl;
// 类 GoodGay 是类 Building 的友元类,因此 GoodGay 类的所有成员函数都可以直接访问 Building 类的私有成员或者保护成员
cout << "Good friend visiting " << this->building->m_BedRoom << endl;
}

int main() {
GoodGay goodGay;
goodGay.visit();
return 0;
}

程序运行的输出结果如下:

1
2
Good friend visiting sitting room
Good friend visiting bed room

运算符重载基础

所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是 一名多用。运算符也可以重载,实际上,开发者已经在不知不觉之中使用了运算符重载。例如,大家都已习惯于用加法运算符 + 对整数、单精度数和双精度数进行加法运算,如 5 + 8,5.8 + 3.67 等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的,但由于 C++ 已经对运算符 + 进行了重载,所以就能适用于 int、float、doUble 类型的运算。又如 << 是 C++ 的位运算中的位移运算符(左移),但在输出操作中又是与流对象 cout 配合使用的流插入运算符。>> 也是位移运算符 (右移),但在输入操作中又是与流对象 cin 配合使用的流提取运算符。这就是运算符重载 (Operator Overloading)。C++ 系统对 <<>> 进行了重载,用户在不同的场合下使用它们时,作用是不同的。对 <<>> 的重载处理是放在头文件 stream 中的。因此,如果要在程序中用 <<>> 作流插入运算符和流提取运算符,必须在本文件模块中包含头文件 iostream,当然还应当包括命名空间的使用声明 using namespace std

提示

运算符重载 (Operator Overloading) 只是一种 "语法上的方便",也就是说它的本质只是另一种函数调用方式而已。另外,操作符重载是靠函数重载来完成的。在 C++ 中,可以定义一个处理类的新运算符,这种定义很像一个普通的函数定义,只是函数的名称由关键字 operator 及其后面紧跟的运算符组成,差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模式时,就会调用这个函数。值得一提的是,对于内置的数据类型的运算符是不可改变的,比如想重载 int 类型数据的 + 号运算符,这是不被允许的。

运算符重载的语法

cplusplus-overload-1

例如:

  • 使用类成员函数完成 - 运算符重载的语法:Complex operator-(Complex &c2)
  • 使用友元函数完成 + 运算符重载的语法:Complex operator+(Complex &c1, Complex &c2)
  • 使用全局函数(普通函数)完成 / 运算符重载的语法:Complex operator/(Complex &c1, Complex &c2)

值得一提的是,在企业开发中一般使用类成员函数和友元函数的方式来实现运算符重载。

运算符重载的限制

cplusplus-overload-6
cplusplus-overload-2

运算符重载的两种方式

cplusplus-overload-3
cplusplus-overload-4
cplusplus-overload-5

前置与后置运算符重载规则

在 C++ 中是通过一个占位参数(int)来区分前置运算符和后置运算符的重载,例如 ++aa++--bb--

cplusplus-overload-7

运算符重载的简单使用案例

二元运算符重载

在下述的案例中,演示了如何使用类成员函数和友元函数实现二元运算符的重载。值得一提的是,除了使用友元函数外,还可以使用全局函数(普通函数)来实现运算符的重载,不同的是使用友元函数更方便,可以直接访问类的所有私有(private)成员和保护(protected)成员。

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
#include <iostream>

using namespace std;

class Complex {
private:
int a;
int b;

public:
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}

void print() {
cout << "a=" << this->a << ", b=" << this->b << endl;
}

public:
// 使用类成员函数完成 "-" 运算符的重载
Complex operator-(Complex& c2) {
Complex c3(this->a - c2.a, this->b - c2.b);
return c3;
}

// 声明用于 "+" 运算符重载的友元函数
friend Complex operator+(Complex& c1, Complex& c2);
};

// 定义友元函数完成 "+" 运算符的重载
Complex operator+(Complex& c1, Complex& c2) {
Complex c3(c1.a + c2.a, c1.b + c2.b);
return c3;
}

int main() {
Complex c1(1, 2), c2(3, 4);

// 直接调用友元函数
Complex c3 = operator+(c1, c2);
c3.print();

// 使用友元函数完成 "+" 运算符的重载
Complex c4 = c1 + c2;
c4.print();

// 直接调用类成员函数
Complex c5 = c1.operator-(c2);
c5.print();

// 使用类成员函数完成 "-" 运算符的重载
Complex c6 = c1 - c2;
c6.print();

return 0;
}

程序运行的输出结果如下:

1
2
3
4
a=4, b=6
a=4, b=6
a=-2, b=-2
a=-2, b=-2

一元运算符重载

在下述的案例中,演示了如何使用类成员函数和友元函数实现一元运算符的重载。值得一提的是,除了使用友元函数外,还可以使用全局函数(普通函数)来实现运算符的重载,不同的是使用友元函数更方便,可以直接访问类的所有私有(private)成员和保护(protected)成员。

前置与后置递增一元运算符介绍

  • 前置 ++(++i): 在使用前置 ++ 时,变量会先递增,然后表达式的值是递增后的值。
  • 后置 ++(i++): 在使用后置 ++ 时,表达式的值是变量当前的值,然后再递增变量。
  • 总结:前置 ++ 和后置 ++ 的主要区别在于返回值的顺序,前者先递增再使用,后者先使用再递增。

前置与后置递减一元运算符介绍

  • 前置 --(--i): 在使用前置 -- 时,变量会先递减,然后表达式的值是递减后的值。
  • 后置 --(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
#include <iostream>

using namespace std;

class Complex {
private:
int a;
int b;

public:
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}

void print() {
cout << "a=" << this->a << ", b=" << this->b << endl;
}

public:
// 使用类成员函数完成 "前置--" 运算符的重载
Complex& operator--() {
this->a--;
this->b--;
return *this;
}

// 使用类成员函数完成 "后置--" 运算符的重载
// 使用占位参数进行函数重载,是为了解决与 "前置--" 类成员函数冲突的问题
Complex operator--(int) {
Complex tmp(this->a, this->b);
this->a--;
this->b--;
return tmp;
}

// 声明用于 "前置++" 运算符重载的友元函数
friend Complex& operator++(Complex& c1);

// 声明用于 "后置++" 运算符重载的友元函数
// 使用占位参数进行函数重载,是为了解决与 "前置++" 友元函数冲突的问题
friend Complex operator++(Complex& c1, int);
};

// 定义友元函数完成 "前置++" 运算符的重载
Complex& operator++(Complex& c1)
{
c1.a++;
c1.b++;
return c1;
}

// 定义友元函数完成 "后置++" 运算符的重载
Complex operator++(Complex& c1, int) {
Complex tmp(c1.a, c1.b);
c1.a++;
c1.b++;
return tmp;
}

int main() {
Complex c1(1, 2), c2(8, 9), c3(15, 16), c4(24, 25);

// 使用友元函数完成 "前置++" 运算符的重载
++c1;
c1.print();

// 使用类成员函数完成 "前置--" 运算符的重载
--c2;
c2.print();

// 使用友元函数完成 "后置++" 运算符的重载
Complex c5 = c3++;
c3.print();
c5.print();

// 使用类成员函数完成 "后置--" 运算符的重载
Complex c6 = c4--;
c4.print();
c6.print();

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
a=2, b=3
a=7, b=8
a=16, b=17
a=15, b=16
a=23, b=24
a=24, b=25

[ ] 运算符的重载

当希望创建一个自定义的数据结构,比如一个向量(Vector)或者一个矩阵(Matrix)时,那么就可能要重载 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
34
35
36
37
38
39
#include <iostream>

using namespace std;

class MyVector {

public:
// [] 运算符重载
int &operator[](int index) {
if (index < 0 || index >= 5) {
cerr << "Index out of bounds" << endl;
exit(1);
}
return data[index];
}

private:
int data[5];

};

int main() {
MyVector vector;

// 设置向量中的值
for (int i = 0; i < 5; i++) {
vector[i] = i * 2;
}

// 设置向量中的值
vector[1] = 100;

// 访问向量中的值并输出
for (int i = 0; i < 5; i++) {
cout << "vector[" << i << "] = " << vector[i] << endl;
}

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
vector[0] = 0
vector[1] = 100
vector[2] = 4
vector[3] = 6
vector[4] = 8

左移运算符的重载

特别注意,<< 左移运算符和 >> 右移运算符的重载,只能使用友元函数或者全局函数(普通函数),不能使用类成员函数,这也是友元函数的重要作用之一。根本原因是:左移运算符的左操作数是输出流对象 std::ostream,而右操作数是要输出的对象。因为左操作数必须是一个标准库类 std::ostream 的对象,而类成员函数将左操作数默认绑定到当前对象,这就意味着无法直接使用类成员函数来重载 << 左移运算符和 >> 右移运算符。

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
#include <iostream>

using namespace std;

class Complex {
private:
int a;
int b;

public:
Complex(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}

public:
// 声明友元函数实现 "<<" 左移运算符的重载
friend ostream& operator<<(ostream& out, Complex& c1);
};

// 定义友元函数实现 "<<" 左移运算符的重载
ostream& operator<<(ostream& out, Complex& c1) {
out << "a=" << c1.a << ", b=" << c1.b << endl;
return out;
}

int main() {
Complex c1(1, 2), c2(6, 9);
cout << c1 << c2;
return 0;
}

程序运行的输出结果如下:

1
2
a=1, b=2
a=6, b=9

关系运算符的重载

在 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
34
35
36
37
38
39
40
41
42
43
#include <iostream>

using namespace std;

class Person {

public:
Person(string name, int age) : m_name(name), m_Age(age) {

}

bool operator==(Person p) {
return this->m_name == p.m_name && this->m_Age == p.m_Age;
}

bool operator>(Person p) {
return this->m_Age > p.m_Age;
}

bool operator<(Person p) {
return this->m_Age < p.m_Age;
}

private :
string m_name;
int m_Age;

};

void test01() {
Person p1("Tom", 18);
Person p2("Peter", 20);
Person p3("Peter", 20);

cout << (p1 < p2) << endl;
cout << (p1 > p2) << endl;
cout << (p2 == p3) << endl;
}

int main() {
test01();
return 0;
}

程序运行的输出结果如下:

1
2
3
1
0
1

赋值运算符的重载

  • = 赋值运算符的结合性是从右到左
  • = 赋值运算符的重载用于对象数据的复制
  • = 赋值运算符的重载,必须通过类成员函数来重载,不能使用友元函数重载
  • = 赋值运算符重载的函数原型为:类型 & 类名 :: operator= ( const 类名 & ) ;
使用案例一

特别注意,C++ 除了会默认提供无参构造函数、拷贝构造函数、析构函数,还会重载 = 赋值运算符。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
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
#include <iostream>
#include <cstring>

using namespace std;

class Person {

public:
Person(int a) : m_A(a) {

}

int getA() const {
return this->m_A;
}

private:
int m_A;
};

void test01() {
Person p1(10);
Person p2(0);
// C++ 除了会默认提供无参构造函数、拷贝构造函数、析构函数,还会重载 `=` 赋值运算符
// C++ 默认提供的 `=` 赋值运算符重载,属于浅拷贝,底层只会对类成员变量进行简单的拷贝(赋值)
p2 = p1;
cout << p2.getA() << endl;

}

class Person2 {

public:
Person2(char *name) {
this->pName = new char[strlen(name) + 1];
strcpy(this->pName, name);
}

~Person2() {
if (nullptr != this->pName) {
delete[] this->pName;
this->pName = nullptr;
}
}

Person2 &operator=(const Person2 &p) {
// 释放内存空间
if (nullptr != this->pName) {
delete[] this->pName;
this->pName = nullptr;
}
// 重新分配内存空间
this->pName = new char[strlen(p.pName) + 1];
strcpy(this->pName, p.pName);
// 返回引用
return *this;
}

char *getName() const {
return this->pName;
}

private :
char *pName;

};

void test02() {
Person2 p1("Peter");
Person2 p2("Tom");
Person2 p3("David");
// 如果不自己重载 `=` 赋值运算符,对象的析构函数在执行时会出现深拷贝和浅拷贝问题,即同一块内存空间会被释放两次,从而导致程序异常退出
p3 = p2 = p1;
cout << p2.getName() << endl;
cout << p3.getName() << endl;
}

int main() {
test01();
test02();
return 0;
}

程序运行的输出结果如下:

1
2
3
10
Peter
Peter
使用案例二
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
#include <iostream>
#include "string.h"

using namespace std;

class Name {

private:
char* p;
int len;

public:

Name(const char* name) {
cout << "有参构造函数被调用了" << endl;
len = strlen(name);
p = new char[len + 1];
strcpy(p, name);
}

// 深拷贝的实现
Name(const Name& name) {
cout << "拷贝构造函数被调用了" << endl;
len = name.getLen();
p = new char[len + 1];
strcpy(p, name.getP());
}

~Name() {
cout << "析构函数被调用了" << endl;
if (p != NULL) {
delete[] p;
p = NULL;
len = 0;
}
}

char* getP() const {
return p;
}

int getLen() const {
return len;
}

public:
// 使用类成员函数实现 "=" 运算符的重载
Name& operator=(const Name& n) {
// 释放内存空间
if (p != NULL) {
delete[] p;
p = NULL;
len = 0;
}
// 重新分配内存空间
len = n.getLen();
p = new char[len + 1];
strcpy(p, n.getP());
return *this;
}
};

int main() {
Name obj1("Peter");
Name obj2("Tom");
Name obj4("Tim");

// 会自动调用拷贝构造函数(属于深拷贝)
Name obj3 = obj1;
cout << "obj3.name: " << obj3.getP() << ", obj3.len: " << obj3.getLen() << endl;

// 不会自动调用拷贝构造函数(属于浅拷贝)
// 默认情况下,若这里不对 "=" 运算符进行重载,最终程序会异常终止运行(由于同一块内存空间被释放两次导致)
obj4 = obj1;
cout << "obj4.name: " << obj4.getP() << ", obj4.len: " << obj4.getLen() << endl;

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
有参构造函数被调用了
有参构造函数被调用了
有参构造函数被调用了
拷贝构造函数被调用了
obj3.name: Peter, obj3.len: 5
obj4.name: Peter, obj4.len: 5
析构函数被调用了
析构函数被调用了
析构函数被调用了
析构函数被调用了

指针运算符的重载

在下述的案例中,演示了如何使用重载指针运算符的方式来简单实现智能指针。在日常开发中,new 出来的对象,在使用完之后都需要开发人员自觉地去 delete,以此来释放对象所占用的内存空间。如果开发人员忘记了 delete,就会很容易造成内存泄漏。有了智能指针后,就可以让智能指针托管指定的对象,这样开发人员就不用关心指定对象的内存释放问题了,也就可以避免内存泄漏了。

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
#include <iostream>

using namespace std;

// 普通类
class Person {

public:
Person() {
this->m_Age = 0;
}

Person(int age) {
this->m_Age = age;
}

~Person() {
cout << "调用Person的析构函数" << endl;
}

void showAge() {
cout << "年龄 = " << this->m_Age << endl;
}

private:
int m_Age;

};

// 智能指针
class SmartPointer {

public:
SmartPointer(Person *p) {
this->person = p;
}

~SmartPointer() {
cout << "调用智能指针的析构函数" << endl;
if (nullptr != this->person) {
// 释放已托管的指针,会自动调用对应的析构函数
delete (this->person);
this->person = nullptr;
}
}

// 重载 -> 符号
Person *operator->() {
return this->person;
}

// 重载 * 符号
Person &operator*() {
return *(this->person);
}

private:
Person *person;

};

void test01() {
// 创建一个Person指针,并托管给智能指针对象,且在栈上给智能指针对象动态分配内存空间
// 函数作用域结束后,会自动释放智能指针对象在栈上的内存空间,并自动调用智能指针对象的析构函数
SmartPointer sp(new Person(20));

// 原本的写法是 sp->->showAge(),只不过编译器做了优化
sp->showAge();

(*sp).showAge();
}

int main() {
test01();
return 0;
}

程序运行的输出结果如下:

1
2
3
4
年龄 = 20
年龄 = 20
调用智能指针的析构函数
调用Person的析构函数

函数调用运算符的重载

在下述的案例中,演示了如何使用类成员函数重载函数调用运算符 ()。值得一提的是,不能用友元函数来重载函数调用运算符 ()

函数对象(仿函数)的实现

在 C++ 中,函数调用运算符 () 的重载,一般是用于在 STL 中实现函数对象(仿函数),更详细的介绍请看 这里

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
#include <iostream>

using namespace std;

class MyPrint {

public:
// 函数调用运算符的重载
void operator()(const string &content) {
cout << content << endl;
}

};

void test01() {
MyPrint myPrint;
myPrint("Hello World");
}

class MyAdd {

public:
// 函数调用运算符的重载
int operator()(const int a, const int b) {
return a + b;
}

};

void test02() {
MyAdd add;

int sum = add(1, 2);
cout << sum << endl;

// 匿名对象
cout << MyAdd()(3, 4) << endl;
}

int main() {
test01();
test02();
return 0;
}

程序运行的输出结果如下:

1
2
3
Hello World
3
7

运算符重载进阶

运算符重载的使用注意事项

  • 不要重载 &&|| 运算符,因为无法实现短路规则。
  • =[]()-> 运算符只能通过成员函数进行重载。
  • <<>> 运算符只能通过普通函数(全局函数)、友元函数进行重载。

为什么不要重载 && 和 || 操作符

  • (a) &&|| 是 C++ 中非常特殊的操作符
  • (b) &&|| 内置实现了短路规则
  • (c) 操作符重载是靠函数重载来完成的
  • (d) 操作数作为函数参数传递
  • (e) C++ 的函数参数都会被求值,无法实现短路规则

同一个运算符的重载可以有多个版本

同一个运算符的重载可以有多个版本,这一点类似在函数重载中有多个同名的函数(但参数个数、参数类型、参数顺序不一样)。

1
2
3
4
5
6
7
8
9
// 加号运算符重载的第一个版本
Person operator+(Person &p1, Person &p2) {

}

// 加号运算符重载的第二个版本
Person operator+(Person &p1, int &a) {

}

不同函数实现运算符重载的应用场景

友元函数和类成员函数的选择建议:

  • (a) =[]()-> 运算符,只能通过类成员函数进行重载
  • (b) 当无法修改左操作数的类时,只能通过友元函数进行重载,例如 << 左移运算符与 >> 右移运算符

友元函数重载 <<>> 运算符:

  • istreamostream 是 C++ 的预定义流类
  • cinistream 的对象,coutostream 的对象
  • 左移运算符 <<ostream 重载为插入操作,用于输出基本类型数据
  • 右移运算符 >>istream 重载为提取操作,用于输入基本类型数据
  • 只能使用友元函数或者普通函数(全局函数)来重载 <<>> 运算符,以便输出和输入用户自定义的数据类型

类成员函数与友元函数实现运算符重载的步骤:

  • (a) 要承认运算符重载是一个函数,写出函数名称,如 operator+()
  • (b) 根据操作数,写出函数参数
  • (c) 根据业务,完善函数的返回值(考虑函数是返回引用、指针还是元素),及实现函数业务;例如当函数的返回值充当左值时,需要返回一个引用

使用友元函数重载运算符的注意事项

  • (a) 友元函数重载运算符常用于运算符的左右操作数类型不相同的场景
  • (b) 在函数的第一个参数需要隐式转换的情形下,使用友元函数重载运算符是正确的选择
  • (c) 友元函数没有 this 指针,所需操作数都必须在函数的参数表中显式声明,这很容易实现类型的隐式转换
  • (d) 在 C++ 中不能用友元函数重载的运算符分别有:=[]()->
  • (e) 在 C++ 中不要重载 &&|| 运算符
  • (f) C++ 的运算符重载遵循函数重载的规则
  • (g) 除了 <<>> 运算符的重载必须使用友元函数之外,建议其他运算符的重载尽量都使用类成员函数,千万不要滥用友元函数,尤其是在类模板与友元函数一起使用的时候

运算符重载的综合使用案例

重载自定义数组类的各种运算符

在本案例中,自定义了数组类 Array,并使用类成员函数分别对 Array 类的 []===!= 运算符进行重载。

  • Array.h
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
#pragma once

#include <iostream>

using namespace std;

class Array {
public:
Array(int length);

Array(const Array& array);

~Array();

public:
int length();

public:
// 使用类成员函数重载 "[]" 数组下标运算符,用于数组元素的赋值和取值
int& operator[](int index);

// 使用类成员函数重载 "=" 运算符,用于数组之间的赋值
Array& operator=(const Array& array);

// 使用类成员函数重载 "==" 运算符,判断两个数组是否相同
bool operator==(const Array & array);

// 使用类成员函数重载 "!=" 运算符,判断两个数组是否不相同
bool operator!=(const Array& array);

private:
int m_length;
int* m_space;
};
  • Array.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
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
#include "Array.h"

Array::Array(int length) {
cout << "有参构造函数被调用" << endl;
if (length < 0) {
length = 0;
}
this->m_length = length;
this->m_space = new int[length];
}

Array::Array(const Array& array) {
cout << "拷贝构造函数被调用" << endl;
// 深拷贝,单独分配内存空间
this->m_length = array.m_length;
this->m_space = new int[array.m_length];
for (int i = 0; i < array.m_length; i++) {
this->m_space[i] = array.m_space[i];
}
}

Array::~Array() {
cout << "析构函数被调用" << endl;
if (this->m_space != NULL) {
delete[] this->m_space;
this->m_space = NULL;
this->m_length = 0;
}
}

// 使用类成员函数重载 "[]" 数组下标运算符,用于数组元素的赋值和取值
int& Array::operator[](int index) {
return this->m_space[index];
}

// 使用类成员函数重载 "=" 运算符,用于数组之间的赋值
Array& Array::operator=(const Array& array) {
if (this->m_space != NULL) {
delete[] this->m_space;
this->m_space = NULL;
this->m_length = 0;
}
// 深拷贝,单独分配内存空间
this->m_length = array.m_length;
this->m_space = new int[array.m_length];
for (int i = 0; i < array.m_length; i++) {
this->m_space[i] = array.m_space[i];
}
return *this;
}

// 使用类成员函数重载 "==" 运算符,判断两个数组是否相同
bool Array::operator==(const Array& array) {
if (this->m_length != array.m_length) {
return false;
}
for (int i = 0; i < this->m_length; i++) {
if (this->m_space[i] != array.m_space[i]) {
return false;
}
}
return true;
}

// 使用类成员函数重载 "!=" 运算符,判断两个数组是否不相同
bool Array::operator!=(const Array& array) {
return !(*this == array);
}

int Array::length() {
return this->m_length;
}
  • main.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include "Array.h"

using namespace std;

int main() {

// 自动调用构造函数
Array array1(5);

for (int i = 0; i < array1.length(); i++) {
array1[i] = i;
}

for (int i = 0; i < array1.length(); i++) {
cout << "array1[" << i << "] = " << array1[i] << endl;
}

// 自动调用拷贝构造函数(属于深拷贝)
Array array2 = array1;

for (int i = 0; i < array2.length(); i++) {
cout << "array2[" << i << "] = " << array2[i] << endl;
}

// 自动调用拷贝构造函数(属于深拷贝)
Array array3 = array1;

// 不会自动调用拷贝构造函数(属于浅拷贝)
// 默认情况下,若这里不对 "=" 运算符进行重载,最终程序会异常终止运行(由于同一块内存空间被释放两次导致)
array3 = array2;
for (int i = 0; i < array3.length(); i++) {
cout << "array3[" << i << "] = " << array3[i] << endl;
}

// 判断两个数组是否相同
bool result1 = array1 == array2;
string strResult1 = result1 ? "=" : "!=";
cout << "array1 " << strResult1 << " array2 " << endl;

// 判断两个数组是否不相同
bool result2 = array1 != array2;
string strResult2 = result2 ? "!=" : "=";
cout << "array1 " << strResult2 << " array2 " << 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
有参构造函数被调用
array1[0] = 0
array1[1] = 1
array1[2] = 2
array1[3] = 3
array1[4] = 4
拷贝构造函数被调用
array2[0] = 0
array2[1] = 1
array2[2] = 2
array2[3] = 3
array2[4] = 4
拷贝构造函数被调用
array3[0] = 0
array3[1] = 1
array3[2] = 2
array3[3] = 3
array3[4] = 4
array1 = array2
array1 = array2
析构函数被调用
析构函数被调用
析构函数被调用

重载自定义字符串类的各种运算符

在本案例中,自定义了字符串类 MyString,并使用类成员函数和友元函数分别对 MyString 类的 []===!=><>><< 运算符进行重载。

  • MyString.h
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
#pragma once

#include <iostream>
#include "string.h"

using namespace std;

class MyString {

public:
MyString();
MyString(int len);
MyString(const char* p);
MyString(const MyString& str);

public:
~MyString();

public:
// 使用类成员函数重载 "[]" 运算符
char& operator[](int index);

// 使用类成员函数重载 "=" 运算符
MyString& operator=(const char* p);
MyString& operator=(const MyString& str);

// 使用类成员函数重载 "==" 运算符
bool operator==(const char* p) const;
bool operator==(const MyString str) const;

// 使用类成员函数重载 "!=" 运算符
bool operator!=(const char* p) const;
bool operator!=(const MyString str) const;

// 使用类成员函数重载 ">" 运算符
bool operator>(const char* p) const;
bool operator>(const MyString str) const;

// 使用类成员函数重载 "<" 运算符
bool operator<(const char* p) const;
bool operator<(const MyString str) const;

// 使用友元函数重载 "<<" 运算符
friend ostream& operator<<(ostream& out, MyString& str);

// 使用友元函数重载 ">>" 运算符
friend iostream& operator>>(iostream& in, MyString& str);

public:
int length();
char* c_str();

private:
int m_length;
char* m_space;
};
  • MyString.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
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include "MyString.h"

// 无参构造函数
MyString::MyString() {
// 初始化为空字符串
this->m_length = 0;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, "");
}

// 有参构造函数
MyString::MyString(int len) {
if (len < 0) {
len = 0;
}
// 初始化为空字符串
this->m_length = len;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, "");
}

// 有参构造函数
MyString::MyString(const char* p) {
if (p == NULL) {
// 初始化为空字符串
this->m_length = 0;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, "");
}
else {
this->m_length = strlen(p);
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, p);
}
}

// 拷贝构造函数
MyString::MyString(const MyString& str) {
// 深拷贝,重新分配内存空间
this->m_length = str.m_length;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, str.m_space);
}

// 析构函数
MyString::~MyString() {
// 释放内存空间
if (this->m_space != NULL) {
delete[] this->m_space;
this->m_space = NULL;
this->m_length = 0;
}
}

// 使用类成员函数重载 "[]" 运算符
char& MyString::operator[](int index) {
return this->m_space[index];
}

// 使用类成员函数重载 "=" 运算符
MyString& MyString::operator=(const char* p) {
// 释放内存空间
if (this->m_space != NULL) {
delete[] this->m_space;
this->m_space = NULL;
this->m_length = 0;
}
// 深拷贝,重新分配内存空间
if (p == NULL) {
// 初始化为空字符串
this->m_length = 0;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, "");
}
else {
this->m_length = strlen(p);
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, p);
}
return *this;
}

// 使用类成员函数重载 "=" 运算符
MyString& MyString::operator=(const MyString& str) {
// 释放内存空间
if (this->m_space != NULL) {
delete[] this->m_space;
this->m_space = NULL;
this->m_length = 0;
}
// 深拷贝,重新分配内存空间
this->m_length = str.m_length;
this->m_space = new char[this->m_length + 1];
strcpy(this->m_space, str.m_space);
return *this;
}

// 使用类成员函数重载 "==" 运算符
bool MyString::operator==(const char* p) const {
if (p == NULL) {
if (this->m_length == 0) {
return true;
}
return false;
}
if (this->m_length != strlen(p)) {
return false;
}
return !strcmp(this->m_space, p);
}

bool MyString::operator==(const MyString str) const {
if (this->m_length != str.m_length) {
return false;
}
return !strcmp(this->m_space, str.m_space);
}

// 使用类成员函数重载 "!=" 运算符
bool MyString::operator!=(const char* p) const {
return !(*this == p);
}

bool MyString::operator!=(const MyString str) const {
return !(*this == str);
}

// 使用类成员函数重载 ">" 运算符
bool MyString::operator>(const char* p) const {
return strcmp(p, this->m_space) < 0;
}

bool MyString::operator>(const MyString str) const {
return strcmp(str.m_space, this->m_space) < 0;
}

// 使用类成员函数重载 "<" 运算符
bool MyString::operator<(const char* p) const {
return strcmp(this->m_space, p) < 0;
}

bool MyString::operator<(const MyString str) const {
return strcmp(this->m_space, str.m_space) < 0;
}

// 使用友元函数重载 "<<" 运算符
ostream& operator<<(ostream& out, MyString& str) {
out << str.m_space;
return out;
}

// 使用友元函数重载 ">>" 运算符
iostream& operator>>(iostream& in, MyString& str)
{
in >> str.m_space;
return in;
}

int MyString::length()
{
return this->m_length;
}

char* MyString::c_str() {
return this->m_space;
}
  • main.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
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
#include "MyString.h"

int main() {

// 自动调用有参构造函数
MyString str1("Tom");
MyString str2(NULL);
MyString str3("Peter");

// 自动调用拷贝构造函数
MyString str4 = str1;

// 重载 "<<" 运算符
cout << "str2 = " << str2 << endl;
cout << "str4 = " << str4 << endl;
cout << endl;

// 不会自动调用拷贝构造函数(属于浅拷贝)
// 重载 "=" 运算符,实现深拷贝
str4 = str3;
cout << "str4 = " << str4 << endl;
str4 = "Jim";
cout << "str4 = " << str4 << endl;
str4 = NULL;
cout << "str4 = " << str4 << endl;
cout << endl;

// 重载 "[]" 运算符
MyString str5("David");
str5[0] = 'F';
cout << "str5[0] = " << str5[0] << endl;
cout << "str5 = " << str5 << endl;
cout << endl;

// 重载 "==" 运算符
MyString str6("Aaron");
MyString str7 = str6;
cout << str6 << (str6 == str7 ? " = " : " != ") << str7 << endl;

// 重载 "!=" 运算符
cout << str6 << (str6 != NULL ? " != " : " = ") << " NULL" << endl;
cout << endl;

// 重载 "<" 运算符
MyString str8("AAAA");
MyString str9("BBBB");
cout << str8 << (str8 < str9 ? " < " : " > ") << str9 << endl;
cout << str8 << (str8 < "CCCC" ? " < " : " > ") << "CCCC" << endl;

// 重载 ">" 运算符
cout << str9 << (str9 > str8 ? " > " : " < ") << str8 << endl;
cout << str9 << (str9 > "DDDD" ? " > " : " < ") << "DDDD" << endl;
cout << endl;

// 重载 ">>" 运算符
MyString str11(5);
cout << "请输入长度为 5 的字符串:" << endl;
cin >> str11.c_str();
cout << "str11 = " << str11 << endl;

// MyString str4 = NULL; 此写法,会自动调用有参构造函数 `MyString(const char* p);`

// MyString str1("AB");
// MyString str2 = str1;
// str2 = NULL: 此写法,会自动调用 "=" 运算符重载的函数 `bool operator==(const char* p) const;`

return 0;
}

程序运行的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
str2 =
str4 = Tom

str4 = Peter
str4 = Jim
str4 =

str5[0] = F
str5 = Favid

Aaron = Aaron
Aaron != NULL

AAAA < BBBB
AAAA < CCCC
BBBB > AAAA
BBBB < DDDD

请输入长度为 5 的字符串:
abcde
str11 = abcde

C++ 运算符和结合性的附录

cplusplus-overload-9
cplusplus-overload-10