RAII机制
RAII 是一种语言特性(靠对象的作用域/生存期来管理资源)让变量控制的资源的生存期严格等同于变量自身的生存期,而变量的生存期已经由语法里作用域部分规定了,所以不应该需要专门的语法来进行说明。
变量作用域结束时自动调用析构函数
变量的复制/移动构造函数
C++ 通过两个事情,使用作用域就可以直接用来控制资源的生存期,这正好满足了 RAII 的要求,所以不再需要额外的语法来说明这个事情。GC 也是控制资源的一种方式。
使用 RAII
// 不使用 RAII , testArray 资源申请,销毁时间点手动控制
// 如果程序很复杂的时候,需要为所有的new 分配的内存delete掉
#include <iostream>
using namespace std;
bool OperationA()
{
// Do some operation, if the operate succeed, then return true, else return false
return true ;
}
bool OperationB()
{
// Do some operation, if the operate succeed, then return true, else return false
return true ;
}
int main()
{
int *testArray = new int [10];
if (!OperationA())
{
// If the operation A failed, we should delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
if (!OperationB())
{
// If the operation A failed, we should delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
// All the operation succeed, delete the memory
delete [] testArray;
testArray = NULL ;
return 0;
}
// 使用 RAII ,资源申请,销毁时间点由对象的生命周期控制
#include <iostream>
using namespace std;
class ArrayOperation
{
public :
ArrayOperation()
{
m_Array = new int [10];
}
void InitArray()
{
for (int i = 0; i < 10; ++i)
{
*(m_Array + i) = i;
}
}
void ShowArray()
{
for (int i = 0; i <10; ++i)
{
cout<<m_Array[i]<<endl;
}
}
~ArrayOperation()
{
cout<< "~ArrayOperation is called" <<endl;
if (m_Array != NULL )
{
delete[] m_Array;
m_Array = NULL ;
}
}
private :
int *m_Array;
};
bool OperationA();
bool OperationB();
int main()
{
ArrayOperation arrayOp;
arrayOp.InitArray();
arrayOp.ShowArray();
return 0;
}
总结:在于资源创建和销毁的 timing ,RAII 的意义在于使得堆区申请的内存,也使用作用域来直接用来控制生存期。将资源封装到类中,定义好构造和析构函数,让 C++ 来自动调用,就可以不用手动地去调用 delete 。
析构函数
第一部分,是写在~T(){}的大括号里面的内容,这部分由程序员掌控,一般干以下事情。
- 释放内存。delete或者free所有在这个对象生存期间产生的堆内存
- 释放句柄。如各种文件(FILE *)、窗口(HANDLE)等
- catch所有的异常(调用的函数有可能产生异常),不可以让异常逃离析构函数。
C++异常被抛出时,会进行栈回退操作(stack unwinding),即在异常层层向上抛出时保证对已经被构造的对象执行析构函数,而这是做到上面说的那一堆释放操作的唯一机会。相对的,执行析构函数时很有可能当前正在进行异常处理,而C++中不可以同时处理两个异常,所以你需要保证析构函数(和它内部执行的其他函数)不能抛出异常。
第二部分,是编译器隐式生成的内容,在第一部分完成之后执行,不需要自己写,主要有:
- 把虚函数表指针变成基类的,也就是此时开始这个对象已经是基类对象了。
- 调整this指针指向基类部分开头,并调用基类的析构函数。
- 虚拟继承情况下,标记虚基类已经被析构过等等
如果是多继承情况下,会对每个基类执行类似的操作,确保所有的基类能成功析构恰好一次。
第三部分,如果你把析构函数定义为虚函数,那么编译器会额外添加一个析构代理函数。这个函数是为了实现使用基类指针delete对象,所做的事情如下:
- 调整this指针。在多继承情况下,基类指针可能指向对象中间,此时这个函数需要把传递进来的this指针调整至对象开头,否则无法保证分配内存时返回的void 和此时的this指针的值一致。(即pthis = static_cast<Derived >(pBase); )
- 调用前面两部分的析构函数。(或者是执行具有相似功能的语句)
- 用调整好的this指针去释放内存(free)。
强调:如果打算让这个类成为基类,必须把析构函数声明为虚函数。
析构函数为了实现C++的核心思想RAII(资源获取即初始化)而存在,但是它的目的并不是为了实现什么逻辑功能,只是为程序员提供个机会为自己干的好事打胎……实际写程序时建议的操作是让编译器为你生成析构函数(如使用智能指针解决问题)而不是自己写。
unique_ptr( )
unique_ptr<int> p(new int(5));
std::unique_ptr 是一个智能指针,它通过指针拥有和管理另一个对象,并在 unique_ptr 超出作用域时处置该对象。
template<typename T>
struct SmartPtr {
SmartPtr(T* p) : ptr(p) {
}
~SmartPtr() {
delete ptr;
}
privite:
T* ptr;
}
更好的写法是下面这种,一个 unique_ptr 只能管理一个对象,不可以将 p 赋值给另一个 unique_ptr 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。这也是它名字的来源。
对应的还有 shared_ptr ,可以多个 shared_ptr 管理同一个对象,采用引用计数的智能指针。
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允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。
int main() {
void *p = malloc(sizeof(Student));
Student* q = new(p) Student();
// code
p->~Student();
free(p);
}
在实现一个内存池,gc 时非常有用,因为内存已经被分配,在预分配内存上构造对象需要更少的时间,没有分配失败的危险。
我们负责传递给 placement new 操作符的指针指向一个足够大的内存区域,并且该区域与您创建的对象类型正确对齐。编译器和运行时系统都不会尝试检查您是否做了正确的操作。