【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}

一、为什么需要智能指针?

下面我们先分析一下下面这段程序有没有什么内存方面的问题?

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}
  1. operator new可能会抛std::bad_alloc异常;div可能会抛std::invalid_argument异常。
  2. 如果p1这里new 抛异常,由于p2还未申请空间,所以不会有什么问题。
  3. 如果p2这里new 抛异常,就必须中途捕获异常,释放p1,再重新抛出异常。
  4. 如果div调用这里抛异常,也必须中途捕获异常,释放p1,p2,再重新抛出异常。
  5. 以此类推,如果还要定义其他指针申请空间的话,需要在每一个new语句位置添加try/catch捕获异常,并将定义在前的,已经申请空间成功的指针释放。这样的话代码书写起来太过冗余难看了。

二、内存泄漏

2.1 什么是内存泄漏,内存泄漏的危害

  • 什么是内存泄漏:
    内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
  • 内存泄漏的危害:
    长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致可用内存也来越少,系统响应越来越慢,申请内存失败等问题。
void MemoryLeaks()
{
	// 1.内存申请了忘记释放
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = new int;
	// 2.异常安全问题
	int* p3 = new int[10];
	Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
	delete[] p3;
}

2.2 内存泄漏分类(了解)

C/C++程序中一般我们关心两种方面的内存泄漏:

  1. 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete释放内存。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  2. 系统资源泄漏
    指程序使用系统分配的资源,比如:套接字、文件描述符、管道等。但是没有使用对应的函数释放掉,导致系统资源的浪费,严重的可能导致系统效能降低,系统执行不稳定。

2.3 如何检测内存泄漏(了解)

  • 在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
  • 在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库-CSDN博客
  • 其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)

2.4 如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这是理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用自己实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总的来说,内存泄漏非常常见,解决方案分为两种:1、事前预防型,如智能指针等。2、事后排查型,如内存泄漏检测工具。


三、智能指针的使用及原理

3.1 RAII设计思想

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所管理的资源在其生命期内始终保持有效。

RAII设计思想的应用:智能指针,std::lock_guard,std::unique_lock等。

3.2 像指针一样使用

指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

template <class T>
class smart_ptr
{
    T *_ptr;

public:
    // RAII设计思想:利用对象生命周期来控制程序资源
    // 构造时,将指针保存到对象内部
    smart_ptr(T *ptr)
        : _ptr(ptr)
    {
    }
    // 析构时,释放指针指向的堆空间
    ~smart_ptr()
    {
        if (_ptr != nullptr)
        {
            cout << "delete _ptr: " << _ptr << endl;
            delete _ptr;
        }
    }
    // 重载*和->使smart_ptr可以向原生指针一样使用
    T &operator*()
    {
        return *_ptr;
    }
    T *operator->()
    {
        return _ptr;
    }
    // 思考一下,使用默认生成的拷贝构造和赋值重载(值拷贝)可以吗?
};

使用smart_ptr修改一下第一段代码:

void Func()
{
	smart_ptr<int> p1(new int(1));
	smart_ptr<int> p2(new int(2));
    cout << *p1 << " " << *p2 << endl;
    *p1 = 10;
    *p2 = 20;
    cout << *p1 << " " << *p2 << endl;
	cout << div() << endl;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第1张图片


3.3 智能指针的拷贝问题

思考一下,使用默认生成的拷贝构造和赋值重载(值拷贝)可以吗?

将上面的p2改为拷贝构p1:smart_ptr p2(p1);

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第2张图片

单纯的值拷贝显然是不行的,出作用域时p1, p2对象都会调用析构函数,对同一堆空间double free,程序运行崩溃。

我们来看一看C++标准中是如何解决智能指针的拷贝问题的

3.3.1 std::auto_ptr (C++98)

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。

auto_ptr的实现原理:管理权转移

下面简化模拟实现了一份jmx::auto_ptr来了解它的原理:

namespace jmx
{ 
    template <class T>
    class auto_ptr
    {
        T *_ptr;

    public:
        auto_ptr(T *ptr)
            : _ptr(ptr)
        {
        }
        // auto_ptr的拷贝方法:管理权转移
        auto_ptr(auto_ptr &sp)
            : _ptr(sp._ptr)
        {
            sp._ptr = nullptr; // 将拷贝对象的指针置空
        }

        auto_ptr &operator=(auto_ptr &ap)
        {
            // 赋值重载注意检测是否是自己给自己赋值
            if (_ptr != ap._ptr)
            {
                // 释放当前指针指向的堆空间
                if (_ptr != nullptr)
                {
                    cout << "delete _ptr: " << _ptr << endl;
                    delete _ptr;
                }
                // 转移ap中的资源到当前对象中
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }
    };
}

不管是使用自己实现的jmx::auto_ptr还是使用C++98提供的std::auto_ptr执行上面的Func函数(用p1拷贝构造p2),都会发生段错误

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第3张图片

这是因为auto_ptr的拷贝方法是管理权转移,完成拷贝后原对象的指针会被置空,此时再解引用访问,就相当于访问空指针,自然会发生内存错误。并且可以看到现在的编译器会告警,不建议使用auto_ptr

auto_ptr的正确使用方法:

void Func()
{
    jmx::auto_ptr<int> p1(new int(1));
    jmx::auto_ptr<int> p2(new int(2));

    cout << "*p1 -> " << *p1 << endl;
    cout << "jmx::auto_ptr p3(p1);" << endl;
    jmx::auto_ptr<int> p3(p1); // 拷贝构造
    cout << "*p3 -> " << *p3 << endl;

    cout << "*p2 -> " << *p2 << endl;
    cout << "p2 = p3;" << endl;
    p2 = p3; // 赋值重载
    cout << "*p2 -> " << *p2 << endl;

    cout << div() << endl;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第4张图片

结论:

auto_ptr是一个的失败设计,因为资源的管理权转移,存在被拷贝对象指针悬空的问题。因此,很多公司明确要求不能使用auto_ptr


3.3.2 C++准标准库 Boost

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第5张图片

Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。

Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的**“准”标准库**。

Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎。

Boost中很多好用的内容都被C++标准吸收了,如C++11中的右值引用、线程库、智能指针等等。

C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptrshared_ptrweak_ptr
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost中的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

要想使用C++标准库定义的unique_ptrshared_ptrweak_ptr必须包含头文件

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第6张图片


3.3.3 std::unique_ptr (C++11)

C++11中开始提供更靠谱的unique_ptr

unique_ptr的实现原理:简单粗暴的防拷贝

下面简化模拟实现了一份jmx::unique_ptr来了解它的原理:

namespace jmx
{ 
    template <class T>
    class unique_ptr
    {
        T *_ptr;
        // C++98: 声明为私有,只声明不实现
        // unique_ptr(const unique_ptr &up);
        // unique_ptr &operator=(const unique_ptr &up);

    public:
        unique_ptr(T *ptr)
            : _ptr(ptr)
        {
        }

        ~unique_ptr()
        {
            if (_ptr != nullptr)
            {
                cout << "delete _ptr: " << _ptr << endl;
                delete _ptr;
            }
        }
        // C++11: delete关键字,语法直接支持
        unique_ptr(const unique_ptr &up) = delete;
        unique_ptr &operator=(const unique_ptr &up) = delete;
    };
}

总结:在一些不需要拷贝指针的场景中使用unique_ptr


3.3.4 std::shared_ptr (C++11)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象共同管理同一个资源。

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

下面简化模拟实现了一份jmx::shared_ptr来了解它的原理:

namespace jmx
{
	template <class T>
    class shared_ptr
    {
        T *_ptr;
        int *_pcount; // 引用计数的指针

    public:
        shared_ptr(T *ptr)
            : _ptr(ptr),
              _pcount(new int(1)) // 注意:在构造时申请引用计数的空间,为每一份资源绑定一个引用计数
        {
        }
        
		// 1.如果引用计数减到0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
        // 2.如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
        void Release()
        {
            cout << "void Release()" << endl;
            if (--(*_pcount) == 0 && _ptr != nullptr)
            {
                cout << "delete _ptr: " << _ptr << endl;
                delete _ptr;
                delete _pcount; // 记得释放引用计数的空间
            }
        }

        void AddCount()
        {
            ++(*_pcount);
        }

        shared_ptr(const shared_ptr &sp)
            : _ptr(sp._ptr),
              _pcount(sp._pcount)
        {
            AddCount();
        }

        shared_ptr &operator=(const shared_ptr &sp)
        {
            if (_ptr != sp._ptr) // 建议比较存储指针,防止不同对象同一资源相互赋值
            {
                Release();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                AddCount();
            }
            return *this;
        }

        ~shared_ptr()
        {
            Release();
        }
        
        T* get()
        {
            return _ptr;
        }
        
        int use_count()
        {
            return *_pcount;
        }
    };
}

使用jmx::shared_ptr再次执行Func函数(用p1拷贝构造p2):

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第7张图片


3.4 std::shared_ptr的线程安全问题

通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

3.4.1 引用计数的线程安全问题

  • 智能指针对象中的引用计数是管理同一资源的多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。
  • 所以智能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。

不加保护的修改引用计数:

namespace jmx
{
	template <class T>
    class shared_ptr
    {
        T *_ptr;
        int *_pcount; // 引用计数

    public:
        void Release()
        {
            if (--(*_pcount) == 0 && _ptr != nullptr)
            {
                cout << "delete _ptr: " << _ptr << endl;
                delete _ptr;
                delete _pcount; // 记得释放引用计数的空间
            }
        }

        void AddCount()
        {
            ++(*_pcount);
        }
    };
}

class Date
{
    int _year = 0;
    int _month = 0;
    int _day = 0;
};

void ThreadFunc(jmx::shared_ptr<Date>& sp, int n)
{
    while(n--)
    {
        // 两个线程并发的进行n次sp的拷贝构造,即n次引用计数的++和--,存在线程安全问题
        jmx::shared_ptr<Date> copy(sp); 
    }
}

int main()
{
    jmx::shared_ptr<Date> sp(new Date);
    cout << sp.get() << endl;
    int n = 100000;
    // 多线程执行
    thread t1(ThreadFunc, ref(sp), n); //线程函数传参传引用需要使用ref
    thread t2(ThreadFunc, ref(sp), n);
    t1.join();
    t2.join();
	// 最后打印sp的引用计数
    cout << sp.use_count() << endl;
    return 0;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第8张图片

发现最终引用计数并不是我们预想的1,sp指针指向的堆空间也未正确释放。

互斥访问引用计数:

namespace jmx
{
	// shared_ptr
    template <class T>
    class shared_ptr
    {
        T *_ptr;
        int *_pcount; // 引用计数的指针
        mutex *_mtx; // 互斥锁的指针
    public:
        shared_ptr(T *ptr)
            : _ptr(ptr),
              _pcount(new int(1)), // 注意:在构造时申请引用计数的空间,为每一份资源绑定一个引用计数
              _mtx(new mutex) // 注意:在构造时申请互斥量的空间,为每一份资源绑定一个互斥量
        {
        }

        void Release()
        {
            bool delete_flag = false;
            //加锁保护引用计数
            _mtx->lock();
            // cout << "void Release()" << endl;
            if (--(*_pcount) == 0 && _ptr != nullptr)
            {
                cout << "delete _ptr: " << _ptr << endl;
                delete _ptr;
                delete _pcount; // 记得释放引用计数的空间
                delete_flag = true;
            }
            _mtx->unlock();
            //必须先解锁再销毁互斥锁,因此:
            if(delete_flag)
            {
                delete _mtx; // 记得释放互斥量的空间
            }
        }

        void AddCount()
        {
            //加锁保护引用计数
            _mtx->lock();
            ++(*_pcount);
            _mtx->unlock();
        }

        shared_ptr(const shared_ptr &sp)
            : _ptr(sp._ptr),
              _pcount(sp._pcount),
              _mtx(sp._mtx) // 记得拷贝互斥量指针
        {
            AddCount();
        }

        shared_ptr &operator=(const shared_ptr &sp)
        {
            if (_ptr != sp._ptr)
            {
                Release();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _mtx = sp._mtx; // 记得拷贝互斥量指针
                AddCount();
            }
            return *this;
        }
    };
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第9张图片

shared_ptr的引用计数加锁保护,所以shared_ptr现在是线程安全的。

shared_ptr管理的对象是线程安全的吗?——不一定!


3.4.2 被管理对象的线程安全问题

智能指针管理的对象存放在堆上,两个线程同时去访问,会导致线程安全问题。

不加保护的访问管理对象:

void ThreadFunc(jmx::shared_ptr<Date>& sp, int n)
{
    while(n--)
    {
        jmx::shared_ptr<Date> copy(sp);
        //两个线程对同一日期类对象的年月日++100000次,如果是线程安全的,最后的结果应该都是200000。
        ++copy->_year;
        ++copy->_month;
        ++copy->_day;
    }
}

int main()
{
    jmx::shared_ptr<Date> sp(new Date);
    cout << sp.get() << endl;
    int n = 100000;
    thread t1(ThreadFunc, ref(sp), n);
    thread t2(ThreadFunc, ref(sp), n);
    t1.join();
    t2.join();

    cout << sp.use_count() << endl;
    //最后打印sp指向的日期类对象的年月日
    cout << sp->_year << "/" <<sp->_month << "/" << sp->_day << endl; 
    return 0;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第10张图片

显然不加保护的访问管理对象不是线程安全的。

互斥的访问管理对象:

void ThreadFunc(jmx::shared_ptr<Date> &sp, int n, mutex &mtx)
{
    while (n--)
    {
        jmx::shared_ptr<Date> copy(sp);
        // 访问管理对象时需要加锁
        lock_guard<mutex> lock(mtx); 
        ++copy->_year;
        ++copy->_month;
        ++copy->_day;
    }
}

int main()
{
    jmx::shared_ptr<Date> sp(new Date);
    cout << sp.get() << endl;
    int n = 100000;
    mutex mtx;
    thread t1(ThreadFunc, ref(sp), n, ref(mtx));
    thread t2(ThreadFunc, ref(sp), n, ref(mtx));
    t1.join();
    t2.join();

    cout << sp.use_count() << endl;
    cout << sp->_year << "/" << sp->_month << "/" << sp->_day << endl;
    return 0;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第11张图片

提示:

  1. 以上的所有测试使用std::shared_ptr可以得到相同的结果。
  2. std::shared_ptr::get用于返回存储的指针;std::shared_ptr::use_count用于返回引用计数。
  3. C++标准库中的std::shared_ptr要考虑的问题会更多,比如内存碎片、与std::weak_ptr进行配合等。因此std::shared_ptr的具体实现会相当复杂。以上的内容(jmx::shared_ptr)只是对其核心功能的简单模拟,二者的差别其实还是很大的。

3.5 std::shared_ptr的循环引用问题

3.5.1 循环引用的场景

请看下面的代码:

// shared_ptr的循环引用问题
template <class T>
struct ListNode
{
    // 不能使用原生指针,因为原生指针和shared_ptr不能相互赋值
    // ListNode *_prev; 
    // ListNode *_next;
    jmx::shared_ptr<ListNode> _prev;
    jmx::shared_ptr<ListNode> _next;
    T _val;

    ListNode(T val = T())
        : _prev(nullptr),
          _next(nullptr),
          _val(val)
    {
    }

    ~ListNode()
    {
        cout << "~ListNode()" << endl;
    }
};

void test_shared_cycle()
{
    jmx::shared_ptr<ListNode<int>> node1(new ListNode<int>(1));
    jmx::shared_ptr<ListNode<int>> node2(new ListNode<int>(2));

    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    // node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第12张图片

链接node1->_next和node2->_prev时,形成循环引用,最终节点并未成功释放,造成了内存泄漏问题。

循环引用分析:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第13张图片

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node1的成员,node1节点释放了,_next才会析构;而node1由_prev管理,_prev属于node2成员,node2节点释放了,_prev才会析构。这就叫循环引用,两者互相牵制谁也不放过谁。

提示:这里的node1, node2既是节点的指针,又是节点名。


3.5.2 std::weak_ptr

解决方案:在循环引用的场景下,把节点中的_prev和_next改成weak_ptr类型就可以了。

weak_ptr的原理:

  1. weak_ptr不是常规的智能指针,不支持RAII。
  2. weak_ptr不支持指针构造,但是支持shared_ptr构造和赋值。
  3. weak_ptr也可以向指针一样使用*和->
  4. weak_ptr是专门用于辅助解决shared_ptr的循环引用问题的:weak_ptr可以指向资源,但是他不参与管理资源,不增加引用计数。

下面简化模拟实现了一份jmx::weak_ptr来了解它的原理:

namespace jmx
{
	template <class T>
    class weak_ptr
    {
        T *_ptr;

    public:
        weak_ptr()
            : _ptr(nullptr)
        {
        }
        // weak_ptr只是单纯的指向资源,不参与管理资源,不增加引用计数。
        weak_ptr(shared_ptr<T> sp)
            : _ptr(sp.get())
        {
        }
		// weak_ptr也可以向指针一样使用*和->
        T &operator*()
        {
            return *_ptr;
        }
        T *operator->()
        {
            return _ptr;
        }
    };
}

把节点中的_prev和_next改成weak_ptr类型,重新编译运行:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第14张图片

shared_ptr的循环引用问题也得到了很好的解决。

提示:

  1. 以上的所有测试使用std::shared_ptrstd::weak_ptr可以得到相同的结果。

  2. 标准库中实现的std::weak_ptr中也包含引用计数,仅用于检查weak_ptr是否过期,即是否还有其他std::shared_ptr指向该资源。已经过期的weak_ptr不能再被访问。

  3. std::weak_ptr::use_count用于返回引用计数;std::weak_ptr::expired用于检查weak_ptr是否过期,This function shall return the same as (use_count()==0)

  4. 以上的jmx::weak_ptrstd::weak_ptr的简单模拟,实际std::weak_ptr的实现要复杂得多。


3.6 定制删除器(jmx::shared_ptr最终版)

如果不是new出来的对象如何通过智能指针管理呢?

  • 其实shared_ptr设计了一个删除器来解决这个问题。所谓删除器其实就是一个可调用对象(三者中的任意一个),shared_ptr最终会通过指定的删除器对存储的指针进行空间释放或销毁。
  • 通过指定删除器,我们不仅可以调用delete或free释放单个空间,还可以调用delete[]释放连续的多个空间,甚至可以调用fclose关闭文件。

jmx::shared_ptr最终版:

namespace jmx
{
	// shared_ptr最终版
    template <class T>
    class shared_ptr
    {
        T *_ptr = nullptr; // 存储指针
        int *_pcount; // 引用计数指针
        mutex *_mtx; // 互斥量指针
        function<void(T *)> _del = [](T *ptr)  // 注意:模板参数D属于构造函数,函数外不能使用,所以借助包装器实现
        { cout << "lambda: delete ptr" << endl;
            delete ptr }; // 删除器,默认为lambda: delete

    public:
        shared_ptr(T *ptr)
            : _ptr(ptr),
              _pcount(new int(1)),
              _mtx(new mutex)
        {
        }
        
        template <class D> //删除器使用模板,支持任意类型的可调用对象
        shared_ptr(T *ptr, D del)
            : _ptr(ptr),
              _pcount(new int(1)),
              _mtx(new mutex),
              _del(del)
        {
        }

        void Release()
        {
            bool delete_flag = false;
            _mtx->lock();
            // cout << "void Release()" << endl;
            if (--(*_pcount) == 0 && _ptr != nullptr)
            {
                cout << "_del(_ptr): " << _ptr << endl;
                _del(_ptr); // 使用删除器释放_ptr
                delete _pcount;
                delete_flag = true;
            }
            _mtx->unlock();
            if (delete_flag)
            {
                delete _mtx;
            }
        }

        void AddCount()
        {
            _mtx->lock();
            ++(*_pcount);
            _mtx->unlock();
        }

        shared_ptr(const shared_ptr &sp)
            : _ptr(sp._ptr),
              _pcount(sp._pcount),
              _mtx(sp._mtx),
              _del(sp._del) // 记得拷贝删除器
        {
            AddCount();
        }

        shared_ptr &operator=(const shared_ptr &sp)
        {
            if (_ptr != sp._ptr)
            {
                Release();
                _ptr = sp._ptr;
                _pcount = sp._pcount;
                _mtx = sp._mtx;
                _del = sp._del; // 记得拷贝删除器
                AddCount();
            }
            return *this;
        }

        ~shared_ptr()
        {
            Release();
        }

        T *get()
        {
            return _ptr;
        }

        int use_count()
        {
            return *_pcount;
        }

        T &operator*()
        {
            return *_ptr;
        }

        T *operator->()
        {
            return _ptr;
        }
    };
}

测试程序:

// 定制删除器
struct Date
{
    int _year = 0;
    int _month = 0;
    int _day = 0;
    ~Date()
    {
        cout << "~Date()" << endl;
    }
};

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

void test_shared_deleter()
{
    // 使用默认的lambda,释放单个空间
    jmx::shared_ptr<Date> sp1(new Date);
    // 使用函数对象DatesDeleter,释放连续的多个空间
    jmx::shared_ptr<Date> sp2(new Date[3], DeleteArray<Date>()); 
    // 使用指定的lambda,调用fclose关闭文件
    jmx::shared_ptr<FILE> sp3(fopen("./smart_ptr.cc", "r"), [](FILE *fp){
        cout << "lambda: fclose(fp)" << endl;
        fclose(fp); });
}

运行结果:

【C++11】智能指针 {内存泄漏;RAII设计思想,智能指针的拷贝问题;auto_ptr;unique_ptr;shared_ptr & weak_ptr:线程安全问题,循环引用问题,定制删除器}_第15张图片

你可能感兴趣的:(C++,c++,指针,内存泄漏,线程安全,开发语言)