05.C++ 运算符重载

本节分为七个部分:

  1. 学习复数类 Complex
  2. 模拟实现 C++ 的 string 类
  3. string 字符串对象的迭代器
  4. vector 容器的迭代器 iterator 实现
  5. 什么是容器的迭代器失效问题
  6. new 和 delete 重载实现的对象池
  7. 深入理解 new 和 delete 的原理

什么是运算符重载

运算符重载实质还是一个 函数。
通过重载运算符,可以让类在一些场景下使用起来更加方便。

语法

1
返回值类型 operator op (参数);​​​​​​​

示例:

1
ClassType& operator= (const ClassType& src); // 重载 “=” 运算符

1. 学习复数类 Complex

复数是形如 a+b 的数,复数由实部和虚部构成,在 C++ 的模板库中由 complex 类,可以直接调用,包含在complex头文件中,再使用时应该添加 #include<complex>。下面介绍一些基本操作

1. 生成复数对象

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
#include <iostream>
#include <complex>
using namespace std;

int main()
{
complex<double>x1(2, 3);
complex<double>x2;
complex<double>x3(x1);

cout << "x1 = " << x1 << endl; // x1 = (2, 3)
cout << "x2 = " << x2 << endl; // x2 = (0, 0)
cout << "x3 = " << x3 << endl; // x3 = (2, 3)

x1.real(22); // 修改实数部
x1.imag(33); // 修改虚数部

cout << "x1 = " << x1 << endl; // x1 = (22,33)

complex<double>a, b, c;
cout << "请输入三个复数:";
cin >> a >> b >> c;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;

return 0;
}

image.png

2. 复数的运算

复数和实数一样都有加减乘除四则运算,这些运算符号在 complex 模板中已经被重载过,能够直接使用,下面代码示例展示了其功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <complex>
using namespace std;

int main()
{
complex<double> z(2, 3);
complex<double> z1 = z + 5.0;
cout << z1 << endl;
complex<double> z2 = z - 5.0;
cout << z2 << endl;
complex<double> z3 = z * 2.0;
cout << z3 << endl;
complex<double>z4 = z / complex<double>(1, 1);
cout << z4 << endl;

return 0;
}

image.png

3. 复数的比较

复数的比较需要同时比较实部和虚部是否都相等,有其一不等两数就 不等,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <complex>
using namespace std;

int main()
{
complex<double> z1(2, 3);
complex<double> z2(3, 4);
complex<double> z3(2, 3);

cout << boolalpha << (z1 == z3) << endl;
cout << boolalpha << (z1 == z2) << endl;

return 0;
}

image.png

4. 实现 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
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
#include <iostream>
//#include <complex>
using namespace std;

// 定义Complex 类
template<typename T>
class Complex
{
public:
// 无参构造函数
Complex()
: real(0)
, imag(0)
{
// 初始化列表
}
// 有参构造函数
Complex(T re, T im)
: real(re)
, imag(im)
{
// 初始化列表
}
// 拷贝构造
Complex(const Complex<T>& com)
: real(com.real)
, imag(com.imag)
{
// 初始化列表
}
// 析构
~Complex()
{
// ......
}

// 方法
// 1. printComplex 方法
void printComplex()
{
cout << "(" << real << ", " << imag << ")" << endl;
}
// 2. + 方法重载:复数的加法
Complex& operator+(const Complex<T>& com)
{
this->real += com.real;
this->imag += com.imag;
return *this;
}
// 3. - 方法重载:复数的减法
Complex& operator-(const Complex<T>& com)
{
this->real -= com.real;
this->imag -= com.imag;
return *this;
}
// 4. * 方法重载:复数的乘法
Complex& operator*(const Complex<T>& com)
{
this->real = (real * com.real) - (imag * com.imag);
this->imag = (real * com.real) + (imag * com.imag);
return *this;
}
// 5. / 方法重载:复数的除法
Complex& operator/(const Complex<T>& com)
{
this->real = (real * com.real + imag * com.imag) / (com.real * com.real + com.imag * com.imag);
this->real = (real * com.real - imag * com.imag) / (com.real * com.real + com.imag * com.imag);
return *this;
}

private:
T real; // 实部
T imag; // 虚部
};

int main()
{
Complex<double> c1(2.0, 1.0);
c1.printComplex();
Complex<double> c2(1.0, 3.0);
c2.printComplex();
cout << "----------------" << endl;

c1 = c1 + c2;
cout << "c1 加法后:";
c1.printComplex();
cout << "----------------" << endl;

c2 = c2 - c1;
cout << "c2 减法后:";
c2.printComplex();
cout << "----------------" << endl;

c1 = c2 * c1;
cout << "c1 乘法后:";
c1.printComplex();
cout << "----------------" << endl;

c2 = c2 / c1;
cout << "c2 除法后:";
c2.printComplex();

return 0;
}

image.png

另一种实现方式:

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
#include <iostream>
using namespace std;

class CComplex
{
public:
CComplex(int r = 0, int i = 0)
:_mreal(r)
,_mimage(i)
{
}
void operator=(const CComplex&obj)
{
this->_mreal = obj._mreal;
this->_mimage = obj._mimage;
}
//指导编译器怎么做CComplex类对象的加法操作
/*CComplex operator+(const CComplex&com)
{
return CComplex(this->_mreal + com._mreal,
this->_mimage + com._mimage);
}*/
CComplex operator++(int)
{
return CComplex(this->_mreal++, this->_mimage++);
/*CComplex comp = *this;
this->_mimage++;
this->_mreal++;
return comp;*/
}
CComplex& operator++()
{
_mreal += 1;
_mimage += 1;
return *this;
}
void operator+=(const CComplex&rhs)
{
this->_mreal += rhs._mreal;
this->_mimage += rhs._mimage;
}
void show() { cout << "real:" << _mreal << "image:" << _mimage << endl; }
private:
int _mreal;
int _mimage;
friend CComplex operator+(const CComplex &lhs, const CComplex &rhs);
friend ostream& operator<<(ostream&out, const CComplex&src);
friend istream& operator>>(istream&in, CComplex&src);
};
istream& operator>>(istream&in, CComplex&src)
{
int a, b;
in >> a >> b;
src._mreal = a;
src._mimage = b;
return in;
}
ostream& operator<<(ostream&out, const CComplex&src)
{
out << "real:" << src._mreal << "image:" << src._mimage << endl;
return out;
}
CComplex operator+(const CComplex &lhs, const CComplex &rhs)
{
return CComplex(lhs._mreal + rhs._mreal, lhs._mimage + rhs._mimage);
}

int main()
{
CComplex c1(1, 2);
CComplex c2(2, 3);
CComplex c4;

c4 = c1+c2;
c4.show();

c4 = c1 + 20;
c4.show();

c4 = 30 + c2;
c4.show();

CComplex c5;
c5 = c4++;
c5.show();

c5 = ++c4;
c5.show();

c5 += c4;
c5.show();
cout << "++++++++++++++++++++++++++++++" << endl;

cout << c5;

CComplex c6;
cin >> c6;
cout << c6;

return 0;
}

2. 模拟实现 C++ 的 string 类

string 类中的基本方法有:

  1. 构造函数
    • 运算符重载
  2. << 运算符重载
  3. 运算符重载

  4. len 方法
  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
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;

int main()
{
//默认构造函数
string str1;
string str2 = "aaa";
string str3 = "bbb";

//+ 运算符重载
string str4 = str2 + str3;
string str5 = str2 + "ccc";
string str6 = "ddd" + str2;

// << 运算符重载
cout << "str6:" << str6 << endl;

// > 运算符重载
if (str5 > str6)
{
cout << str5 << " > " << str6 << endl;
}
else
{
cout << str5 << " < " << str6 << endl;
}

// 获取长度
int len = str6.length();
for (int i = 0; i < len; i++)
{
// []运算符重载
cout << str6[i] << " ";
}
cout << endl;

// c_str()
char buff[1024] = { 0 };
strcpy(buff, str6.c_str());
cout << "buff:" << buff << endl;

// 迭代器实现
string::iterator it = str2.begin();
for (it = str2.begin(); it != str2.end(); ++it) {
cout << (*it) << " ";
}

return 0;
}

image.png

1. 实现 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
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <ostream>
using namespace std;

// 创建 string 类
class String
{
public:
// 默认构造函数
String(const char* p = nullptr)
{
if (p != nullptr)
{
// 创建
_pstr = new char[strlen(p) + 1];
// 拷贝
strcpy(_pstr, p);
}
else
{
_pstr = new char[1];
*_pstr = '\0';
}
}
// 拷贝构造函数
String(const String& str)
{
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
// 析构函数
~String()
{
delete[] _pstr;
_pstr = nullptr;
}

// 运算符重载
// 1. =
String& operator=(const String& str)
{
if (this == &str)
{
return *this;
}
delete[] _pstr;

// 创建
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
return *this;
}
// 2. >
bool operator>(const String& str) const
{
return strcmp(_pstr, str._pstr) > 0;
}
// 3. <
bool operator<(const String& str) const
{
return strcmp(_pstr, str._pstr) < 0;
}
// 4. ==
bool operator==(const String& str) const
{
return strcmp(_pstr, str._pstr) == 0;
}
// 5. []
char& operator[](int index)
{
return _pstr[index];
}
const char& operator[](int index) const
{
return _pstr[index];
}

// 公共方法
int length()
{
return strlen(_pstr);
}
const char* c_str() const
{
return _pstr;
}


private:
char* _pstr;

// 由于调用 << 不依赖于特定的对象,设计为全局函数
// 为了方位到类型的私有数据(private),重载方法定义为该类型的友元函数。
friend ostream& operator<<(ostream& out, const String& str);
/************************************************************************/
/*
重载+;
重载+ 有两种方式:
(1)定义成成员函数
String operator+(const String& s){
String tmp;
.....
return tmp;
如 s1 = s2+s3; => s2.operator+(s3);在运算符重载中,将计算的值返回,而不是修改*this
}
弊端:s1 = "a" + s2;无法重载+ ,因为"a"不是String类型。
(2)重载为全局函数
*/
/************************************************************************/
friend String operator+(const String& lhs, const String& rhs);

};

ostream& operator<<(ostream& out, const String& str)
{
out << str._pstr;
return out;
};
/*
合并后的字符串是否有足够的空间容纳,我们会发现,前面的相关构造函数都是通过strlen()方法计算实参的大小,
并以此开辟空间大小,
换句话说:有多大开辟多大,一点不剩。所以,将后一个字符串连接到前一个字符串的时候,就会出现空间不够的问题。
*/
String operator+(const String& lhs, const String& rhs)
{
// 计算长度
char* _ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
// 拷贝
strcpy(_ptmp, lhs._pstr);
// 合并
strcat(_ptmp, rhs._pstr);

String tmp(_ptmp);
delete[]_ptmp;

return tmp;
};

int main()
{
//默认构造函数
String str1;
String str2 = "aaa";
String str3 = "bbb";

//+ 运算符重载
String str4 = str2 + str3;
String str5 = str2 + "ccc";
String str6 = "ddd" + str2;

// << 运算符重载
cout << "str6:" << str6 << endl;

// > 运算符重载
if (str5 > str6)
{
cout << str5 << " > " << str6 << endl;
}
else
{
cout << str5 << " < " << str6 << endl;
}

// 获取长度
int len = str6.length();
for (int i = 0; i < len; i++)
{
// []运算符重载
cout << str6[i] << " ";
}
cout << endl;

// c_str()
char buff[1024] = { 0 };
strcpy(buff, str6.c_str());
cout << "buff:" << buff << endl;

//
// string::iterator it = str2.begin();
// for (it = str2.begin(); it != str2.end(); ++it) {
// cout << (*it) << " ";
//}

return 0;
}

存在的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
String operator+(const String& lhs, const String& rhs)
{
// 计算长度
char* _ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
// 拷贝
strcpy(_ptmp, lhs._pstr);
// 合并
strcat(_ptmp, rhs._pstr);

String tmp(_ptmp);
delete[]_ptmp;

return tmp;
};

为了将局部新开辟的内存空间 _ptmp 资源释放,我们后面又构造了一个临时对象 String tmp(_ptmp),之后将局部变量资源释放 delete[]_ptmp,但是这样一来的效率非常低!

3. string 字符串对象的迭代器

容器的迭代器可以透明的访问容器内部的元素的值

1
2
3
4
string::iterator it = str2.begin();
for (it = str2.begin(); it != str2.end(); ++it) {
cout << (*it) << " ";
}

迭代器代码:

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
// 迭代器创建
class iterator
{
public:
// 构造函数
iterator(char *p = nullptr) : _p(p) { }
// 拷贝函数
iterator(const iterator& iter) : _p(iter._p) { }

// 重载运算符
// != 如 it != s1.end()
bool operator!=(const iterator& it) {
return _p != it._p;//比较地址
}
//前置++ 返回引用:效率更高,因为后置++ 会产生临时变量
iterator& operator++() {
++_p;
return *this;
}
//后置++ 返回临时量
iterator operator++(int) {
iterator tmp(*this);
_p++;
return tmp;
}
//解引用 *iter
char& operator*() { return *_p; }

private:
char* _p;
};

// 创建迭代器的 begin 和 end
iterator begin() { return iterator(_pstr); }
iterator end() { return iterator(_pstr + length()); }

完整代码:

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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <ostream>
using namespace std;

// 创建 string 类
class String
{
public:
// 迭代器创建
class iterator
{
public:
// 构造函数
iterator(char *p = nullptr) : _p(p) { }
// 拷贝函数
iterator(const iterator& iter) : _p(iter._p) { }

// 重载运算符
// != 如 it != s1.end()
bool operator!=(const iterator& it) {
return _p != it._p;//比较地址
}
//前置++ 返回引用:效率更高,因为后置++ 会产生临时变量
iterator& operator++() {
++_p;
return *this;
}
//后置++ 返回临时量
iterator operator++(int) {
iterator tmp(*this);
_p++;
return tmp;
}
//解引用 *iter
char& operator*() { return *_p; }

private:
char* _p;
};

// 创建迭代器的 begin 和 end
iterator begin() { return iterator(_pstr); }
iterator end() { return iterator(_pstr + length()); }


// 默认构造函数
String(const char* p = nullptr)
{
if (p != nullptr)
{
// 创建
_pstr = new char[strlen(p) + 1];
// 拷贝
strcpy(_pstr, p);
}
else
{
_pstr = new char[1];
*_pstr = '\0';
}
}
// 拷贝构造函数
String(const String& str)
{
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
// 析构函数
~String()
{
delete[] _pstr;
_pstr = nullptr;
}

// 运算符重载
// 1. =
String& operator=(const String& str)
{
if (this == &str)
{
return *this;
}
delete[] _pstr;

// 创建
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
return *this;
}
// 2. >
bool operator>(const String& str) const
{
return strcmp(_pstr, str._pstr) > 0;
}
// 3. <
bool operator<(const String& str) const
{
return strcmp(_pstr, str._pstr) < 0;
}
// 4. ==
bool operator==(const String& str) const
{
return strcmp(_pstr, str._pstr) == 0;
}
// 5. []
char& operator[](int index)
{
return _pstr[index];
}
const char& operator[](int index) const
{
return _pstr[index];
}

// 公共方法
int length()
{
return strlen(_pstr);
}
const char* c_str() const
{
return _pstr;
}


private:
char* _pstr;

// 由于调用 << 不依赖于特定的对象,设计为全局函数
// 为了方位到类型的私有数据(private),重载方法定义为该类型的友元函数。
friend ostream& operator<<(ostream& out, const String& str);
/************************************************************************/
/*
重载+;
重载+ 有两种方式:
(1)定义成成员函数
String operator+(const String& s){
String tmp;
.....
return tmp;
如 s1 = s2+s3; => s2.operator+(s3);在运算符重载中,将计算的值返回,而不是修改*this
}
弊端:s1 = "a" + s2;无法重载+ ,因为"a"不是String类型。
(2)重载为全局函数
*/
/************************************************************************/
friend String operator+(const String& lhs, const String& rhs);

};

ostream& operator<<(ostream& out, const String& str)
{
out << str._pstr;
return out;
};
/*
合并后的字符串是否有足够的空间容纳,我们会发现,前面的相关构造函数都是通过strlen()方法计算实参的大小,
并以此开辟空间大小,
换句话说:有多大开辟多大,一点不剩。所以,将后一个字符串连接到前一个字符串的时候,就会出现空间不够的问题。
*/
String operator+(const String& lhs, const String& rhs)
{
// 计算长度
char* _ptmp = new char[strlen(lhs._pstr) + strlen(rhs._pstr) + 1];
// 拷贝
strcpy(_ptmp, lhs._pstr);
// 合并
strcat(_ptmp, rhs._pstr);

String tmp(_ptmp);
delete[]_ptmp;

return tmp;
};

int main()
{
//默认构造函数
String str1;
String str2 = "aaa";
String str3 = "bbb";

//+ 运算符重载
String str4 = str2 + str3;
String str5 = str2 + "ccc";
String str6 = "ddd" + str2;

// << 运算符重载
cout << "str6:" << str6 << endl;

// > 运算符重载
if (str5 > str6)
{
cout << str5 << " > " << str6 << endl;
}
else
{
cout << str5 << " < " << str6 << endl;
}

// 获取长度
int len = str6.length();
for (int i = 0; i < len; i++)
{
// []运算符重载
cout << str6[i] << " ";
}
cout << endl;

// c_str()
char buff[1024] = { 0 };
strcpy(buff, str6.c_str());
cout << "buff:" << buff << endl;


// 迭代器
String::iterator it = str2.begin();
for (it = str2.begin(); it != str2.end(); ++it) {
cout << (*it) << " ";
}

// auto
for (char ch : str2) {
cout << ch << " ";
}

return 0;
}

1. 代码优化

带右值引用参数的拷贝构造与带右值引用参数的赋值重载函数

考虑这样一个操作:

由于问题场景的特殊,子函数调用时我们无法返回一个临时对象。
而且我们也只能用赋值的方式接收一个函数调用的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String GetString(const String&str) {
std::cout << "\n GetString() begin\n";
String tmp(str.c_str());
std::cout << "\n GetString() end\n";
return tmp;
}


int main()
{
String s1 = "123";
String s2;

std::cout << "\n main call GetString() begin\n";
s2 = GetString(s1);
std::cout << "\n main call GetString() end\n";
return 0;
}

image.png

存在的问题:

(1)在调用 return tmp; 时 实际是使用构造一个临时对象,临时对象调用拷贝构造函数从 tmp 中复制数据,然后调用析构函数析构 tmp 对象,这样就白耗费资源了,tmpStr 资源不要就早说,直接给函数栈帧上临时对象就好了;
(2)s2 调用赋值运算符 :临时量给 str2 赋值,str2 是原本已经存在的对象,它也有一个指针 mptr,原先也指向了一个空间。

对于赋值来说,排除自赋值,然后把原先指向的空间释放掉,然后按照 str 的尺寸开辟空间,然后拷贝数据进行来。即按照临时对象的字符串大小开辟空间,然后把数据一个一个拷贝进来。

然后出语句,把这个临时对象析构及它指向的堆内存空间释放掉。

过程过于复杂,为什么不能直接把临时对象的外部资源给 str2 不就完了吗?

2. 右值引用

1
2
3
//C++11把临时量都当做右值处理 
String &&e = String("aaa");//可以!
const String &e = MyString("aaa");//可以!
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
  /************************************************************************/
/*拷贝构造函数 //String s2 = s1;*/
/************************************************************************/
String(const String& str)
{
if (str._pstr !=nullptr) {
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
std::cout << "String 左值拷贝构造函数\n";
}

//带右值引用参数的拷贝构造
String(String &&str)//str引用的就是一个临时对象
{
std::cout << "String 右值拷贝构造函数\n";
_pstr = str._pstr;
str._pstr = nullptr;
}

/************************************************************************/
/*赋值运算符 //s2 = s1*/
/************************************************************************/
String& operator=(const String& src)
{
std::cout << "String 左值赋值运算符\n";
if (this == &src)
return *this;

if (src._pstr == nullptr) {
return *this;
}

if (_pstr) {
delete[]_pstr;
}


_pstr = new char[strlen(src._pstr) + 1];
strcpy(_pstr, src._pstr);
return *this;
}

//带右值引用参数的赋值重载函数
String& operator=(String &&str)//str引用的是临时对象
{
std::cout << "String 右值引用参数的赋值重载函数\n";
if (this == &str)
return *this;

if (str._pstr == nullptr) {
return *this;
}

delete[]_pstr;

char * tmp=_pstr ;
_pstr = str._pstr;
str._pstr = tmp;
return *this;
}

通过右值引用,没有任何内存的开辟和释放和数据的拷贝

image.png

右值引用前:

image.png

右值引用后:

image.png

4. vector 容器的迭代器 iterator 实现

问题 1:删除 vector 中所有的偶数:

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
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec;
int i = 0;
for (i = 0; i < 10; i++)
{
vec.push_back(i);
}

// 删除偶数
auto it = vec.begin();
for (; it < vec.end(); it++)
{
if ((*it) % 2 == 0)
{
vec.erase(it);
}
}

return 0;
}

image.png

运行导致程序崩溃!

问题 2:vector 容器插入元素问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec;
int i = 0;
for (i = 0; i < 10; i++)
{
vec.push_back(i);
}

// 把vec容器中的所有偶数前面添加一个小于偶数值1的数字
vector<int>::iterator it = vec.begin();
for (; it < vec.end(); it++)
{
if ((*it) % 2 == 0) {
vec.insert(it, *it - 1);
}
}

return 0;
}

image.png

运行导致程序崩溃!

问题 3:push_back 触发扩容时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vector<int>::iterator it = vec.begin();

cout << "it = " << *it << endl;
cout << "容量:" << vec.capacity() << endl;

vec.push_back(3);
vec.push_back(4);

cout << "后push_back容量:" << vec.capacity() << endl;
cout << "添加后,it = " << *it << endl;

return 0;
}

image.png

原因:iterator 失效

image.png

当删除(获取增加)it 位置的元素时,导致 it 后面的迭代器全部失效。因此多次调用 erase\insert导致崩溃

5. 什么是容器的迭代器失效问题

1. 原因

问题一:

当容器调用 erase 时,当前位置到容器末尾元素的所有的迭代器全部失效

问题二:

当容器调用 insert 时,当前位置到容器末尾元素的所有的迭代器全部失效

当容器调用 insert 时,如果引起容器内存扩容,原来容器的所有的迭代器就全部失效

问题三:

当容器 push_back 时,如果当前容量不足,则会触发扩容,导致整个容器重新申请内存,并且将原有的数据复制到新内存中将原有内存释放,这自然是会导致迭代器失效的,因为迭代器所指的内存都已经被释放。

2. 解决

进行更新操作:erase\insert 后会返回指向下一个元素的迭代器

image.png

从向量中删除单个元素(位置)或一系列元素([第一、最后一个])。

这有效地减少了容器的大小,减少了被删除的元素的数量,这些元素会被销毁。

由于向量使用数组作为其底层存储,擦除向量端以外位置的元素会导致容器在段擦除后将所有元素重新定位到其新位置。与其他类型的序列容器对相同操作执行的操作相比,这通常是一种低效的操作(如列表或转发列表)。

同理,insert

image.png

通过在指定位置的元素之前插入新元素来扩展向量,从而通过插入的元素数量有效地增加容器大小。

当且仅当新向量大小超过当前向量容量时,这会导致自动重新分配分配分配的存储空间。

因为向量使用数组作为其底层存储,所以在向量末端以外的位置插入元素会导致容器将位置之后的所有元素重新定位到它们的新位置。与其他类型的序列容器(如 list 或 forward_list)对相同操作执行的操作相比,这通常是一种低效的操作。

这些参数确定插入的元素数量及其初始化值:

也说明了进行插入操作会导致之后的迭代器失效。

修改代码:

erase 解决代码:

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 <vector>
using namespace std;

int main()
{
vector<int> vec;
int i = 0;
for (i = 0; i < 10; i++)
{
vec.push_back(i);
}

// 把 vec 容器中的所有偶数删除
auto it = vec.begin();
while (it != vec.end())
{
if ((*it) % 2 == 0)
{
// 重新赋值
it = vec.erase(it);
}
else
{
it++;
}
}

// 输出查看
for (auto it : vec)
{
cout << it << " ";
}

return 0;
}

image.png

insert 解决代码:

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
#include <iostream>
#include <vector>
using namespace std;

int main()
{
vector<int> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}

// 把vec容器中的所有偶数前面添加一个小于偶数值1的数字
auto it = vec.begin();
for (; it != vec.end(); it++) {
if ((*it) % 2 == 0) {
it = vec.insert(it, *it - 1);
// it原来的位置插入了新的,需要++it两次,才能到该偶数的后一个元素
it++;
}
}

for (auto val : vec) {
cout << val << " ";
}

return 0;
}

image.png

3. vector 实现中的 insert 和 erase

头插法:

image.png

检查迭代器失效:

在进行删除或增加的时候,要检测该位置到 last 位置,使其迭代器失效

1
2
3
4
5
6
7
8
9
10
11
12
void pop_back() // 从容器末尾删除元素
{
if (empty())
return;

//检查迭代器 从该位置到最后
verify(Last-1,Last);

// 不仅要把_last指针--,还需要析构删除的元素
--Last;
_allocator.destroy(Last);
}

迭代器检查实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void verify(T* first, t* last)
{
Iterator_Base* pre = &this->head;
Iterator_Base* it = &this->head._next;

while (it != nullptr)
{
if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
{
// 迭代器失效,把 iterator 持有的容器指针置空
it->_cur->_pVec = nullptr;
// 删除当前迭代器节点,继续判断后面的迭代器是否失效
pre->_next = it->_next;
delete it;
it = pre->_next;
}
else
{
pre = it;
it = it->_next;
}
}
}

6. new 和 delete 重载实现的对象池

在编程中,我们经常会涉及到对象的操作,而经常的操作模式如下图所示:创建对象->使用对象->销毁对象

image.png

而这个对象有可能创建的时候会需要构建很多资源,消耗比较大, 比如:在hiredis的SDK中每次都创建一个redisContext,如果需要查询,那就首先要进行网络连接。如果一直都是上图的工作方式,那将会频繁的创建连接,查询完毕后再释放连接。重新建立连接,让网络的查询效率降低。

这个时候就可以构建一个对象池来重复利用这个对象,并且一般要做到线程安全:

  1. 对象池中获取对象,如果没有对象,则创建一个,并返回
  2. 使用对象
  3. 使用完成对象后,将对象还回对象池

image.png

那么符合如下条件的,应该适合使用对象池技术:

  • 有一些对象虽然创建开销比较大,但是不一定能够重复使用。要使用对象池一定要确保对象能够重复使用。
  • 这个对象构建的时候,有一些耗时的资源可以重复利用。比如redisContext的网络连接。又或者如果对象的频繁申请释放会带来一些其他的资源使用问题,比如内存碎片。重复利用能够提升程序的效率。
  • 对象池的数量应该控制在能够接受的范围内,并不会无限膨胀。

1. 对象池实现原理

  1. 分配过程:

我们首先申请比如 100000 块内存空间,用这个块类型的指针指向这个申请好内存的首地址。如图:这个块可以是一个链式队列的一个节点或者一个树的节点等。

image.png
当我们要分配给用户一块空间的时候我们就可以,先将 _itemPool 的地址保存到临时指针 p,然后再 _itemPool++ 的操作,然后再将 p 返回给用户即可。

image.png

  1. 释放过程:

比如现在的内存分配状况如图所示:现在用户需要归还第 1 块内存。

image.png

我们首先要归还块的首地址指向 _itemPool 的下一个块,然后再将 _itemPool 指向归还的块的地址。即可

image.png

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

using namespace std;

/*
运算符的重载 :成员方法、全局方法
内存池 进程池 线程池 连接池 对象池
*/
template<typename T>
class Queue
{
public:
// 构造函数
Queue()
{
// 初始化指针
_front = _rear = new QueueItem();
}
// 析构函数
~Queue()
{
// 创建新对象
QueueItem* cur = _front;
while (cur != nullptr)
{
_front = cur->_next;
delete cur;
cur = _front;
}
}
// 拷贝对象
Queue(const Queue& obj) = delete;

// 赋值重载
void operator=(const Queue& obj) = delete;

// 方法
// 1. push 方法
void push(const T& val)
{
QueueItem* item = new QueueItem(val);
_rear->_next = item;
_rear = item;
}
// 2. pop 方法
void pop()
{
if (empty())
{
return;
}
QueueItem* first = _front->_next;
_front->_next = first->_next;
if (_front->_next == nullptr)
{
_rear = _front;
}
delete first;
}
// 3. front 方法
T front() const
{
return _front->_next->_data;
}
// 4. empty 方法
bool empty()const { return _rear == _front; }

// 产生一个 QueueItem 的对象池
// 创建一个对象池结构
struct QueueItem
{
// 构造函数
QueueItem(T data = T()) :_data(data), _next(nullptr) {}

// new 重载
void* operator new(size_t size)
{
if (_itemPool == nullptr)
{
//由于我们不知道QueueItem的具体大小(模板未进行实例化)所以我们这里用char 1字节作为new的基准
_itemPool = (QueueItem*)new char[POOL_ITEM_SIZE * sizeof(QueueItem)];
QueueItem* p = _itemPool;
//把开辟的POOL_ITEM_SIZE个块关联起来
for (; p != _itemPool + POOL_ITEM_SIZE - 1; ++p)
{
p->_next = p + 1;
}
//最后一块的next置为空
p->_next = nullptr;
}
//将第一块可以分配的块分配给用户
QueueItem* p = _itemPool;
_itemPool = _itemPool->_next;
return p;
}
// delete 重载
void operator delete(void* ptr)
{
//进行归还操作
QueueItem* p = (QueueItem*)ptr;
p->_next = _itemPool;
_itemPool = p;
}

// 静态数据
static QueueItem* _itemPool;
static const int POOL_ITEM_SIZE = 1000000;

// 对象池单个对象
T _data;
QueueItem* _next;
};

private:
QueueItem* _front;//指向头节点
QueueItem* _rear;//指向队尾
};

//typename 告诉编译器后面是类型
template<typename T>
typename Queue<T>::QueueItem* Queue<T>::QueueItem::_itemPool = nullptr;
int main()
{
Queue<int> que;
for (int i = 0; i < 1000000; ++i)//大量的new delete;不划算
{
que.push(i);
que.pop();
}

cout << que.empty() << endl;

return 0;
}

7. 深入理解 new 和 delete 的原理

new和delete是用户进行动态内存申请和释放的操作符operator new 和operator delete是系统提供的全局函数new在底层调用operator new全局函数来申请空间,delete在底层通过operator 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
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}

return (p);
}

/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;

RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY

/* get a pointer to memory block header */
pHead = pHdr(pUserData);

/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

_free_dbg(pUserData, pHead->nBlockUse);

__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY

return;
}

/*
free的实现
*/

#define free(p) _free_dbg(p, _NORMAL_BLOCK)

1. 内置类型:

如果申请的是内置类型的空间 newmallocdeletefree 基本类似,不同的地方是:new/delete 申请和释放的是单个元素的空间,new[]delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc会返回NULL。

2. 自定义类型:

new的原理:

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理:

在空间上执行析构函数,完成对象中资源的清理工作在空间上执行析构函数,完成对象中资源的清理工作

new T[N] 的原理:

  1. 调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请
  2. 在申请的空间上执行N次构造函数

delete[ ]的原理:

  1. 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理
  2. 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间

05.C++ 运算符重载
http://example.com/2023/09/21/02.C++ 基础部分/05.C++ 运算符重载/
Author
Yakumo
Posted on
September 21, 2023
Licensed under