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_ptrshared_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;
}

 

参考C++智能指针及其简单实现

posted @ 2021-08-27 16:21  默行于世  阅读(265)  评论(0编辑  收藏  举报