RAII机制

RAII 是一种语言特性(靠对象的作用域/生存期来管理资源)让变量控制的资源的生存期严格等同于变量自身的生存期,而变量的生存期已经由语法里作用域部分规定了,所以不应该需要专门的语法来进行说明。

  • 变量作用域结束时自动调用析构函数

  • 变量的复制/移动构造函数

C++ 通过两个事情,使用作用域就可以直接用来控制资源的生存期,这正好满足了 RAII 的要求,所以不再需要额外的语法来说明这个事情。GC 也是控制资源的一种方式。

使用 RAII

  1. // 不使用 RAII , testArray 资源申请,销毁时间点手动控制
  2. // 如果程序很复杂的时候,需要为所有的new 分配的内存delete掉
  3. #include <iostream>
  4. using namespace std;
  5. bool OperationA()
  6. {
  7. // Do some operation, if the operate succeed, then return true, else return false
  8. return true ;
  9. }
  10. bool OperationB()
  11. {
  12. // Do some operation, if the operate succeed, then return true, else return false
  13. return true ;
  14. }
  15. int main()
  16. {
  17. int *testArray = new int [10];
  18. if (!OperationA())
  19. {
  20. // If the operation A failed, we should delete the memory
  21. delete [] testArray;
  22. testArray = NULL ;
  23. return 0;
  24. }
  25. if (!OperationB())
  26. {
  27. // If the operation A failed, we should delete the memory
  28. delete [] testArray;
  29. testArray = NULL ;
  30. return 0;
  31. }
  32. // All the operation succeed, delete the memory
  33. delete [] testArray;
  34. testArray = NULL ;
  35. return 0;
  36. }
  1. // 使用 RAII ,资源申请,销毁时间点由对象的生命周期控制
  2. #include <iostream>
  3. using namespace std;
  4. class ArrayOperation
  5. {
  6. public :
  7. ArrayOperation()
  8. {
  9. m_Array = new int [10];
  10. }
  11. void InitArray()
  12. {
  13. for (int i = 0; i < 10; ++i)
  14. {
  15. *(m_Array + i) = i;
  16. }
  17. }
  18. void ShowArray()
  19. {
  20. for (int i = 0; i <10; ++i)
  21. {
  22. cout<<m_Array[i]<<endl;
  23. }
  24. }
  25. ~ArrayOperation()
  26. {
  27. cout<< "~ArrayOperation is called" <<endl;
  28. if (m_Array != NULL )
  29. {
  30. delete[] m_Array;
  31. m_Array = NULL ;
  32. }
  33. }
  34. private :
  35. int *m_Array;
  36. };
  37. bool OperationA();
  38. bool OperationB();
  39. int main()
  40. {
  41. ArrayOperation arrayOp;
  42. arrayOp.InitArray();
  43. arrayOp.ShowArray();
  44. return 0;
  45. }

总结:在于资源创建和销毁的 timing ,RAII 的意义在于使得堆区申请的内存,也使用作用域来直接用来控制生存期。将资源封装到类中,定义好构造和析构函数,让 C++ 来自动调用,就可以不用手动地去调用 delete 。

析构函数

第一部分,是写在~T(){}的大括号里面的内容,这部分由程序员掌控,一般干以下事情。

  1. 释放内存。delete或者free所有在这个对象生存期间产生的堆内存
  2. 释放句柄。如各种文件(FILE *)、窗口(HANDLE)等
  3. catch所有的异常(调用的函数有可能产生异常),不可以让异常逃离析构函数

C++异常被抛出时,会进行栈回退操作(stack unwinding),即在异常层层向上抛出时保证对已经被构造的对象执行析构函数,而这是做到上面说的那一堆释放操作的唯一机会。相对的,执行析构函数时很有可能当前正在进行异常处理,而C++中不可以同时处理两个异常,所以你需要保证析构函数(和它内部执行的其他函数)不能抛出异常

第二部分,是编译器隐式生成的内容,在第一部分完成之后执行,不需要自己写,主要有:

  1. 把虚函数表指针变成基类的,也就是此时开始这个对象已经是基类对象了。
  2. 调整this指针指向基类部分开头,并调用基类的析构函数。
  3. 虚拟继承情况下,标记虚基类已经被析构过等等

如果是多继承情况下,会对每个基类执行类似的操作,确保所有的基类能成功析构恰好一次

第三部分,如果你把析构函数定义为虚函数,那么编译器会额外添加一个析构代理函数。这个函数是为了实现使用基类指针delete对象,所做的事情如下:

  1. 调整this指针。在多继承情况下,基类指针可能指向对象中间,此时这个函数需要把传递进来的this指针调整至对象开头,否则无法保证分配内存时返回的void 和此时的this指针的值一致。(即pthis = static_cast<Derived >(pBase); )
  2. 调用前面两部分的析构函数。(或者是执行具有相似功能的语句)
  3. 用调整好的this指针去释放内存(free)。

强调:如果打算让这个类成为基类,必须把析构函数声明为虚函数。

析构函数为了实现C++的核心思想RAII(资源获取即初始化)而存在,但是它的目的并不是为了实现什么逻辑功能,只是为程序员提供个机会为自己干的好事打胎……实际写程序时建议的操作是让编译器为你生成析构函数(如使用智能指针解决问题)而不是自己写。

unique_ptr(

  1. unique_ptr<int> p(new int(5));

std::unique_ptr 是一个智能指针,它通过指针拥有和管理另一个对象,并在 unique_ptr 超出作用域时处置该对象。

  1. template<typename T>
  2. struct SmartPtr {
  3. SmartPtr(T* p) : ptr(p) {
  4. }
  5. ~SmartPtr() {
  6. delete ptr;
  7. }
  8. privite:
  9. T* ptr;
  10. }

更好的写法是下面这种,一个 unique_ptr 只能管理一个对象,不可以将 p 赋值给另一个 unique_ptr 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。这也是它名字的来源。

对应的还有 shared_ptr ,可以多个 shared_ptr 管理同一个对象,采用引用计数的智能指针。

  1. unique_ptr<int> P = make_unique<int>(8);

make_unique 会返回 unique_ptr 类型,而 new 操作符会返回目标对象的指针。func(new A(), new B()) 考虑这样的调用。当新的A()已经成功了,B()却抛出了异常,于是 A 的内存将被悄悄地泄露。std::make_unique 提供了一个“基本异常安全”,即由new分配的内存和创建的对象无论如何都不会被孤立。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3588.txt

new 和 malloc

  • new在申请空间的时候会调用构造函数,malloc不会调。
  • new在申请空间失败后返回的是错误码bad_alloc,malloc在申请空间失败后会返回 NULL 。
  • new/delete是C++关键字需要编译器支持,maollc是库函数,需要添加头文件。
  • new在申请内存分配时不需要指定内存块大小,编译器会更具类型计算出大小,malloc需要显示的指定所需内存的大小。
  • new操作符申请内存成功时,返回的是对象类型的指针,类型严格与对象匹配,无需进行类型转换,因此new是类型安全性操作符。malloc申请内存成功则返回void*,需要强制类型转换为我们所需的类型。
  • new会先调operator new函数,申请足够的内存(底层也是malloc实现),然后调用类的构造函数,初始化成员变量,最后返回自定义类型指针。delete 先调用析构函数,然后调用 operator delete 函数来释放内存(底层是通过free实现)。malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构函数。

定位构造

placement new是 operator new 的一个重载的版本,如果你想在已经分配的内存中创建一个对象,使用new时行不通的。也就是说placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。

  1. int main() {
  2. void *p = malloc(sizeof(Student));
  3. Student* q = new(p) Student();
  4. // code
  5. p->~Student();
  6. free(p);
  7. }

在实现一个内存池,gc 时非常有用,因为内存已经被分配,在预分配内存上构造对象需要更少的时间,没有分配失败的危险。

我们负责传递给 placement new 操作符的指针指向一个足够大的内存区域,并且该区域与您创建的对象类型正确对齐。编译器和运行时系统都不会尝试检查您是否做了正确的操作。