Skip to content

Commit 5f658ad

Browse files
committed
add temp
1 parent 167fb60 commit 5f658ad

File tree

14 files changed

+5380
-13
lines changed

14 files changed

+5380
-13
lines changed

slides/design/dtor.md

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

0 commit comments

Comments
 (0)