01 | 堆、栈、RAII:C++里该如何管理资源?

自由存储区和堆区

  • new 和 delete 操作的区域是 free store,malloc 和 free 操作的区域是 heap
  • 但 new 和 delete 通常底层使用 malloc 和 free 来实现

内存管理的相关操作

  • 1.分配一个某个大小的内存块

  • 2.释放一个之前分配的内存块

    • 空闲块的合并
  • 3.进行垃圾收集操作,寻找不再使用的内存块并予以释放

内存泄露

  • 如果动态申请的变量在运行过程中,抛出异常,导致最后没有被delete掉,将会导致内存泄露

过程调用的机器级表示

  • IA-32的寄存器使用约定
    • 调用者保存寄存器: EAX,ECX,EDX.
      • Q可以直接使用这三个寄存器,而不需要保存他们的内容
      • 所以为了减少准备和结束阶段的开销,每个过程应先使用EAX,ECX,EDX.
    • 被调用者保存寄存器: EBX,ESI,EDI.
      • 如果Q中寄存器不够用,需要先将他们的数据保存到栈中在使用他们,并且在恢复到P前还要恢复他们的值
    • ESP,EBP

析构函数的调用

构造函数和析构函数中抛出异常

  • 可以抛出异常,不会导致内存泄露,new能保证不发生内存泄露
  • 不推荐在析构函数中抛出异常,会有两个问题
    • 如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 正常情况下调用析构函数抛出异常导致资源泄露
    • 通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。 在发生异常的情况下调用析构函数抛出异常,会导致程序崩溃

什么是stack-unwinding

  • 过程:

    • 抛出异常时,将暂停当前函数的执行,开始查找匹配的catch子句
    • 首先检查throw本身是否在try块内部,如果是,检查与该try相关的catch子句,看是否可以处理该异常。
    • 如果不能处理,就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到上层的调用函数中查找,直到找到一个可以处理该异常的catch
      • 先调用析构函数,再处理异常
    • 当处理该异常的catch结束之后,紧接着该catch之后的点继续执行
  • 作用:

    • 为局部对象调用析构函数
      • 但需要注意的是,如果一个块通过new动态分配内存,并且在释放该资源之前发生异常,该块因异常而退出,那么在栈展开期间不会释放该资源,编译器不会删除该指针,这样就会造成内存泄露。
    • 析构函数应该从不抛出异常
      • 在为某个异常进行栈展开的时候,析构函数如果又抛出自己的未经处理的另一个异常,将会导致调用标准库terminate函数。通常terminate函数将调用abort函数,导致程序的非正常退出。
        • 可能导致内存泄露
    • 异常与构造函数
      • 如果在构造函数对象时发生异常,此时该对象可能只是被部分构造,要保证能够适当的撤销这些已构造的成员
    • 未捕获的异常将会终止程序
      • 不能不处理异常。如果找不到匹配的catch,程序就会调用库函数terminate

RAII - Resource Acquisition Is Initialization(资源分配及初始化) - 资源管理方式

  • RAII 依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理
  • 让析构函数来执行资源释放,即使函数抛异常,此时仍然可以调用析构函数而不会直接中断

对象切片

对象切片(object slicing)和多态

  • 在函数传参处理多态性时,如果一个派生类对象在UpCasting时,用的是传值的方式,而不是指针和引用,那么,这个派生类对象在UpCasting以后,将会被slice成基类对象。
class Father{
public:
    virtual void info(){
        cout<<"father"<<endl;
    }
};
class Son: public Father{
public:
    void info(){
        cout<<"son"<<endl;
    }
};
int main() {
    Father f1 = Son(); // 栈中分配,由操作系统进行内存的分配和管理
    f1.info();  // cout: father 没有了多态
    Father* f2 = new Son(); // 堆中分配,由管理者进行内存的分配和管理,用完必须delete(),否则可能造成内存泄漏
    f2->info();  // cout: son
    Son s = Son();
    Father& f3 = s;
    f3.info(); // cout: son
    return 0;
}

如何防止内存泄露

  • 把指向堆内存的指针包裹到一个局部对象中,该对象的析构函数在析构时会 delete 自己的指针变量,这样可以通过栈展开保证new出来的内存被释放,不会遗漏。

class shape_wrapper {
public:
  explicit shape_wrapper(
    shape* ptr = nullptr)
    : ptr_(ptr) {}
  ~shape_wrapper()
  {
    delete ptr_;
  }
  shape* get() const { return ptr_; }
private:
  shape* ptr_;
};

void foo()
{
  …
  shape_wrapper ptr_wrapper(
    create_shape(…));
  …
}

delete空指针

  • delete 空指针不会报错,但是delete悬垂指针,可能只想正在使用的对象,会出现意料之外的结果