13.2. 难点分析 —— 显式内存管理

暂时回顾一下第 12 章中编写的 Vector<T> 的一个构造函数。为了简单起见,选用那个接受元素数量和初始值作为参数的构造函数。如果仅限于“简单版本” —— 即 elems 指向一个由 T 类型对象组成的序列,并且暂时不考虑更复杂的版本(在那个版本中,elems 指向一块内存,其前部存放着 T 类型的对象,后部则是原始内存),则有如下代码:

  // 简单版本中,elems 的类型为 T*
  Vector(size_type n, const_reference init)
    : elems{ new value_type[n] }, nelems{ n }, cap{ n } {
    try {
      std::fill(begin(), end(), init);
    } catch (...) {
      delete [] elems;
      throw;
    }
  }
// ...

这个构造函数会分配一个 T 类型对象的数组,并通过一系列赋值操作进行初始化,同时处理异常等。其中的 try 块和对应的 catch 块是我们实现的一部分,但并不是因为我们想捕获 T 类型构造函数可能抛出的异常 —— 事实上,我们根本不知道 T 可能会抛出什么异常,因为不知道 T 的具体类型。

我们加入这些代码块的原因是,如果想避免内存泄漏,就必须在异常发生时显式地释放内存并销毁数组。如果将目光转向那个更复杂的版本 —— 也就是将内存分配与对象构造分离的版本,情况会变得更加复杂:

  // 复杂版本中,elems 的类型为 T*
  Vector(size_type n, const_reference init)
    : elems{ static_cast<pointer>(
        std::malloc(n * sizeof(value_type))
      ) }, nelems{ n }, cap{ n } {
    try {
      std::uninitialized_fill(begin(), end(), init);
    } catch (...) {
      std::free(elems);
      throw;
    }
  }
// ...

之所以做这么多工作,是因为我们决定让 Vector<T> 成为这块内存的拥有者。当然,我们完全有权这么做!但如果把内存管理的责任交给其他东西来负责,情况又会如何呢?