C++:各构造/析构函数以及背后机制
写出良好的构造和析构函数是 RAII 成功运转的基础。今天就说说怎么写,为什么这么写。
本文使用的示例类:
1class vec2d {
2 public:
3 int x;
4 int y;
5
6 std::string to_string() const {
7 std::stringstream ss;
8 ss << "vec2d{a:" << x << ", b:" << y << "}";
9 return ss.str();
10 }
11
12 // 其它构造函数、析构函数等等
13 // ...
14};
自定义构造函数
问: 如果不写构造函数,会初始化吗?
答:对于 POD 类型(主要是和 C 共有的那些)的成员,不会。
1 vec2d d1;
2 std::cout << d1.to_string() << std::endl;
在我的电脑上,得到:
1vec2d{a:-1865866784, b:32743}
显然是一个内存随机值。
但如果成员有无参构造函数的话,是会初始化的。例子:
1#include <iostream>
2#include <sstream>
3#include <string>
4
5class magic {
6 int a, b, c, d;
7
8 public:
9 magic() {
10 a = b = c = d = 0xf;
11 std::cout << "guid(" << a << ", " << b << ", " << c << ", " << d << ")"
12 << std::endl;
13 }
14 ~magic() {
15 std::cout << "~guid(" << a << ", " << b << ", " << c << ", " << d << ")"
16 << std::endl;
17 }
18};
19
20class vec2d {
21 public:
22 int x;
23 int y;
24 magic g;
25
26 vec2d(int x, int y) {
27 this->x = x;
28 this->y = y;
29 }
30};
31
32int main() {
33 vec2d d1{1, 2};
34 std::cout << d1.to_string() << std::endl;
35 return 0;
36}
输出:
guid(15, 15, 15, 15)
vec2d{a:1, b:2}
~guid(15, 15, 15, 15)
我们应该 能初始化则初始化:
1class vec2d {
2 public:
3 int x{0};
4 int y{0};
5
6 vec2d() { std::cout << this->to_string() << std::endl; }
7};
8
9int main() {
10 vec2d d1;
11 return 0;
12}
输出:
vec2d{a:0, b:0}
问: 使用赋值初始化和初始化列表的区别
为什么使用初始化表达式?
-
只需一次赋值
-
能初始化 const、引用 类成员
-
如果类成员没有无参构造函数,可以使用初始化表达式为其初始化
1 vec2d(int x, int y) {
2 this->x = x;
3 this->y = y;
4 }
这种方式会先调用 default 构造函数赋予初值,然后再执行我们的代码再次赋值。因此更好的做法是使用初始化列表:
1vec2d(int x, int y) : x(x), y(y) {
2}
注意:如果你使用初始化列表,务必保证初始化顺序和成员声明顺序的一致
但是,如果编译器做了相关优化,那么除了语义外,上述例子实际上二者没有区别。
除此之外,初始化列表允许常量初始化常量成员。
1class vec2d {
2 public:
3 int x;
4 int y;
5 const int magic;
6 // ...
7 vec2d(int x, int y) {
8 this->magic = 42; // 报错
9 this->x = x;
10 this->y = y;
11 }
1 vec2d(int x, int y) : magic(42) { // OK
2 this->x = x;
3 this->y = y;
4 }
问: 常成员不能用变量初始化吗?
未必。可以下面这么写,语义上表示 magic 的值对此实例而言是只读的。
1 vec2d(int x, int y, const int magic) : magic(magic) {
2 this->x = x;
3 this->y = y;
4 }
5
6 // main
7 int magic;
8 std::cin >> magic;
9 vec2d d1{1, 2, magic};
自动生成的构造析构函数
对于代码
1class vec2d {
2 public:
3 int x{0};
4 int y{0};
5
6 std::string to_string() const {
7 // ...
8 }
9};
10
11int main() {
12 vec2d d1 = {1, 2};
13 std::cout << d1.to_string() << std::endl;
14 vec2d d2{3, 4};
15 std::cout << d2.to_string() << std::endl;
16 return 0;
17}
如果你查看它的 AST(clang++ -cc1 -ast-dump main.cpp
),会发现:
|-CXXRecordDecl 0x14226c0 <main.cpp:5:1, line:19:1> line:5:7 referenced class vec2d definition
| |-DefinitionData pass_in_registers aggregate standard_layout trivially_copyable literal has_constexpr_non_copy_move_ctor can_const_default_init
| | |-DefaultConstructor exists non_trivial constexpr needs_implicit defaulted_is_constexpr
| | |-CopyConstructor simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveConstructor exists simple trivial needs_implicit
| | |-CopyAssignment simple trivial has_const_param needs_implicit implicit_has_const_param
| | |-MoveAssignment exists simple trivial needs_implicit
| | `-Destructor simple irrelevant trivial needs_implicit
这是因为 C++11 帮我们生成了五个构造函数 DefaultConstructor、CopyConstructor、MoveConstructor、CopyAssignment、MoveAssignment、Destructor。
初始化列表构造函数
实际上,除此 5 个之外 C++ 还会生成一个初始化列表函数。,因此上述程序输出:
vec2d{a:1, b:2}
vec2d{a:3, b:4}
此外在 C++11 中,不带有任何修饰符的析构函数,都会被编译器默认带上 “noexcept(true)” 标记,以表示这个析构函数不会抛出异常。
除了看编译器的中间输出,你也可以通过
#include <type_traits>
提供的库函数来判断。is_move_constructible<vec2d>
和is_move_assignable<vec2d>
可判断类是否具有移动构造函数和移动赋值函数。is_trivially_move_constructible<vec2d>
和is_trivially_move_assignable<vec2d>
可判断类是否具有平凡移动构造函数和平凡移动赋值函数。(关于 trivial 的标准,请参考 C++11 参考文档)is_nothrow_move_constructible<vec2d>
和is_nothrow_move_assignable<vec2d>
可判断类是否具有不抛出异常的移动构造函数和移动赋值函数。
DefaultConstructor
DCtor 默认构造函数,即无参构造函数。
当一个类没有定义任何构造函数,且所有成员都有无参构造函数时,编译器会自动生成一个无参构造函数 Pig(),他会调用每个成员的无参构造函数。
原型:vec2d();
当定义了构造函数后,默认的构造函数就不会被生成。如果你想要生成默认的无参构造函数:
1vec2d() = default;
CopyConstructor
CCtor 拷贝构造函数,
原型:vec2d(const vec2d&);
调用特征:
1vec2d v2 = v1; // v2 还未生成,v1 已经生成
和 DCtor 的区别:即便定义了自定义的 CCtor,编译器也会自动生成默认 CCtor。如何关闭?
1vec2d() = delete;
如何写?
1vec2d(const vec2d& v) : a(v.a), b(v.b) {}
上面也是默认生成的 CCtor 的形式.
除非是智能指针是浅拷贝,其它的拷贝均是深拷贝。
有的开发者会让类删除拷贝构造函数,以避免深拷贝。转而用智能指针管理这个类,这样这个类的拷贝就变成智能指针的浅拷贝,从而提高传递性能。
CopyAssignment
CAsgn 拷贝赋值函数。
原型:vec2d& operator=(const vec2d&);
调用特征:
1vec2d v2 = v1; // v2 已经生成,v1 已经生成。因为没有新的对象生成,所以被称为“赋值”
如何写?
1
2vec2d& vec2d::operator=(const vec2d& v) {
3 a = v.a;
4 b = v.b;
5 return *this; // 返回自身,这是为了能够进行连等赋值操作:v1 = v2 = v3;
6}
MoveConstructor
MCtor 移动构造函数
原型:vec2d(vec2d&& other);
调用特征:
1vec2d v2 = std::move(v1); // v1 已经生成,v2 尚未生成。将 v1 的所有权移交给 v2
怎么写?
1vec2d(vec2d&& v) : a(std::move(v.a)), b(std::move(v.b)) {}
MoveAssignment
MAsgn 移动赋值函数
原型:vec2d& operator=(vec2d&& other);
调用特征:
1vec2d v2 = std::move(v1); // v2 已经生成,v1 已经生成。因为没有新的对象生成,所以被称为“赋值”
怎么写?
1vec2d& vec2d::operator=(vec2d&& v) {
2 a = std::move(v.a);
3 b = std::move(v.b);
4 return *this;
5}
Destructor
Dtor 析构函数
实战:实现 vector
三五法则
1.如果一个类定义了解构函数,那么您必须同时定义或删除拷贝构造函数和拷贝赋值函数,否则出错。(因为解构意味着浅拷贝)
2.如果一个类定义了拷贝构造函数,那么您必须同时定义或删除拷贝赋值函数,否则出错,删除可导致低效。
3.如果一个类定义了移动构造函数,那么您必须同时定义或删除移动赋值函数,否则出错,删除可导致低效。
4.如果一个类定义了拷贝构造函数或拷贝赋值函数,那么您必须最好同时定义移动构造函数或移动赋值函数,否则低效。
口诀:解拷,拷拷,移移,拷拷移移
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
什么时候会触发 move?
1return v2 // v2 作返回值
2v1 = std::vector<int>(200) // 就地构造的 v2
3v1 = std::move(v2) // 显式地移动
这些情况下编译器会调用拷贝:
1return std::as_const(v2) // 显式地拷贝
2v1 = v2 // 默认拷贝
注意,以下语句没有任何作用(这俩东西只是语义上给编译器看的,所以你单独调用没有意义):
1std::move(v2) // 不会清空 v2,需要清空可以用 v2 = {} 或 v2.clear()
2std::as_const(v2) // 不会拷贝 v2,需要拷贝可以用 { auto _ = v2; }
这两个函数只是负责转换类型,实际产生移动/拷贝效果的是在类的构造/赋值函数里。
vector 实现
需要注意的几点:
-
拷贝赋值要记得先释放自己占有的内存
-
拷贝构造和拷贝赋值都尽量用 const 修饰 other
-
移动构造和移动赋值才是 move 的具体实现,所以要释放从 other 复制到 this 然后清空 other
-
不要滥用
std::move
:“std::move is used to indicate that an object t may be ‘moved from’”
1#include <chrono>
2#include <cstdlib>
3#include <cstring>
4#include <iostream>
5#include <map>
6#include <vector>
7
8namespace lb {
9class vector {
10 private:
11 size_t _size;
12 size_t _capacity;
13 int* _data{nullptr};
14 static const size_t _min_capicity{16};
15
16 public:
17 explicit vector(size_t size = 0) noexcept
18 : _size(size), _capacity(size > _min_capicity ? size : _min_capicity) {
19 _data = reinterpret_cast<int*>(calloc(_capacity, sizeof(int)));
20 if (size > 0) {
21 std::cout << "set zero " << _size << std::endl;
22 memset(_data, 0, size * sizeof(int));
23 }
24 }
25
26 vector(vector const& other) : _size(other._size), _capacity(other._capacity) {
27 _data = reinterpret_cast<int*>(calloc(_capacity, sizeof(int)));
28 memcpy(_data, other._data, _capacity * sizeof(int));
29 }
30
31 // NOT OK
32 // vector(vector const &other) {
33 // this->~vector();
34 // new (this) vector(other);
35 // }
36
37 vector(vector&& other)
38 : _size(other._size), _capacity(other._capacity), _data(other._data) {
39 other._data = nullptr;
40 other._size = 0;
41 other._capacity = 0;
42 }
43
44 vector& operator=(vector const& other) {
45 this->_size = other._size;
46 this->_capacity = other._capacity;
47 _data = reinterpret_cast<int*>(reallocarray(_data, _capacity, sizeof(int)));
48 memcpy(_data, other._data, _capacity * sizeof(int));
49 return *this;
50 }
51
52 // 也可以先释放再拷贝,但性能会差一点
53 // vector& operator=(vector const &other) {
54 // free(_data);
55
56 // this->_size = other._size;
57 // this->_capacity = other._capacity;
58 // _data = reinterpret_cast<int*>(calloc(_capacity, sizeof(int)));
59 // memcpy(_data, other._data, _capacity * sizeof(int));
60 // return *this;
61 // }
62
63 // 最偷懒的做法,但是没毛病
64 // vector& operator=(vector const &other) {
65 // this->~vector();
66 // new (this) vector(other);
67 // return *this;
68 // }
69
70 vector& operator=(vector&& other) {
71 this->_capacity = other._capacity;
72 other._capacity = 0;
73 this->_data = other._data;
74 other._data = nullptr;
75 this->_size = other._size;
76 other._size = 0;
77
78 return *this;
79 }
80
81 ~vector() noexcept { free(_data); }
82
83 size_t size() const noexcept { return _size; }
84
85 size_t capacity() const noexcept { return _capacity; }
86
87 void resize(size_t new_size, int value = 0) {
88 if (new_size > this->_capacity) {
89 this->_capacity = new_size;
90 this->_data = reinterpret_cast<int*>(
91 reallocarray(this->_data, new_size, sizeof(int)));
92 memset(this->_data + this->_size, value,
93 (new_size - this->_size) * sizeof(int));
94 } else {
95 memset(this->_data + new_size, value,
96 (this->_capacity - new_size) * sizeof(int));
97 }
98 this->_size = new_size;
99 }
100
101 void reserve(size_t new_capacity) {
102 if (new_capacity > _capacity) {
103 _data = reinterpret_cast<int*>(
104 reallocarray(_data, new_capacity, sizeof(int)));
105 _capacity = new_capacity;
106 }
107 }
108
109 int& operator[](size_t index) const {
110 if (index >= _size) {
111 throw std::out_of_range("index out of range");
112 }
113 return _data[index];
114 }
115
116 void push_back(int value) {
117 if (_size == _capacity) {
118 reserve(_capacity * 2);
119 }
120 _data[_size++] = value;
121 }
122
123 void insert(size_t pos, int value) {
124 if (pos == _size) {
125 push_back(value);
126 return;
127 }
128 if (pos > _size) {
129 throw std::out_of_range("index out of range");
130 }
131 if (_size == _capacity) {
132 reserve(_capacity << 1);
133 }
134 memmove(_data + pos + 1, _data + pos, (_size - pos) * sizeof(int));
135 _data[pos] = value;
136 _size++;
137 }
138
139 void erase(size_t pos) {
140 if (pos >= _size) {
141 throw std::out_of_range("index out of range: " + std::to_string(pos));
142 }
143 memmove(_data + pos, _data + pos + 1, (_size - pos - 1) * sizeof(int));
144 _size--;
145 if (_size < _capacity / 4) {
146 _data = reinterpret_cast<int*>(
147 reallocarray(_data, _capacity / 2, sizeof(int)));
148 _capacity >>= 1;
149 }
150 }
151
152 void clear() {
153 _size = 0;
154 _data = reinterpret_cast<int*>(calloc(_min_capicity, sizeof(int)));
155 }
156};
157} // namespace lb
158
159int sum(lb::vector& v) {
160 int ret = 0;
161 for (size_t i = 0; i < v.size(); i++) {
162 ret += v[i];
163 }
164 return ret;
165}
166
167bool test_vector_push() {
168 lb::vector v;
169 for (size_t i = 0; i < 100; i++) {
170 v.push_back(i);
171 }
172 return sum(v) == 4950;
173}
174
175bool test_vector_insert() {
176 lb::vector v;
177 int total = 0;
178 for (size_t i = 0; i < 100; i++) {
179 int num = rand() % 100;
180 total += num;
181 v.insert(i, num);
182 }
183 return sum(v) == total;
184}
185
186bool test_vector_erase() {
187 lb::vector v;
188 for (size_t i = 0; i < 100; i++) {
189 v.push_back(i);
190 }
191 for (size_t i = 0; i < 100; i++) {
192 int idx = rand() % v.size();
193 v.erase(idx);
194 }
195 return sum(v) == 0;
196}
197
198bool test_vector_clear() {
199 lb::vector v;
200 for (size_t i = 0; i < 100; i++) {
201 v.push_back(i);
202 }
203 v.clear();
204 return sum(v) == 0;
205}
206
207bool test_copy_constructor() {
208 {
209 lb::vector v;
210 for (size_t i = 0; i < 100; i++) {
211 v.push_back(i);
212 }
213 lb::vector v2(v);
214 }
215 return true;
216}
217
218bool test_move_constructor() {
219 {
220 lb::vector v;
221 for (size_t i = 0; i < 100; i++) {
222 v.push_back(i);
223 }
224 lb::vector v2(std::move(v));
225 }
226 return true;
227}
228
229bool test_copy_assignment() {
230 {
231 lb::vector v;
232 for (size_t i = 0; i < 100; i++) {
233 v.push_back(i);
234 }
235 lb::vector v2;
236 v2 = v;
237 }
238 return true;
239}
240
241bool test_move_assignment() {
242 {
243 lb::vector v;
244 for (size_t i = 0; i < 100; i++) {
245 v.push_back(i);
246 }
247 lb::vector v2;
248 v2 = std::move(v);
249 }
250 return true;
251}
252
253typedef bool (*test_func)();
254
255int main() {
256 {
257 std::map<std::string, test_func> tests{
258 {"vector push", test_vector_push},
259 {"vector insert", test_vector_insert},
260 {"vector erase", test_vector_erase},
261 {"vector clear", test_vector_clear},
262 {"vector copy constructor", test_copy_constructor},
263 {"vector move constructor", test_move_constructor},
264 {"vector copy assignment", test_copy_assignment},
265 {"vector move assignment", test_move_assignment},
266 };
267 bool all_passed{true};
268 for (auto test : tests) {
269 std::cout << "TEST " << test.first << "...";
270 auto start = std::chrono::high_resolution_clock::now();
271 bool ret{false};
272 try {
273 ret = test.second();
274 } catch (std::exception& e) {
275 std::cout << "Exception: " << e.what() << std::endl;
276 }
277 auto end = std::chrono::high_resolution_clock::now();
278 if (!ret) {
279 std::cout << "**FAILED**" << std::endl;
280 all_passed = false;
281 continue;
282 }
283 std::cout << " in "
284 << std::chrono::duration_cast<std::chrono::nanoseconds>(end -
285 start)
286 .count()
287 << " ns" << std::endl;
288 }
289 if (all_passed) {
290 std::cout << "ALL TESTS PASSED" << std::endl;
291 }
292 }
293}
参考和结语
本文是 双笙子佯谬 / 第02讲:RAII与智能指针 的笔记。
接下来有时间的话我们可以讲一下如何改造成适用于任何类型的向量(模板化)。同时为了和内存的具体分配解耦,我们有空的话来实现一个分配器。
本文如有错误还请指出。