• C++11智能指针


    一、什么是智能指针

    C++中的智能指针是一种特殊类型的指针,它能够自动管理动态分配的内存资源,从而简化内存管理的过程并减少内存泄漏的风险。智能指针通过在对象上使用引用计数技术来跟踪资源的使用情况,并在不再需要该资源时自动释放它。这种自动化的内存管理可以帮助开发人员避免手动释放内存的繁琐工作,并减少因忘记释放内存而导致的错误。

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

    在“异常”出来之前我们对于动态申请的内存资源只要遵循“谁申请,谁释放”的原则就能够很大程度上减少内存泄漏问题了;但是在有了“异常”之后,因为抛异常会使得执行流不按代码的顺序执行,即执行流通过抛异常会直接跳到catch的地方,所以即使我们在new之后已经写上了detele释放,但是由于抛异常的行为导致我们的程序依然会内存泄漏,而且这个内存泄漏是不可预测的,因为你也不知道什么时候会抛异常,抛异常之后有哪些资源是申请了没有释放的,这些我们都无法预测,而内存泄漏是一个很严重的问题,所以必须要解决,而智能指针就是用来解决这个问题的。

    内存泄漏的示例:

    double 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 抛异常会如何?答:会导致p1指向的内存没有被释放,内存泄露
    	// 3、如果div调用这里又会抛异常会如何?答:导致p1和p2指向的内存都没有被释放,内存泄漏
    	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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    三、内存泄漏

    3.1 什么是内存泄漏?内存泄漏的危害是什么?

    什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
    该段内存的控制,因而造成了内存的浪费。
    内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

    3.2 内存泄漏的分类

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

    3.3 如何检测内存泄漏?

    使用一些第三方工具检测。
    1、Valgrind:Valgrind是一个用于内存调试、内存泄漏检测以及性能分析的开源工具。它支持多种平台,包括Linux、macOS等。Valgrind可以检测出程序中未被释放的内存、内存泄漏、内存访问错误等问题。

    2、AddressSanitizer (ASan):ASan是Google开发的一个快速内存错误检测器。它可以检测出诸如堆溢出、使用后释放、越界读写等错误,并且可以与Clang和GCC编译器无缝集成。虽然ASan主要关注运行时内存错误,但它也可以帮助发现某些类型的内存泄漏。

    3、Purify:Purify是IBM开发的一款商业内存调试工具,它可以检测出内存泄漏、内存访问错误、野指针等问题。Purify提供了一个直观的界面来显示检测结果,并提供了详细的报告来帮助开发人员定位问题。

    4、Visual Leak Detector (VLD):VLD是一个用于Visual Studio的开源内存泄漏检测插件。它可以集成到Visual Studio中,实时检测内存泄漏,并提供详细的报告。VLD特别适用于Windows平台上的C++项目。

    5、Intel Inspector:Intel Inspector是Intel开发的一款功能强大的性能分析和内存调试工具。它支持多种平台,包括Windows、Linux和macOS。Intel Inspector可以检测出内存泄漏、内存访问错误、线程错误等问题,并提供详细的报告和可视化界面。

    3.4 如何避免内存泄漏?

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

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

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

    4.1 RAII

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

    //使用RAII思想设计的一个SmartPtr类
    template <class T>
    class SmartPtr
    {
    public:
    	//构造函数
    	SmartPtr(T* ptr = nullptr)
    		:_ptr(ptr)
    	{
    		cout << _ptr << endl;
    	}
    
    	//析构函数
    	~SmartPtr()
    	{
    		if (_ptr)
    		{
    			cout << "~SmartPtr()" << endl;
    			delete _ptr;
    		}
    	}
    private:
    	T* _ptr;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4.2 智能指针的原理

    上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针是可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要重载一下* 和->,才可让其像指针一样去使用。

    
    //使用RAII思想设计的一个简单的智能指针,要求要能像指针一样使用
    template <class T>
    class SmartPtr
    {
    public:
    	//构造函数
    	SmartPtr(T* ptr = nullptr)
    		:_ptr(ptr)
    	{
    		cout << _ptr << endl;
    	}
    
    	//析构函数
    	~SmartPtr()
    	{
    		if (_ptr)
    		{
    			cout << "~SmartPtr()" << endl;
    			delete _ptr;
    		}
    	}
    
    	T& operator*()
    	{
    		return *_ptr;
    	}
    
    	T* operator->()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    };
    
    int main()
    {
    	SmartPtr<int> ptr(new int(2));
    	//SmartPtr ptr1(ptr);
    	//像指针一样使用
    	cout << *ptr << endl;
    	*ptr = 1;
    	cout << *ptr << endl;
    
    	SmartPtr<pair<string, string>> ptr1(new pair<string, string>("科比","kb"));
    	//注意:这里其实是有两个->的,但是在语法上为了可读性,省略了一个->
    	cout << ptr1->first << ":" << ptr1->second << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    所以智能指针的原理就是:
    1、RAII特性
    2、重载operator*和operator->,使它可以像指针一样使用。

    4.3 std::auto_ptr

    std::auto_ptr

    C++98版本的库中就提供了auto_ptr的智能指针。
    auto_ptr的实现原理:管理权转移的思想.

    赋值重载函数:
    在这里插入图片描述
    拷贝构造函数:
    在这里插入图片描述

    	//缺陷:因为auto_ptr的本质是管理权转移,所以拷贝构造时会导致被拷贝
    	//的对象的指针变为空指针,再访问该指针就会出问题,所以不能拷贝和赋值
    	template <class T>
    	class auto_ptr
    	{
    	public:
    		//构造函数
    		auto_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    
    		//析构函数
    		~auto_ptr()
    		{
    			if (_ptr)
    			{
    				cout << "~auto_ptr()" << endl;
    				delete _ptr;
    			}
    		}
    
    		//拷贝构造函数
    		auto_ptr(auto_ptr<T>& aptr)
    		{
    			//管理权转移
    			_ptr = aptr._ptr;
    			aptr._ptr = nullptr;
    		}
    
    		auto_ptr<T>& operator=(auto_ptr<T>& aptr)
    		{
    			if (&aptr != this)
    			{
    				if (_ptr)
    				{
    					delete _ptr;
    				}
    				//管理权转移
    				_ptr = aptr._ptr;
    				aptr._ptr = nullptr;
    			}
    			return *this;
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    	};
    	
    	int main()
    	{
    		kb::auto_ptr<int> ap1 = new int(1);
    		kb::auto_ptr<int> ap3 = new int(3);
    	
    		//赋值之后ap1变成了nullptr,不能再访问ap1指向的资源了
    		ap3 = ap1;
    	
    		//	kb::auto_ptr ap1 = new int(1);
    		//	kb::auto_ptr ap3(ap1);//管理权转移,ap1变为nullptr,不能再访问ap1指向的内容
    	
    		return 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    4.4 std::unique_ptr

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

    原理:简单粗暴 – 防拷贝

    	// C++11库才更新智能指针出来
    	// C++11出来之前,boost搞出了更好用的scoped_ptr/shared_ptr/weak_ptr
    	// C++11将boost库中智能指针精华部分吸收了过来
    	// C++11->unique_ptr/shared_ptr/weak_ptr
    
    
    	// unique_ptr/scoped_ptr
    	// 原理:简单粗暴 -- 防拷贝
    	//unique_ptr是唯一的指针,本质是防拷贝,缺陷也是不能拷贝和赋值
    	template <class T>
    	class unique_ptr
    	{
    	public:
    		//构造函数
    		unique_ptr(T* up)
    			:_ptr(up)
    		{}
    
    		//析构函数
    		~unique_ptr()
    		{
    			if (_ptr)
    			{
    				cout << "~unique_ptr()" << endl;
    				delete _ptr;
    			}
    		}
    
    		//因为unique_ptr是防拷贝的,所以直接把拷贝构造函数和赋值重载函数删除即可
    		unique_ptr(const unique_ptr<T>& up) = delete;
    		unique_ptr<T>& operator=(const unique_ptr& up) = delete;
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    	};
    	int main()
    	{
    		kb::unique_ptr<int> ptr(new int(1));
    		cout << *ptr << endl;
    		(*ptr)++;
    		cout << *ptr << endl;
    	
    		return 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    在这里插入图片描述

    4.5 std::shared_ptr

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

    std::shared_ptr

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

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

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

    shared_ptr的线程安全分为两方面:

    1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++、- -是需要加锁的,也就是说引用计数的操作是线程安全的。
    2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
      第一个线程安全问题是智能指针可以解决的,就是访问引用计数时加锁,但是第二个线程安全问题智能指针是管不了的,需要由调用的人确保堆上的资源的访问是线程安全的。

    4.5.3 定制删除器

    如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题。
    具体细节可以看以下shared_ptr的实现代码。

    4.5.3 shared_ptr 的模拟实现代码

    	template <class T>
    	class shared_ptr
    	{
    	public:
    
    		//要求:
    		//1、RAII
    		//2、像指针一样
    
    		//构造函数
    		shared_ptr(T* ptr = nullptr)
    			:_ptr(ptr)
    			, _pRefCount(new int(1))
    			,_pmtx(new mutex)
    		{}
    
    		//构造函数
    		//这是一个函数模板,通过传过来的参数自动推导类型,因为_del是一个包装器,
    		//所以无论D是什么类型的删除器,_del都能接收
    		template <class T,class D>
    		shared_ptr(T* ptr , D del)
    			:_ptr(ptr)
    			,_pRefCount(new int(1))
    			,_pmtx(new mutex)
    			,_del(del)
    		{}
    
    		// 这里的返回值千万不能写成shared_ptr,否则程序会崩溃
    		// 因为写成shared_ptr相当于拷贝构造了一个shared_ptr
    		// 但是这个shared_ptr并没有开辟空间,而是用_ptr+n位置的
    		// 指针来构造,所以这个临时对象在析构的时候相当于把原来的_ptr+n
    		// 的空间析构了,而这个空间本身就不属于这个临时对象,并且指针也
    		// 只能从其实位置析构,不能从中间位置析构,所以这里写曾shared_ptr
    		// 是一定会崩溃的
    		//shared_ptr operator+(int n)
    		T* operator+(int n)
    		{
    			return _ptr + n;
    		}
    
    		//析构函数
    		~shared_ptr()
    		{
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    			bool flag = false;
    			if (--(*_pRefCount) == 0 && _ptr)
    			{
    				cout << "~shared_ptr()" << endl;
    				_del(_ptr);
    				delete _pRefCount;
    				flag = true;
    			}
    			//解锁
    			_pmtx->unlock();
    			//如果这是最后一个指向这块资源的指针,即_ptr和_pRefCount都释放了,那么应该把_pmtx锁也释放掉
    			if (flag == true)
    			{
    				delete _pmtx;
    			}
    		}
    
    		//拷贝构造函数
    		//sp3(sp1)
    		shared_ptr(const shared_ptr<T>& sp)
    			: _ptr(sp._ptr)
    			, _pRefCount(sp._pRefCount)
    			,_pmtx(sp._pmtx)
    		{
    			cout << "shared_ptr(const shared_ptr& sp)" << endl;
    
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    			(*_pRefCount)++;
    			//解锁
    			_pmtx->unlock();
    		}
    
    		//赋值重载函数
    		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    		{
    			//首先一定要判断不能给自己赋值,原因在于下一个if语句解释。
    			//这里能用if(this!=&sp)吗?可以是可以,但是有一种场景会
    			//做多余的工作,具体场景看图解
    			if (_ptr == sp._ptr)
    			{
    				return *this;
    			}
    			//也可以通过_pRefCount 是否等于 sp._pRefCount判断是不是给自己赋值
    			//if (_pRefCount == sp._pRefCount)
    			//{
    			//	return *this;
    			//}
    
    			cout << "shared_ptr operator=(const shared_ptr& sp)" << endl;
    
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    
    			//如果自己给自己赋值,那么如果此时只有自己一个指针指向这块空间
    			//那么--(*_pRefCount)就会等于0,那么进入if会把_ptr和_pRefCount释放掉,
    			//自己给自己赋值说明sp也是自己,所以sp._ptr会把已经释放的空间又赋
    			// 值给_ptr,后续访问_ptr的时候就是非法访问了,会导致程序崩溃
    			if (--(*_pRefCount) == 0)
    			{
    				_del(_ptr);
    				delete _pRefCount;
    			}
    			_ptr = sp._ptr;
    			_pRefCount = sp._pRefCount;
    			(*_pRefCount)++;
    
    			//解锁
    			_pmtx->unlock();
    
    			return *this;
    		}
    
    		T& operator*() const
    		{
    			return *_ptr;
    		}
    
    		T* operator->() const
    		{
    			return _ptr;
    		}
    
    		T* get() const
    		{
    			return _ptr;
    		}
    
    		int UseCount() const
    		{
    			return *_pRefCount;
    		}
    
    	private:
    		T* _ptr;
    		//1、这里能否直接用一个int _pRefCount,不能,因为每个指针应该只有一个引用计数,
    		//如果每一个对象都有一个引用计数,那么引用计数就失去了它的作用了
    		//2、那么就用一个静态的static int _pRefCount变量,静态变量保证每一个类只有一个,
    		//所以所有的对象都是共用一个_pRefCount变量的。也不能用静态变量,为什么?因为不同的
    		//对象可能管理不同的资源,如果所有的对象都共用一个引用计数的话那么指向不同资源的
    		//指针的引用计数也会混到一起,所以不能用静态变量
    		
    		//3、所以最好的方案就是每一块内存资源对应一个引用计数,在拷贝构造和赋值的时候对
    		//指向同一块资源的引用计数++即可,如何保证每一块内存资源对应一个引用计数呢?可以
    		//在构造函数的时候动态开辟一个保存引用计数的变量,因为每个对象只会调用一次构造函数,
    		//所以后面的拷贝构造和赋值都只会对同一个引用计数进行操作。
    		int* _pRefCount = nullptr;
    
    		//因为引用计数有可能是多个对象共享的,所以当一个对象在修改引用计数的时候,其它对象
    		//不能同时修改引用计数,否则可能会导致引用计数不准确,出现程序崩溃或者内存泄漏的问题
    		mutex* _pmtx = nullptr;
    
    		//定制删除器
    		// 默认情况下,释放内存用的都是delete,所以缺省值就是delete释放,
    		//但是当我们的内存不是new出来的时候,比如是new[]的,或者fopen的,这时需要
    		//正确地释放内存需要用对应的关键字,例如fopen对应fclose,new[]对应delete[],
    		//我们发现,虽然指针的释放的关键字会有区别,但是释放的函数都是只需要传一个T*指针的指针
    		//就可以了。而删除器本质就是一个可调用对象,可调用对象分为函数指针,仿函数
    		//和lambda表达式,我们并不确定用户会使用哪一种可调用对象作为删除器,
    		// 但是删除器的参数和返回值类型都是一样的(无返回值),对于malloc,释放函数为free(ptr),
    		// 无返回值;对于new,释放函数为delete ptr,无返回值;对于new[],释放函数为delete[] ptr,
    		// 无返回值;对于fopen,释放函数为fclose,无返回值;所以这时我们的包装器就派上用场了,因为
    		// 包装器就是包装具有同样参数和返回值的可调用对象的,所以我们可以用包装器接收用户传过来的删除器,
    		// 删除器有对应的释放内存的方法,在智能指针析构的时候通过删除器就可以做到正确地释放内存了
    		function<void(T*)> _del = [](T* ptr) {
    			delete ptr;
    			ptr = nullptr; };
    	};
    	// 仿函数的删除器
    template<class T>
    struct FreeFunc 
    {
    	void operator()(T* ptr)
    	{
    		cout << "free:" << ptr << endl;
    		free(ptr);
    	}
    };
    
    template<class T>
    struct DeleteArrayFunc 
    {
    	void operator()(T* ptr)
    	{
    		cout << "delete[]" << ptr << endl << endl;
    		delete[] ptr;
    	}
    };
    
    
    int main()
    {
    	kb::shared_ptr<int> ptr1(new int[10], DeleteArrayFunc<int>());
    	*ptr1 = 1;
    	cout << *ptr1 << endl;
    	*(ptr1 + 1) = 2;
    	cout << *(ptr1 + 1) << endl;
    
    	kb::shared_ptr<int> ptr2((int*)malloc(100), [](int* p) {
    		cout << "free:" << p << endl << endl;
    		free(p);
    		p = nullptr;
    		});
    
    	kb::shared_ptr<FILE> ptr((FILE*)fopen("test.txt", "w"), [](FILE* fp) {
    		cout << "fclose:" << fp << endl << endl;
    		fclose(fp);
    		fp = nullptr;
    		});
    
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218

    4.5.4 std::shared_ptr的循环引用

    在这里插入图片描述
    在这里插入图片描述

    	struct Node
    	{
    		int _a = 0;
    	
    		//用shared_ptr赋值给weak_ptr的_prev和_next时不
    		//增加节点的引用计数,即用weak_ptr指向某个节点,不会
    		//增加这个节点的引用计数
    		kb::weak_ptr<Node> _prev;
    		kb::weak_ptr<Node> _next;
    	
    		~Node()
    		{
    			cout << "~Node()" << endl;
    		}
    	};
    	template <class T>
    	class shared_ptr
    	{
    	public:
    
    		//要求:
    		//1、RAII
    		//2、像指针一样
    
    		//构造函数
    		shared_ptr(T* ptr = nullptr)
    			:_ptr(ptr)
    			, _pRefCount(new int(1))
    			,_pmtx(new mutex)
    		{}
    
    		//构造函数
    		//这是一个函数模板,通过传过来的参数自动推导类型,因为_del是一个包装器,
    		//所以无论D是什么类型的删除器,_del都能接收
    		template <class T,class D>
    		shared_ptr(T* ptr , D del)
    			:_ptr(ptr)
    			,_pRefCount(new int(1))
    			,_pmtx(new mutex)
    			,_del(del)
    		{}
    
    		// 这里的返回值千万不能写成shared_ptr,否则程序会崩溃
    		// 因为写成shared_ptr相当于拷贝构造了一个shared_ptr
    		// 但是这个shared_ptr并没有开辟空间,而是用_ptr+n位置的
    		// 指针来构造,所以这个临时对象在析构的时候相当于把原来的_ptr+n
    		// 的空间析构了,而这个空间本身就不属于这个临时对象,并且指针也
    		// 只能从其实位置析构,不能从中间位置析构,所以这里写曾shared_ptr
    		// 是一定会崩溃的
    		//shared_ptr operator+(int n)
    		T* operator+(int n)
    		{
    			return _ptr + n;
    		}
    
    		//析构函数
    		~shared_ptr()
    		{
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    			bool flag = false;
    			if (--(*_pRefCount) == 0 && _ptr)
    			{
    				cout << "~shared_ptr()" << endl;
    				_del(_ptr);
    				delete _pRefCount;
    				flag = true;
    			}
    			//解锁
    			_pmtx->unlock();
    			//如果这是最后一个指向这块资源的指针,即_ptr和_pRefCount都释放了,那么应该把_pmtx锁也释放掉
    			if (flag == true)
    			{
    				delete _pmtx;
    			}
    		}
    
    		//拷贝构造函数
    		//sp3(sp1)
    		shared_ptr(const shared_ptr<T>& sp)
    			: _ptr(sp._ptr)
    			, _pRefCount(sp._pRefCount)
    			,_pmtx(sp._pmtx)
    		{
    			cout << "shared_ptr(const shared_ptr& sp)" << endl;
    
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    			(*_pRefCount)++;
    			//解锁
    			_pmtx->unlock();
    		}
    
    		//赋值重载函数
    		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    		{
    			//首先一定要判断不能给自己赋值,原因在于下一个if语句解释。
    			//这里能用if(this!=&sp)吗?可以是可以,但是有一种场景会
    			//做多余的工作,具体场景看图解
    			if (_ptr == sp._ptr)
    			{
    				return *this;
    			}
    			//也可以通过_pRefCount 是否等于 sp._pRefCount判断是不是给自己赋值
    			//if (_pRefCount == sp._pRefCount)
    			//{
    			//	return *this;
    			//}
    
    			cout << "shared_ptr operator=(const shared_ptr& sp)" << endl;
    
    			//在每一个修改_pRefCount的地方都要先加锁,保证访问_pRefCount是线程安全
    			_pmtx->lock();
    
    			//如果自己给自己赋值,那么如果此时只有自己一个指针指向这块空间
    			//那么--(*_pRefCount)就会等于0,那么进入if会把_ptr和_pRefCount释放掉,
    			//自己给自己赋值说明sp也是自己,所以sp._ptr会把已经释放的空间又赋
    			// 值给_ptr,后续访问_ptr的时候就是非法访问了,会导致程序崩溃
    			if (--(*_pRefCount) == 0)
    			{
    				_del(_ptr);
    				delete _pRefCount;
    			}
    			_ptr = sp._ptr;
    			_pRefCount = sp._pRefCount;
    			(*_pRefCount)++;
    
    			//解锁
    			_pmtx->unlock();
    
    			return *this;
    		}
    
    		T& operator*() const
    		{
    			return *_ptr;
    		}
    
    		T* operator->() const
    		{
    			return _ptr;
    		}
    
    		T* get() const
    		{
    			return _ptr;
    		}
    
    		int UseCount() const
    		{
    			return *_pRefCount;
    		}
    
    	private:
    		T* _ptr;
    		//1、这里能否直接用一个int _pRefCount,不能,因为每个指针应该只有一个引用计数,
    		//如果每一个对象都有一个引用计数,那么引用计数就失去了它的作用了
    		//2、那么就用一个静态的static int _pRefCount变量,静态变量保证每一个类只有一个,
    		//所以所有的对象都是共用一个_pRefCount变量的。也不能用静态变量,为什么?因为不同的
    		//对象可能管理不同的资源,如果所有的对象都共用一个引用计数的话那么指向不同资源的
    		//指针的引用计数也会混到一起,所以不能用静态变量
    		
    		//3、所以最好的方案就是每一块内存资源对应一个引用计数,在拷贝构造和赋值的时候对
    		//指向同一块资源的引用计数++即可,如何保证每一块内存资源对应一个引用计数呢?可以
    		//在构造函数的时候动态开辟一个保存引用计数的变量,因为每个对象只会调用一次构造函数,
    		//所以后面的拷贝构造和赋值都只会对同一个引用计数进行操作。
    		int* _pRefCount = nullptr;
    
    		//因为引用计数有可能是多个对象共享的,所以当一个对象在修改引用计数的时候,其它对象
    		//不能同时修改引用计数,否则可能会导致引用计数不准确,出现程序崩溃或者内存泄漏的问题
    		mutex* _pmtx = nullptr;
    
    		//定制删除器
    		// 默认情况下,释放内存用的都是delete,所以缺省值就是delete释放,
    		//但是当我们的内存不是new出来的时候,比如是new[]的,或者fopen的,这时需要
    		//正确地释放内存需要用对应的关键字,例如fopen对应fclose,new[]对应delete[],
    		//我们发现,虽然指针的释放的关键字会有区别,但是释放的函数都是只需要传一个T*指针的指针
    		//就可以了。而删除器本质就是一个可调用对象,可调用对象分为函数指针,仿函数
    		//和lambda表达式,我们并不确定用户会使用哪一种可调用对象作为删除器,
    		// 但是删除器的参数和返回值类型都是一样的(无返回值),对于malloc,释放函数为free(ptr),
    		// 无返回值;对于new,释放函数为delete ptr,无返回值;对于new[],释放函数为delete[] ptr,
    		// 无返回值;对于fopen,释放函数为fclose,无返回值;所以这时我们的包装器就派上用场了,因为
    		// 包装器就是包装具有同样参数和返回值的可调用对象的,所以我们可以用包装器接收用户传过来的删除器,
    		// 删除器有对应的释放内存的方法,在智能指针析构的时候通过删除器就可以做到正确地释放内存了
    		function<void(T*)> _del = [](T* ptr) {
    			delete ptr;
    			ptr = nullptr; };
    	};
    
    	 weak_ptr不是RAII智能指针,专门为了解决shared_ptr的循环引用的
    	// 问题,weak_ptr不参与修改引用计数和资源的释放,允许访问资源
    	template <class T>
    	class weak_ptr
    	{
    	public:
    		weak_ptr()
    			:_ptr(nullptr)
    		{}
    
    		//拷贝构造不增加sp对象本身的引用计数,但是weak_ptr可以正常访问sp指向的对象
    		weak_ptr(const shared_ptr<T>& sp)
    			:_ptr(sp.get())
    		{}
    
    		//赋值也不增加sp对象本身的引用计数,但是weak_ptr可以正常访问sp指向的对象
    		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    		{
    			if (_ptr != sp.get())
    			{
    				_ptr = sp.get();
    			}
    			return *this;
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    	};
    
    	//shared_ptr智能指针是线程安全的吗?
    	//是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的,因为指
    	//向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。引用
    	//计数的线程安全问题,是智能指针要处理的。
    
    	//循环引用问题
    	int main()
    	{
    		// 循环引用
    		kb::shared_ptr<Node> sp1(new Node);
    		kb::shared_ptr<Node> sp2(new Node);
    	
    		cout << sp1.UseCount() << endl;
    		cout << sp2.UseCount() << endl;
    	
    		sp1->_next = sp2;
    		sp2->_prev = sp1;
    	
    		cout << sp1.UseCount() << endl;
    		cout << sp2.UseCount() << endl;
    	
    		return 0;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252

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

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

    五、总结

    通过以上的内容我们知道智能指针其实也就是通过一个类来封装管理一个指针,在构造函数的地方把这个指针给智能指针的类管理,在智能指针的析构函数中释放内存;这样在程序结束的时候智能指针就会自动调用析构函数释放内存,所以正确地使用智能指针是可以有效地减少因忘记释放而导致内存泄漏的问题的。

    5.1 智能指针一般都要处理以下问题:

    一、RAII,资源申请即初始化,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
    1、不需要显式地释放资源。
    2、采用这种方式,对象所需的资源在其生命期内始终保持有效。

    二、能够像指针一样使用。即这个智能指针要重载*,->,+等等指针能用的运算符。

    三、拷贝问题。详情看上文介绍。

    5.2 对比四个智能指针

    1、auto_ptr是通过管理权转移的方式实现的,缺点就是会使被拷贝对象的指针悬空,再访问可能会导致程序崩溃。所以一般不建议使用auto_ptr。
    2、unique_ptr是防拷贝的智能指针。禁止拷贝,简单粗暴,日常的不需要拷贝的场景建议使用。
    3、shared_ptr是能够共享的指针,也就是说可以拷贝。通过引用计数来实现,但是可能会导致循环引用,循环引用会导致内存泄漏,这时需要用weak_ptr来配合使用解决循环引用问题。
    4、weak_ptr不是RAII的智能指针,专门用来解决shared_ptr的循环引用问题,不参与引用计数的修改和指针的管理,能正常访问资源。

    以上就是今天想要跟大家分享的所有内容了,你学会了吗?如果感觉到有所帮助,那么点点赞点点关注呗,后期还会持续更新C++的相关知识哦,我们下期见!!!!

  • 相关阅读:
    如何进行 360 评估
    常用技能点:Java中数组复制的三种方式
    想知道图片转表格怎么转?简单实用的转换方法分享
    【Golang星辰图】Go语言的机器学习之旅:从基础知识到实际应用的综合指南
    百度爬虫的工作原理解析
    【紫光同创国产FPGA教程】——【PGL22G第九章】HDMI环路实验例程
    自定义组件-behaviors
    【开发教程7】疯壳·ARM功能手机-BLE透传实验教程
    向毕业妥协系列之深度学习笔记(三)DL的实用层面(上)
    华为云云耀云服务器L实例评测|docker 常用操作命令
  • 原文地址:https://blog.csdn.net/weixin_70056514/article/details/133747626