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}

问: 使用赋值初始化和初始化列表的区别

为什么使用初始化表达式?

  1. 只需一次赋值

  2. 能初始化 const、引用 类成员

  3. 如果类成员没有无参构造函数,可以使用初始化表达式为其初始化

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 实现

需要注意的几点:

  1. 拷贝赋值要记得先释放自己占有的内存

  2. 拷贝构造和拷贝赋值都尽量用 const 修饰 other

  3. 移动构造和移动赋值才是 move 的具体实现,所以要释放从 other 复制到 this 然后清空 other

  4. 不要滥用 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与智能指针 的笔记。

接下来有时间的话我们可以讲一下如何改造成适用于任何类型的向量(模板化)。同时为了和内存的具体分配解耦,我们有空的话来实现一个分配器。

本文如有错误还请指出。