C++ 资源管理

对 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
2
3
4
5
6
7
8
9
10
11
int very_big[100000] = {1, 2}; // 在.data段会产生很大的编译结果
int empty[100000]; // 在.bss段

// Demo in https://stackoverflow.com/questions/30838144/what-all-local-variables-goto-data-bss-segment
void foo(void)
{
static char array1[256] = ""; // Goes in BSS, probably
static char array2[256] = "ABCDEFXYZ"; // Goes in Data
static const char string[] = "Kleptomanic Hypochondriac"; // Goes in Text, probably
...
}

RAII

管理堆内存的注意点:

  • 没有调用 delete/free 进行释放会造成泄露
  • 重复调用 delete/free,造成悬空指针。访问悬空指针式 undefined behaviour

一些管理方式:

  • 建立一张表(称为对象池)登记这些指针,当满足一些条件的时候进行删除的手动管理。
  • RAII

复制

一般对象之间的复制行为分为4种:

  1. 浅复制
    浅复制也是默认复制构造函数的实现,将源对象中的成员复制到新的对象中。因此如果源对象中存在指针,那么实际上源对象和新对象是共享指针指向的对象的,这并不是一个错误的逻辑,但是问题在于新老对象都没有意识到自己和别的对象共享着资源,如果存在析构函数(除非使用手动管理,否则必然要有析构函数用来释放指针指向对象),那么必然会造成悬空指针。
  2. 深复制
    需要自定义复制构造函数,在复制行为发生时递归地建立对象成员以及指针指向对象的副本。
  3. 资源控制权转移
    资源占用具有排他性,这种有点类似于 Rust 的移动语义,或者 std::unique_ptr
  4. 资源控制权共享
    类似 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 来分配的