|
| 1 | +# 深入理解析构函数与生命周期 |
| 2 | + |
| 3 | +[TOC] |
| 4 | + |
| 5 | +## C++ 对象生命周期 |
| 6 | + |
| 7 | +C++ 中一个类可以具有构造函数和析构函数。 |
| 8 | + |
| 9 | +- 构造函数固定为 `类名(构造函数参数列表)`。 |
| 10 | +- 析构函数固定为 `~类名()`。 |
| 11 | + |
| 12 | +- 构造函数和析构函数都没有返回值类型。 |
| 13 | +- 析构函数不得拥有任何参数,但构造函数可以有。 |
| 14 | + |
| 15 | +```cpp |
| 16 | +struct Class { |
| 17 | + Class() { |
| 18 | + puts("构造函数"); |
| 19 | + } |
| 20 | + |
| 21 | + ~Class() { |
| 22 | + puts("析构函数"); |
| 23 | + } |
| 24 | +}; |
| 25 | +``` |
| 26 | + |
| 27 | +```cpp |
| 28 | +int main() { |
| 29 | + puts("进入 main"); |
| 30 | + Class c; |
| 31 | + puts("离开 main"); |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +运行结果: |
| 36 | + |
| 37 | +``` |
| 38 | +进入 main |
| 39 | +构造函数 |
| 40 | +离开 main |
| 41 | +析构函数 |
| 42 | +``` |
| 43 | + |
| 44 | +这是 C++ 中最基本的现象。每当一个对象被创建时,会调用构造函数,每当一个对象离开定义了他的函数体时,会调用析构函数。 |
| 45 | + |
| 46 | +> 函数体指的是从 `{` 到 `}` 之间的代码块。 |
| 47 | +
|
| 48 | +其中构造函数中通常负责创建资源,析构函数中通常销毁资源。对于智能指针和 vector 而言,这个资源就是内存。 |
| 49 | + |
| 50 | +为什么要及时销毁不用的资源?只分配不释放,一个程序占用的内存和其他各种资源就会越来越多,这种程序如果长期运行,会吃光整个系统的所有资源然后被 Linux 内核视为危险进程而杀死。除非你的程序只会运行一会会,如果是长期运行的程序,例如服务器,必须严格管理所有自己曾经分配过的内存,不用时就立即释放,不要占着茅坑不拉史。 |
| 51 | + |
| 52 | +`}` 被誉为**最伟大的运算符**,就是因为他可以触发析构函数,帮你自动释放掉资源,你就不用自己费心手动释放内存,和其他各种资源了。 |
| 53 | + |
| 54 | +## 三大存储周期 |
| 55 | + |
| 56 | +在进一步深入之前,我们必须明确以下术语:自动存储周期、动态存储周期、静态存储周期。 |
| 57 | + |
| 58 | +变量定义在不同的位置,其生命周期(构造函数和析构函数调用的时机)会有所不同。 |
| 59 | + |
| 60 | +比如一个变量定义在函数体内、类体内、通过 new 创建,之类的。 |
| 61 | + |
| 62 | +1. 自动存储周期,这种变量直接定义在**函数体**内。俗称“栈上”或“局部变量” |
| 63 | + |
| 64 | +```cpp |
| 65 | +void func() { |
| 66 | + Class a; // a 是自动存储周期 |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +- 构造时机:当变量定义时被调用。 |
| 71 | +- 析构时机:当变量所在的 `{}` 代码块执行到 `}` 处时调用。 |
| 72 | + |
| 73 | +2. 动态存储周期,这种变量通过 new 来创建。俗称“堆上”或“堆对象” |
| 74 | + |
| 75 | +```cpp |
| 76 | +void func() { |
| 77 | + Class *p = new Class; // *p 是动态存储周期 |
| 78 | + delete p; // 释放动态分配的内存 |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +- 构造时机:当变量通过 new 创建时被调用。 |
| 83 | +- 析构时机:当 delete 被调用时被调用。 |
| 84 | + |
| 85 | +> 特别注意,`p` 依然是“栈上变量”,`p` 指向的 `*p` 才是“堆上变量”! |
| 86 | +
|
| 87 | +> 用律师语再说一遍:`p` 是自动存储周期,`p` 指向的 `*p` 才是动态存储周期!(白律师最满意的一集) |
| 88 | +
|
| 89 | +指针本身,和指针指向的对象,是两个东西,不要混淆。 |
| 90 | + |
| 91 | +`p` 本身会随着 func 的 `}` 而析构,但是 `*p` 的类型是 `Class *`,是一个 C 语言原始指针,原始指针属于 C 语言原始类型,没有析构函数。也就是说,抵达 `}` 时,`p` 名义上会析构,但是他没有析构函数,并不会产生任何作用。这一切和 `p` 指向的对象 `*p` 没有任何关系,你需要手动 delete 才会调用到 `*p` 的析构函数,并释放分配的内存。 |
| 92 | + |
| 93 | +- new 分为两部分:内存分配 + **对象构造** |
| 94 | +- delete 分为两部分:**对象析构** + 内存释放 |
| 95 | + |
| 96 | +智能指针的优势在于,智能指针是个 C++ 类,具有定制的析构函数。当 `}` 抵达,**智能指针本身**由于自动存储周期的规则析构时,其会 `delete p`,利用动态存储周期的规则,触发**智能指针指向对象**的析构函数,也就是从而调用 `*p` 的析构函数。 |
| 97 | + |
| 98 | +3. 静态存储周期,这种变量又要具体分三种情况,俗称“全局变量”或“静态变量” |
| 99 | + |
| 100 | +(1) 定义在**名字空间**内,不论是不是 static 或 inline(在名字空间中,static 和 inline 影响的只是“符号可见性”,而不是存储周期) |
| 101 | + |
| 102 | +```cpp |
| 103 | +namespace hello { |
| 104 | +Class s; // s 是静态存储周期 |
| 105 | +static Class s; // s 是静态存储周期 |
| 106 | +inline Class s; // s 是静态存储周期 |
| 107 | +} |
| 108 | +``` |
| 109 | +
|
| 110 | +- 构造时机:当程序启动时调用(main 函数之前);对 DLL 来说则是 DLL 首次加载时调用。 |
| 111 | +- 析构时机:当程序退出时调用(main 函数之后)。 |
| 112 | +
|
| 113 | +(2) 注意,**全局名字空间**是一个特殊的**名字空间**,外面没有包裹任何 `namespace` 时就属于这种情况,俗称“全局变量”。所以下面这种也属于“在 (全局) 名字空间内”: |
| 114 | +
|
| 115 | +```cpp |
| 116 | +Class s; // s 是静态存储周期 |
| 117 | +static Class s; // s 是静态存储周期 |
| 118 | +inline Class s; // s 是静态存储周期 |
| 119 | +``` |
| 120 | + |
| 121 | +(3) 定义在类内的静态成员变量,也就是通过 static 修饰过的成员变量(在类内,static 就影响存储周期了,inline 继续只影响“符号可见性”) |
| 122 | + |
| 123 | +```cpp |
| 124 | +struct Other { |
| 125 | + static Class s; |
| 126 | +}; |
| 127 | +Class Other::s; // s 是静态存储周期 |
| 128 | + |
| 129 | +struct Other { |
| 130 | + inline static Class s; // s 是静态存储周期 |
| 131 | +}; |
| 132 | + |
| 133 | +struct Other { |
| 134 | + Class a; // a 不是静态存储周期,而是跟随其所属的 Other 结构体的存储周期 |
| 135 | +}; |
| 136 | +``` |
| 137 | +
|
| 138 | +4. 定义在类内的成员变量(没有 static 的),跟随所属类的存储周期 |
| 139 | +
|
| 140 | +```cpp |
| 141 | +struct Other { |
| 142 | + Class a; // a 跟随 Other 结构体的存储周期 |
| 143 | +}; |
| 144 | +
|
| 145 | +Other o; // o.a 是静态存储周期 |
| 146 | +
|
| 147 | +int main() { |
| 148 | + Other o; // o.a 是自动存储周期 |
| 149 | + Other *p; // p->a 是动态存储周期 |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +- 构造时机:当 Other 结构体构造时调用。 |
| 154 | +- 析构时机:当 Other 结构体析构时调用。 |
| 155 | + |
| 156 | +## 总结 |
| 157 | + |
| 158 | +- 自动存储周期 - 函数的局部变量,自动析构 |
| 159 | +- 动态存储周期 - 通过 new 创建的,delete 时析构 |
| 160 | +- 静态存储周期 - 全局变量,程序结束时析构 |
| 161 | + |
| 162 | +```cpp |
| 163 | +``` |
| 164 | + |
| 165 | +## 析构函数的逆天大坑 |
| 166 | + |
| 167 | +定义了析构函数,就**必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数**! |
| 168 | + |
| 169 | +原因很复杂,整个故事要从 boost 当年如何设计出右值引用到图灵的停机问题讲起,讲了你也记不住,只需要记住结论: |
| 170 | + |
| 171 | +如果你要定义析构函数,就**必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数**! |
| 172 | + |
| 173 | +## 虚类的析构函数必须是虚的 |
| 174 | + |
| 175 | +- `-Wnon-virtual-dtor` |
| 176 | +- `-Wdelete-non-virtual-dtor` |
| 177 | + |
| 178 | +TODO: 介绍 |
0 commit comments