对 Effective C++ 在资源管理部分的内容进行总结。这一部分其实已经比较基础了,等待重构。
内存管理
内存分段
主要分为:
- bss(Block Started by Symbol)
存储程序中未被初始化的变量,比如全局变量和 static local。在程序运行开始前会被初始化为 0。这么做目的是为了优化可执行文件的大小,考虑到未给这些变量赋值,所以它们在可执行文件中并不需要实际占有空间,所谓的 bss 段在可执行文件中只是一个 placeholder。
对应到 C++ 中就是做了 zero initialization,详见C++初始化方式。 - data
data 段存储程序中已经被初始化的变量。 - stack
stack 中的变量由编译器自动分配和清除,所以称作自动变量。 - heap
heap 区由new
调用构造函数初始化,由delete
调用析构函数回收。
另外一些提法中,将 heap 进一步细化出一个自由存储区,指由malloc
等分配并由free
等释放的内存。
我也习惯将它们成为“动态分配的内存”。 - 常量区
包含init、text 和 rodata 段
1 | int very_big[100000] = {1, 2}; // 在.data段会产生很大的编译结果 |
RAII
管理堆内存的注意点:
- 没有调用
delete
/free
进行释放会造成泄露 - 重复调用
delete
/free
,造成悬空指针。访问悬空指针式 undefined behaviour
一些管理方式:
- 建立一张表(称为对象池)登记这些指针,当满足一些条件的时候进行删除的手动管理。
- RAII
复制
一般对象之间的复制行为分为4种:
- 浅复制
浅复制也是默认复制构造函数的实现,将源对象中的成员复制到新的对象中。因此如果源对象中存在指针,那么实际上源对象和新对象是共享指针指向的对象的,这并不是一个错误的逻辑,但是问题在于新老对象都没有意识到自己和别的对象共享着资源,如果存在析构函数(除非使用手动管理,否则必然要有析构函数用来释放指针指向对象),那么必然会造成悬空指针。 - 深复制
需要自定义复制构造函数,在复制行为发生时递归地建立对象成员以及指针指向对象的副本。 - 资源控制权转移
资源占用具有排他性,这种有点类似于 Rust 的移动语义,或者std::unique_ptr
。 - 资源控制权共享
类似std::shared_ptr
这类 RC。
深复制
在深复制中可能存在一个问题,假设有若干个派生类继承基类Derived_i : public Base
,现在有一个Base * d
,但是不知道具体类型,现在希望对这个基类进行深复制。
直接调用基类的复制构造函数 Base p = new Base(d)
行不通,这是因为 C++ 标准规定了复制构造函数,包括构造函数都不能是虚的。StackOverflow 上详细地说明了原因,简单来说,因为 C++ 是静态类型的,所以多态地创建对象没有意义。
因此,这里常用的办法是自己定义一个 clone 函数。
同样地,对于其他构造函数,也不存在多态。因此如果需要对于不同的参数返回不同的派生类的指针,可以通过使用工厂函数这样的设计模式来解决。
当然对于非继承的类型,当然可以定义复制、赋值、析构这三个函数都进行深复制。假设复制对象 obj,可以认为复制了一棵树。树中任何节点对应(成员)的析构都会导致该节点为树根的子树被删除,所以对于这棵树只能修改树根或叶子,否则会造成内存泄露。
复制构造函数和赋值构造函数
复制构造函数指的形如 T(const T &)
的构造函数,它的行为由 C++ 的复制初始化来规定,可以参考C++初始化方式。
复制构造函数常被定义为 explicit
的,此时必须显式地使用该构造函数。
在 C++11 标准之后,将非 explicit 构造函数称为转换构造函数。
区分:
- 转换构造函数
T::T(const U &)
从U
构造T
- 类型转换运算符
operator T::U()
从自己也就是T
构造U
。
类型转换运算符有很多的作用,常见的是实现将函数返回值加入重载决议。
赋值运算符指的形如 T & operator=(const T &)
的运算符,指的是赋值而不是初始化操作。
构造
创建只能在栈上构造的类
将 new 和 delete 变成私有
创建只能在堆上构造的类
- 私有构造函数
要在类里面写一个 static 工厂方法,用来产生一个对象。 - 也可以私有析构函数
析构
delete this
在 MFC 里面经常看到 delete this。C++ 是允许这么用的,但需要注意确保:
- delete this 是这个类的最后一个调用
- 这个对象是用 new 来分配的