深入理解析构函数与生命周期
C++ 对象生命周期
C++ 中一个类可以具有构造函数和析构函数。
- 构造函数固定为
类名(构造函数参数列表)
。 -
析构函数固定为
~类名()
。 -
构造函数和析构函数都没有返回值类型。
- 析构函数不得拥有任何参数,但构造函数可以有。
struct Class {
Class() {
puts("构造函数");
}
~Class() {
puts("析构函数");
}
};
int main() {
puts("进入 main");
Class c;
puts("离开 main");
}
运行结果:
进入 main
构造函数
离开 main
析构函数
这是 C++ 中最基本的现象。每当一个对象被创建时,会调用构造函数,每当一个对象离开定义了他的函数体时,会调用析构函数。
函数体指的是从
{
到}
之间的代码块。
其中构造函数中通常负责创建资源,析构函数中通常销毁资源。对于智能指针和 vector 而言,这个资源就是内存。
为什么要及时销毁不用的资源?只分配不释放,一个程序占用的内存和其他各种资源就会越来越多,这种程序如果长期运行,会吃光整个系统的所有资源然后被 Linux 内核视为危险进程而杀死。除非你的程序只会运行一会会,如果是长期运行的程序,例如服务器,必须严格管理所有自己曾经分配过的内存,不用时就立即释放,不要占着茅坑不拉史。
}
被誉为最伟大的运算符,就是因为他可以触发析构函数,帮你自动释放掉资源,你就不用自己费心手动释放内存,和其他各种资源了。
三大存储周期
在进一步深入之前,我们必须明确以下术语:自动存储周期、动态存储周期、静态存储周期。
变量定义在不同的位置,其生命周期(构造函数和析构函数调用的时机)会有所不同。
比如一个变量定义在函数体内、类体内、通过 new 创建,之类的。
- 自动存储周期,这种变量直接定义在函数体内。俗称“栈上”或“局部变量”
void func() {
Class a; // a 是自动存储周期
}
- 构造时机:当变量定义时被调用。
- 析构时机:当变量所在的
{}
代码块执行到}
处时调用。
- 动态存储周期,这种变量通过 new 来创建。俗称“堆上”或“堆对象”
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
的析构函数。
- 静态存储周期,这种变量又要具体分三种情况,俗称“全局变量”或“静态变量”
(1) 定义在名字空间内,不论是不是 static 或 inline(在名字空间中,static 和 inline 影响的只是“符号可见性”,而不是存储周期)
namespace hello {
Class s; // s 是静态存储周期
static Class s; // s 是静态存储周期
inline Class s; // s 是静态存储周期
}
- 构造时机:当程序启动时调用(main 函数之前);对 DLL 来说则是 DLL 首次加载时调用。
- 析构时机:当程序退出时调用(main 函数之后)。
(2) 注意,全局名字空间是一个特殊的名字空间,外面没有包裹任何 namespace
时就属于这种情况,俗称“全局变量”。所以下面这种也属于“在 (全局) 名字空间内”:
Class s; // s 是静态存储周期
static Class s; // s 是静态存储周期
inline Class s; // s 是静态存储周期
(3) 定义在类内的静态成员变量,也就是通过 static 修饰过的成员变量(在类内,static 就影响存储周期了,inline 继续只影响“符号可见性”)
struct Other {
static Class s;
};
Class Other::s; // s 是静态存储周期
struct Other {
inline static Class s; // s 是静态存储周期
};
struct Other {
Class a; // a 不是静态存储周期,而是跟随其所属的 Other 结构体的存储周期
};
- 定义在类内的成员变量(没有 static 的),跟随所属类的存储周期
struct Other {
Class a; // a 跟随 Other 结构体的存储周期
};
Other o; // o.a 是静态存储周期
int main() {
Other o; // o.a 是自动存储周期
Other *p; // p->a 是动态存储周期
}
- 构造时机:当 Other 结构体构造时调用。
- 析构时机:当 Other 结构体析构时调用。
总结
- 自动存储周期 - 函数的局部变量,自动析构
- 动态存储周期 - 通过 new 创建的,delete 时析构
- 静态存储周期 - 全局变量,程序结束时析构
析构函数的逆天大坑
定义了析构函数,就必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数!
原因很复杂,整个故事要从 boost 当年如何设计出右值引用到图灵的停机问题讲起,讲了你也记不住,只需要记住结论:
如果你要定义析构函数,就必须删除移动构造函数、移动赋值函数、拷贝构造函数、拷贝赋值函数!
虚类的析构函数必须是虚的
-Wnon-virtual-dtor
-Wdelete-non-virtual-dtor
TODO: 介绍
临时变量的生命周期是一行
TODO
int main() {
std::string const &s = std::string("hello");
std::cout << s; // OK
}
std::string const &identity(std::string const &s) {
return s;
}
int main() {
std::string const &s = identity(std::string("hello"));
std::cout << s; // BOOM!
}