C++智能指针的发展

时间:2021-05-10 01:13:18

智能指针

GC–garbage collection垃圾回收,Java里的机制。在头文件<memory>

内存泄漏

堆内存泄漏(Heap leak)

堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

采用RAII思想或者智能指针来管理资源。

RAII资源获得即初始化

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
相当于把资源生命周期和对象生命周期绑定在一起了==>在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:1.不需要显式地释放资源;2.采用这种方式,对象所需的资源在其生命期内始终保持有效。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    //保存资源
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
        {}
    //释放资源
    ~SmartPtr()
    {
        if(_ptr) delete _ptr;
    }
private:
	T* _ptr;
};

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。

C98 auto_ptr

原理如下:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

template<class T>
class auto_ptr {
public:
    //保存资源
    auto_ptr(T* ptr = nullptr)
        : _ptr(ptr)
        {}
    //释放资源
    ~auto_ptr()
    {
        if(_ptr) delete _ptr;
    }
    //重载operator*
    T& operator*()
    {
        return *_ptr;
    }
    //重载operator->
    T* operator->()
    {
        return _ptr;
    }
private:
	T* _ptr;
};

存在的问题-对象悬空

auto_ptr类中没有写拷贝构造,构造函数就是浅拷贝,auto_ptr<int> auto_p1(new int); auto_ptr<int> autop2(auto_p2);这样的代码,就会造成资源重复析构。C98中对于auto_ptr资源重复析构提出的解决方法是资源管理器转移,即只有一个对象管一份资源,auto_p1就不管了,让auto_p2管==>会导致对象悬空,即auto_p1悬空。

auto_ptr(SmartPtr<T>& sp) :_ptr(sp.ptr) {
    // 管理权转移
    sp._ptr = nullptr;
}
auto_ptr<T>& operator=(SmartPtr<T>& sp) {
	// 检测是否为自己给自己赋值
    if (this != &sp) {
        // 释放当前对象中资源
        if (_ptr) delete _ptr;
        // 转移sp中资源到当前对象中
        _ptr = sp._ptr;
        sp._ptr = NULL;
    }
    return *this;
}

注意:解决办法绝对不能写深拷贝!!因为要模拟的是原生指针,且这份资源归属是用户的,咱不能偷偷地copy一份。

C++11 unique_ptr

借鉴的是boost的scoped_ptr(防拷贝的智能指针),只是在auto_ptr的基础上加了两句。详细的模拟实现请参考我的gitee库

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

C++11 shared_ptr

shared_ptr本身是线程安全的,用锁保护了引用计数的++/–,但它管理的资源不是线程安全的,需要用户手动控制。共享指针的mutex保护的是自己的计数器的线程安全,不保护资源的线程安全。

借鉴的是boost的shared_ptr(可以拷贝的智能指针),原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数器,用于记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

计数器的实现

可以采用 static map<T* ptr, static int count>; ,但是后续加锁是个大问题。

简单的方法就是申请新资源的时候再开一块计数器空间,让对象中的一个指针指向资源,另个指针指向计数器空间。

成员变量的计数器要写成int* count; ,可以实现对单份资源的管理,让一份资源拥有一个计数器

不能写int count; 这个变成了单个对象的计数;

也不能写static int count; 因为静态成员变量是属于整个类的,所有对象均可以访问,相当于公共计数器,不是独一份资源的计数器。

锁的实现

一份资源对应一把锁,故写成mutex* _mutex;

在用shared_ptr的时候,如何保证资源的线程安全?**重新用一把锁保护资源!**注意此处不能去用计数器的锁,因为两块资源不是一样的空间。

存在的问题-循环引用

在如下所示struct ListNode对象创建的双向链表处会出问题,因为其成员变量是std::shared_ptr<ListNode>类型的,node1本身和node2._prev会同时指向node1的引用计数,node2本身和node1._next会同时指向node2的引用计数。

node1和node2都是局部对象,出了作用域就会销毁,但是他们的_prev_next没法对引用计数器–。类对象的成员什么时候销毁?对象销毁的时候,其成员变量才会销毁,但是node2要等node1._next销毁了才会销毁,node1要等node2._prev销毁了才会销毁,这就造成了死循环,谁都没有销毁。

拓展来说,假设有两个类,类A中有个成员管理着类B的另一个成员,类B中有个成员管理着类A的另一个成员,这也会造成循环引用。

struct ListNode
{
    std::shared_ptr<ListNode> _prev;
    std::shared_ptr<ListNode> _next;
    ~ListNode() {std::cout << "~ListNode()" << std::endl; }//仅为观察实验现象,无其他作用
};
int main()
{
    std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);
    node1->_next = node2;//赋值构造 会对node1的引用计数做++
    node2->_prev = node1;//赋值构造 会对node2的引用计数做++
    return 0;
}

只要屏蔽node1->_next = node2;或者node2->_prev = node1;里的任何一句,都不会有循环引用的问题。

C++11 weak_ptr

没有使用RAII思想。其功能是可以指向资源/访问资源,但是不参与资源的管理,不增加引用计数与shared_ptr搭配使用。

struct ListNode
{
    std::weak_ptr<ListNode> _prev;//使用weak_ptr来解决
    std::weak_ptr<ListNode> _next;//使用weak_ptr来解决
    ~ListNode() {std::cout << "~ListNode()" << std::endl; }//仅为观察实验现象,无其他作用
};
int main()
{
    std::shared_ptr<ListNode> node1(new ListNode);
    std::shared_ptr<ListNode> node2(new ListNode);
    node1->_next = node2;//赋值构造 会对node1的引用计数做++
    node2->_prev = node1;//赋值构造 会对node2的引用计数做++
    return 0;
}

C++11 default_delete

以上4种指针只能管理单个new空间,因为delete在释放资源的时候,需要考虑到是数组(要用delete[])还是单个数(delete即可),new[]delete[]要匹配,否则可能会导致问题。

于是C++11引入定制删除器,默认的定制删除器用的是delete。constexpr shared_ptr() noexcept;

用到定制删除器可以看这个构造函数template <class U, class D> shared_ptr (U* p, D del);,其中del可以用函数指针、仿函数、lambda表达式。

template<class T>
struct delete_array
{
	operator()(const T* ptr)
	{
		delete[] ptr;
		cout << "delete[] :" << ptr << endl;
	}
};

int main()
{
	std::shared_ptr<int> sp1(new int[10], delete_array<int>());
    //仿函数
	std::shared_ptr<string> sp2(new string[10], delete_array<string>());
    //lambda表达式
	std::shared_ptr<string> sp3(new string[10], [](string* ptr) {delete[] ptr; });
	std::shared_ptr<FILE> sp4(fopen("text.txt", "r"), [](FILE* ptr) {fclose(ptr); });
	
	return 0;
}

结合定制删除器改造shared_ptr

template<class T>
struct defaule_delete
{
    void operator()(T* ptr)
    {
        delete ptr;
    }
};

template<class T, class D = defaule_delete<T>>
class shared_ptr
{
public:
    shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pRefCount(new int(1)), _mutex(new mutex)
    {}

    void Release()
    {
        int flag = 0;//标记是否需要释放锁,这是局部变量(独立栈空间)
        _mutex->lock();
        if (--(*_pRefCount) == 0)
        {
            flag = 1;
            //delete _ptr;
            _del(_ptr);
            delete _pRefCount;
        }
        _mutex->unlock();

        if (flag) delete _mutex;
    }

    ~shared_ptr()
    {
        Release();
    }
    shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr), _pRefCount(sp._pRefCount), _mutex(sp._mutex)
    {
        _mutex->lock();
        ++(*_pRefCount);
        _mutex->unlock();
    }
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        if (sp._ptr != _ptr)
        {
            Release();
            _ptr = sp._ptr;
            _pRefCount = sp._pRefCount;
            _mutex = sp._mutex;

            _mutex->lock();
            ++(*_pRefCount);
            _mutex->unlock();
        }

        return *this;
    }

    int use_count() { return *_pRefCount; }
    T* get()
    {
        return _ptr;
    }
    // 像指针一样使用
    T& operator*() { return *_ptr; }
    T* operator->() { return _ptr; }
    T* get() const { return _ptr; }
private:
    T* _ptr;
    int* _pRefCount;
    mutex* _mutex;
    D _del;
};

template<class T>
struct delete_array
{
    void operator()(const T* ptr)
    {
        delete[] ptr;
        //cout << "delete[]" << endl;
    }
};

struct close_file
{
    void operator()(FILE* ptr)
    {
        fclose(ptr);
    }
};
int main()
{
    shared_ptr<int, delete_array<int>> sp1(new int[5]);
    shared_ptr<string, delete_array<string>> node1(new string[5]);
    shared_ptr<FILE, close_file> file1(fopen("test.txt", "r"));
    //shared_ptr<int> sp2(new int);
    return 0;
}