# 深入理解析构函数与生命周期

[TOC]

## C++ 对象生命周期

C++ 中一个类可以具有构造函数和析构函数。

- 构造函数固定为 `类名(构造函数参数列表)`。
- 析构函数固定为 `~类名()`。

- 构造函数和析构函数都没有返回值类型。
- 析构函数不得拥有任何参数，但构造函数可以有。

```cpp
struct Class {
    Class() {
        puts("构造函数");
    }

    ~Class() {
        puts("析构函数");
    }
};
```

```cpp
int main() {
    puts("进入 main");
    Class c;
    puts("离开 main");
}
```

运行结果：

```
进入 main
构造函数
离开 main
析构函数
```

这是 C++ 中最基本的现象。每当一个对象被创建时，会调用构造函数，每当一个对象离开定义了他的函数体时，会调用析构函数。

> 函数体指的是从 `{` 到 `}` 之间的代码块。

其中构造函数中通常负责创建资源，析构函数中通常销毁资源。对于智能指针和 vector 而言，这个资源就是内存。

为什么要及时销毁不用的资源？只分配不释放，一个程序占用的内存和其他各种资源就会越来越多，这种程序如果长期运行，会吃光整个系统的所有资源然后被 Linux 内核视为危险进程而杀死。除非你的程序只会运行一会会，如果是长期运行的程序，例如服务器，必须严格管理所有自己曾经分配过的内存，不用时就立即释放，不要占着茅坑不拉史。

`}` 被誉为**最伟大的运算符**，就是因为他可以触发析构函数，帮你自动释放掉资源，你就不用自己费心手动释放内存，和其他各种资源了。

## 三大存储周期

在进一步深入之前，我们必须明确以下术语：自动存储周期、动态存储周期、静态存储周期。

变量定义在不同的位置，其生命周期（构造函数和析构函数调用的时机）会有所不同。

比如一个变量定义在函数体内、类体内、通过 new 创建，之类的。

1. 自动存储周期，这种变量直接定义在**函数体**内。俗称“栈上”或“局部变量”

```cpp
void func() {
    Class a;  // a 是自动存储周期
}
```

- 构造时机：当变量定义时被调用。
- 析构时机：当变量所在的 `{}` 代码块执行到 `}` 处时调用。

2. 动态存储周期，这种变量通过 new 来创建。俗称“堆上”或“堆对象”

```cpp
void func() {
    Class *p = new Class;  // *p 是动态存储周期
    delete p;              // 释放动态分配的内存
}
```

- 构造时机：当变量通过 new 创建时被调用。
- 析构时机：当 delete 被调用时被调用。

> 特别注意，`p` 依然是“栈上变量”，`p` 指向的 `*p` 才是“堆上变量”！

> 用律师语再说一遍：`p` 是自动存储周期，`p` 指向的 `*p` 才是动态存储周期！（白律师最满意的一集）

指针本身，和指针指向的对象，是两个东西，不要混淆。

`p` 本身会随着 func 的 `}` 而析构，但是 `*p` 的类型是 `Class *`，是一个 C 语言原始指针，原始指针属于 C 语言原始类型，没有析构函数。也就是说，抵达 `}` 时，`p` 名义上会析构，但是他没有析构函数，并不会产生任何作用。这一切和 `p` 指向的对象 `*p` 没有任何关系，你需要手动 delete 才会调用到 `*p` 的析构函数，并释放分配的内存。

- new 分为两部分：内存分配 + **对象构造**
- delete 分为两部分：**对象析构** + 内存释放

智能指针的优势在于，智能指针是个 C++ 类，具有定制的析构函数。当 `}` 抵达，**智能指针本身**由于自动存储周期的规则析构时，其会 `delete p`，利用动态存储周期的规则，触发**智能指针指向对象**的析构函数，也就是从而调用 `*p` 的析构函数。

3. 静态存储周期，这种变量又要具体分三种情况，俗称“全局变量”或“静态变量”

(1) 定义在**名字空间**内，不论是不是 static 或 inline（在名字空间中，static 和 inline 影响的只是“符号可见性”，而不是存储周期）

```cpp
namespace hello {
Class s;         // s 是静态存储周期
static Class s;  // s 是静态存储周期
inline Class s;  // s 是静态存储周期
}
```

- 构造时机：当程序启动时调用（main 函数之前）；对 DLL 来说则是 DLL 首次加载时调用。
- 析构时机：当程序退出时调用（main 函数之后）。

(2) 注意，**全局名字空间**是一个特殊的**名字空间**，外面没有包裹任何 `namespace` 时就属于这种情况，俗称“全局变量”。所以下面这种也属于“在 (全局) 名字空间内”：

```cpp
Class s;         // s 是静态存储周期
static Class s;  // s 是静态存储周期
inline Class s;  // s 是静态存储周期
```

(3) 定义在类内的静态成员变量，也就是通过 static 修饰过的成员变量（在类内，static 就影响存储周期了，inline 继续只影响“符号可见性”）

```cpp
struct Other {
    static Class s;
};
Class Other::s;             // s 是静态存储周期

struct Other {
    inline static Class s;  // s 是静态存储周期
};

struct Other {
    Class a;                // a 不是静态存储周期，而是跟随其所属的 Other 结构体的存储周期
};
```

4. 定义在类内的成员变量（没有 static 的），跟随所属类的存储周期

```cpp
struct Other {
    Class a;  // a 跟随 Other 结构体的存储周期
};

Other o;      // o.a 是静态存储周期

int main() {
    Other o;  // o.a  是自动存储周期
    Other *p; // p->a 是动态存储周期
}
```

- 构造时机：当 Other 结构体构造时调用。
- 析构时机：当 Other 结构体析构时调用。

## 总结

- 自动存储周期 - 函数的局部变量，自动析构
- 动态存储周期 - 通过 new 创建的，delete 时析构
- 静态存储周期 - 全局变量，程序结束时析构

```cpp
```

## 析构函数的逆天大坑

定义了析构函数，就**必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数**！

原因很复杂，整个故事要从 boost 当年如何设计出右值引用到图灵的停机问题讲起，讲了你也记不住，只需要记住结论：

如果你要定义析构函数，就**必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数**！

## 虚类的析构函数必须是虚的

- `-Wnon-virtual-dtor`
- `-Wdelete-non-virtual-dtor`

TODO: 介绍

## 临时变量的生命周期是一行

TODO

```cpp
int main() {
    std::string const &s = std::string("hello");
    std::cout << s;  // OK
}
```

```cpp
std::string const &identity(std::string const &s) {
    return s;
}

int main() {
    std::string const &s = identity(std::string("hello"));
    std::cout << s;  // BOOM!
}
```
