C++11 RAII 与 智能指针
使用智能指针管理内存资源,RAII
1) RAII全称是“Resource Acquisition is Initialization”,直译过来是“资源获取即初始化”,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
2) 智能指针(std::shared_ptr和std::unique_ptr)即RAII最具代表的实现,使用智能指针,可以实现自动的内存管理,再也不需要担心忘记 delete 造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。
智能指针的前身:auto_ptr
1) auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题;抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄漏;
2) auto_ptr构造时取得某个对象的控制权,在析构时释放该对象。我们实际上是创建一个auto_ptr<Type>类型的局部对象,该局部对象析构时,会将自身所拥有的指针空间释放,所以不会有内存泄漏;
3) auto_ptr 的构造函数是 explicit,阻止了一般指针隐式转换为 auto_ptr 的构造,所以不能直接将一般类型的指针赋值给 auto_ptr 类型的对象,必须用 auto_ptr 的构造函数创建对象;
4) 由于 auto_ptr 对象析构时会删除它所拥有的指针,所以使用时避免多个 auto_ptr对象管理同一个指针;
5) auto_ptr 内部实现,析构函数中删除对象用的是 delete 而不是 delete[],所以auto_ptr不能管理数组;
6) auto_ptr支持所拥有的指针类型之间的隐式类型转换。
7) 可以通过*和->运算符对auto_ptr所有用的指针进行提领操作;
8) T* get(),获得auto_ptr所拥有的指针;T* release(),释放auto_ptr的所有权,并将所有用的指针返回。
智能指针
1) C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等。智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。auto_ptr(c++98的方案,c++11已经抛弃)采用所有权模式。被p1赋值的p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
2) 智能指针在 C++11 版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多个指针指向相同的对象。shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。shared_ptr的每一个拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。可以通过成员函数use_count()来查看资源的所有者个数。unique()返回是否是独占所有权(use_count为1)。swap交换两个shared_ptr对象(即交换所拥有的对象)。reset放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少。get返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的.如shared_ptr sp(newint(1));sp与sp.get()是等价的。
3) 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。shared_ptr 调 用 一 个 名 为 make_shared 的 标 准 库 函 数,shared_ptr<int> p = make_shared<int>(42);通常用auto更方便,auto p = …;shared_ptr<int> p2(new int(2));。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p4 = new int(1);的写法是错误的拷贝和赋值。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr, weak_ptr来构造。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
4) unique_ptr“唯一”拥有其所指对象(替换auto_ptr),同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。当程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做。如果确实想执行类似赋值的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。相比与原始指针 unique_ptr 用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用 delete 操作符,用户可指定其他操作)。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过 reset 方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
5) 智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
6) weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr用于专门解决 shared_ptr 循环引用的问题。循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。weak_ptr能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
注意:不能通过weak_ptr直接访问对象的方法,应该先把它转化为shared_ptr,如:shared_ptr p=pa->pb_.lock();p->print();
实现智能指针类
1) 智能指针是一个数据类型,一般用模板实现,模拟指针行为的同时还提供自动垃圾回收机制。它会自动记录SmartPointer<T*>对象的引用计数,一旦T类型对象的引用计数为 0,就释放该对象。除了指针对象外,我们还需要一个引用计数的指针设定对象的值,并将引用计数计为 1,需要一个构造函数。新增对象还需要一个构造函数,析构函数负责引用计数减少和释放内存。通过重载赋值运算符,才能将一个旧的智能指针赋值给另一个指针,同时旧的引用计数减1,新的引用计数加1 。
2) 一个构造函数、拷贝构造函数、复制构造函数、析构函数、移走函数;
#include <iostream> using namespace std; template<class T> class SmartPtr { public: SmartPtr(T *p); ~SmartPtr(); SmartPtr(const SmartPtr<T> &orig); // 浅拷贝 SmartPtr<T>& operator=(const SmartPtr<T> &rhs); // 浅拷贝 private: T *ptr; // 将use_count声明成指针是为了方便对其的递增或递减操作 int *use_count; }; template<class T> SmartPtr<T>::SmartPtr(T *p) : ptr(p) { try { use_count = new int(1); } catch (...) { delete ptr; ptr = nullptr; use_count = nullptr; cout << "Allocate memory for use_count fails." << endl; exit(1); } cout << "Constructor is called!" << endl; } template<class T> SmartPtr<T>::~SmartPtr() { // 只在最后一个对象引用ptr时才释放内存 if (--(*use_count) == 0) { delete ptr; delete use_count; ptr = nullptr; use_count = nullptr; cout << "Destructor is called!" << endl; } } template<class T> SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig) { ptr = orig.ptr; use_count = orig.use_count; ++(*use_count); cout << "Copy constructor is called!" << endl; } // 重载等号函数不同于复制构造函数,即等号左边的对象可能已经指向某块内存。 // 这样,我们就得先判断左边对象指向的内存已经被引用的次数。如果次数为1, // 表明我们可以释放这块内存;反之则不释放,由其他对象来释放。 template<class T> SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs) { // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使rhs的使用计数加1, // 从而防止自身赋值”而导致的提早释放内存 ++(*rhs.use_count); // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象 if (--(*use_count) == 0) { delete ptr; delete use_count; cout << "Left side object is deleted!" << endl; } ptr = rhs.ptr; use_count = rhs.use_count; cout << "Assignment operator overloaded is called!" << endl; return *this; }
测试代码:
#include <iostream> #include "smartptr.h" using namespace std; int main() { // Test Constructor and Assignment Operator Overloaded SmartPtr<int> p1(new int(0)); p1 = p1; // Test Copy Constructor SmartPtr<int> p2(p1); // Test Assignment Operator Overloaded SmartPtr<int> p3(new int(1)); p3 = p1; return 0; }