C++进阶(智能指针)

智能指针原理

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。

智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

智能指针的使用

智能指针在C++11版本之后提供,包含在头文件< memory>中:shared_ptrunique_ptrweak_ptr(注意:auto_ptr是一种存在缺陷的智能指针,在C++11中已经被禁用了)

shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象。标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。

RAII

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

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

(2)代码模拟

实现智能指针时需要考虑以下三个方面的问题:

  • 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性
  • *->运算符进行重载,使得该对象具有像指针一样的行为
  • 智能指针对象的拷贝问题
// RAII
// 用起来像指针一样
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
 
	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
 
	// 像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}
 
	T* operator->()
	{
		return _ptr;
	}
 
private:
	T* _ptr;
};

(3)为什么要解决智能指针对象的拷贝问题

对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1); //拷贝构造
 
	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4(new int);
	sp3 = sp4; //拷贝赋值
	
	return 0;
}
  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
  • 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。
  • 需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

unique_ptr

原理和使用

unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

相比与原始指针,unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
示例:

#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能赋值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷贝
        std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
        uptr2.release(); //释放所有权
    }
    //超过uptr的作用域,内存释放
}

说明:C++有一个标准库函数move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

模拟实现

namespace XM
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{}
 
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
 
		T& operator*() { return *_ptr; }
		T* operator->() { return _ptr; }
 
	private:
		// C++98防拷贝的方式:只声明不实现+声明成私有
		unique_ptr(const unique_ptr<T>& sp);
		unique_ptr& operator=(const unique_ptr<T>& sp);
 
		// C++11防拷贝的方式:delete
		unique_ptr(const unique_ptr<T>& sp) = delete;
		unique_ptr& operator=(const unique_ptr<T>& sp) = delete;
 
	private:
		T* _ptr;
	};
}

share_ptr

原理和使用

C++ 11中最常用的智能指针类型为shared_ptr。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr, weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

  • shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

注意事项:

  • 初始化。智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如:std::shared_ptr< int> p4 = new int(1);的写法是错误的!

  • 拷贝和赋值。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。

  • get函数获取原始指针。

  • 不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。

  • 避免循环引用。shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。循环引用在weak_ptr中介绍。

成员函数:

  • use_count 返回引用计数的个数;
  • unique 返回是否是独占所有权(use_count 为 1);
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象);
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少;
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的。

示例:

class A
{
public:
	int _a = 10;
	~A()
	{
		cout << "~A()" << endl;
	}
};

void test()
{
	shared_ptr<A> sp(new A);
	shared_ptr<A> sp2(new A);
	shared_ptr<A> sp3(sp2);//ok
	sp3 = sp;//ok
	sp->_a = 100;
	sp2->_a = 1000;
	sp3->_a = 10000;
	cout << sp->_a << endl;
	cout << sp2->_a << endl;
	cout << sp3->_a << endl;
}

运行结果如下:

我们发现申请多少资源就会释放多少资源,此时的sp和sp3共享一份资源,修改sp3也就相等于修改了sp。所以最终都会打印10000。那共享了一份资源,是如何实现资源只释放一次呢?----引用计数

我们可以通过shared_ptr提供的接口use_count()来查看,当前有多少个智能指针来管理同一份资源

void test()
{
	shared_ptr<A> sp(new A);
	cout << sp.use_count() << endl;//1
	shared_ptr<A> sp2(sp);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	shared_ptr<A> sp3(new A);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//1
	sp3 = sp;
	sp3 = sp2;
	cout << sp.use_count() << endl;//3
	cout << sp2.use_count() << endl;//3
	cout << sp3.use_count() << endl;//3
}

运行截图:之所以中间会有调析构函数,是因为当sp3指向sp时,sp3的引用计数为0,则会调用析构函数来释放资源。此时sp创建的资源就有3个指智能指针来管理

图解:

在实现时,我们应该确保一个资源只对应一个计数器,而不是每个智能指针都有各自的计数器。所以我们可以将资源和计数器绑定在一起,此时指向同一份资源的智能指针,访问的也都是同一个计数器(后面会解释)

模拟实现

  • 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
  • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
  • 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
  • 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
  • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
  • 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。
namespace XM
{
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pRefCount(new int(1))
	{}
 
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pRefCount(sp._pRefCount)
	{
		++(*_pRefCount);
	}
 
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			if (--(*_pRefCount) == 0)
			{
				delete _ptr;
				delete _pRefCount;
			}
 
			_ptr = sp._ptr;
			_pRefCount = sp._pRefCount;
			++(*_pRefCount);
		}
 
		return *this;
	}
 
	~shared_ptr()
	{
		if (--(*_pRefCount) == 0 && _ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pRefCount;
 
			//_ptr = nullptr;
			//_pRefCount = nullptr;
		}
	}
    
    int use_count() const
    {
       return *_pRefCount;
    }
 
	// 像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}
 
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pRefCount;
};
}

思考一个问题:为什么引用计数要放在堆区?★

  • ①首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
  • ②其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
  • ③而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
  • ④这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
  • ⑤但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

线程安全问题

我们实现的shared_ptr智能指针在多线程的场景下其实是存在线程安全问题的----引用计数器指针是一个共享变量,多个线程进行修改时会导致计数器混乱。导致资源提前被释放或者会产生内存泄漏问题
我们来看看一下代码

#include<iostream>
#include<memory>
#include<mutex>
#include<thread>
 
using namespace std;
 
 
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
 
 
namespace XM
{
 
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
		{}
 
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
		{
			AddRef();
		}
 
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				Release();
 
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				AddRef();
 
			}
 
			return *this;
		}
 
 
 
		~shared_ptr()
		{
			Release();
		}
 
		T* get() const
		{
			return _ptr;
		}
 
		int use_count()
		{
			return *_pRefCount;
		}
 
		
		T& operator*()
		{
			return *_ptr;
		}
 
		T* operator->()
		{
			return _ptr;
		}
 
	private:
		void Release()
		{
			if (--(*_pRefCount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pRefCount;
			}
		}
 
		void AddRef()  //增加计数
		{
			++(*_pRefCount);
		}
 
	private:
		T* _ptr;
		int* _pRefCount;
	};
}
 
 
 
void SharePtrFunc(XM::shared_ptr<Date>& sp, size_t n,mutex& mtx)
{
	cout << sp.get() << endl;
 
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,自己模拟实现是不安全的
		XM::shared_ptr<Date> copy(sp);
 
        
		{
			unique_lock<mutex> lk(mtx);
			copy->_year++;
			copy->_month++;
			copy->_day++;
		}
 
	}
}
 
int main()
{
	XM::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 10000;
	mutex mtx;
 
	thread t1(SharePtrFunc, std::ref(p), n,std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n,std::ref(mtx));
 
	t1.join();
	t2.join();
 
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
 
	cout << p.use_count() << endl;
 
	return 0;
}
  • ①通过实验结果可知,如果share_ptr不加锁在多线程的情况下是不安全的,在pRefCount ++,- - 时 可能出现错误
  • ②智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  • ③智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
  • ④这里智能指针访问管理的资源,不是线程安全的;对Date的成员 ++ , 所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n ; 为了保证线程安全还要手动加锁

shared_ptr智能指针是线程安全的吗?

  • 是的,引用计数的加减是加锁保护的。但是指向的资源不是线程安全的,需要自己管
  • 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了; 引用计数的线程安全问题,是智能指针要处理的

模拟线程安全的代码 , 引用计数加锁
①要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成Release函数,这样就只需要对AddRef和Release函数进行加锁保护即可。
namespace XM
{
 
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pRefCount(new int(1))
			,_pmtx(new mutex)
		{}
 
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pRefCount(sp._pRefCount)
			,_pmtx(sp._pmtx)
		{
			AddRef();
		}
 
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp) 这样判断不太好,防止自己给自己赋值应该判断指针的值是否相同
			if (_ptr != sp._ptr)
			{
				Release();
 
				_ptr = sp._ptr;
				_pRefCount = sp._pRefCount;
				_pmtx = sp._pmtx;
				AddRef();
 
			}
 
			return *this;
		}
 
 
 
		~shared_ptr()
		{
			Release();
		}
 
		T* get() const
		{
			return _ptr;
		}
 
		int use_count()
		{
			return *_pRefCount;
		}
 
 
		T& operator*()
		{
			return *_ptr;
		}
 
		T* operator->()
		{
			return _ptr;
		}
 
	private:
		void Release() //释放资源
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_pRefCount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pRefCount;
 
				flag = true;  //锁不能在这里释放,因为后面要解锁
			}
			_pmtx->unlock();
 
			if (flag == true)
			{
				delete _pmtx;
			}
		}
 
		void AddRef()  //增加计数
		{
			_pmtx->lock();
 
			++(*_pRefCount);
 
			_pmtx->unlock();
		}
 
	private:
		T* _ptr;
		int* _pRefCount;
		mutex* _pmtx;
	};
}

小结:

  • 在Release函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源。
  • shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证

循环引用

shared_ptr其实也存在一些小问题,也就是循环引用问题

#include<iostream>
#include<memory>
#include<string>
using namespace std;
class A;
class B;
class A {
public:
	shared_ptr<B> bptr;
	~A()
	{
		cout << "class Ta is disstruct" << endl;
	}
};
class B {
public:
	shared_ptr<A>aptr;
	~B()
	{
		cout << "class Tb is disstruct" << endl;
	}
};
void testPtr()
{
	shared_ptr<A>ap(new A);
	shared_ptr<B>bp(new B);
	cout << "ap的引用计数" << ap.use_count() << endl;//ap的引用计数1
	cout << "bp的引用计数" << bp.use_count() << endl;//bp的引用计数1
	ap->bptr = bp;
	bp->aptr = ap;
	cout << "ap的引用计数" << ap.use_count() << endl;//ap的引用计数2
 
 
	cout << "bp的引用计数" << bp.use_count() << endl;//bp的引用计数2
}
int main()
{
	testPtr();
	return 0;
}

我们可以用图来理解一下上述程序智能指针引用关系:

共享智能指针ap指向A的实例对象,内存引用计数+1,B的实例对象里面的成员aptr被ap赋值,所以aptr与ap共同指向同一块内存,该内存引用计数变为2;同理指向B对象的也有两个共享智能指针,其引用计数也为2。

当函数结束时,ap,bp两个共享智能指针离开作用域,引用计数均减为1,在这种情况下不会删除智能指针所管理的内存,导致A,B的实例对象不能被析构,最终造成内存泄漏,如图:

循环引用的解决方式 weak_ptr

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造和析构不会引起引用记数的增加或减少。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。但当expired()true的时候,lock()函数将返回一个存储空指针的shared_ptr。
示例:

#define _CRT_SECURE_NO_WARNINGS
#include"bitset.h"
#include<memory>
int main() {
    shared_ptr<int> sh_ptr = make_shared<int>(10);
    cout << sh_ptr.use_count() << endl;//1

    weak_ptr<int> wp(sh_ptr);
    cout << wp.use_count() << endl;//1

    if (!wp.expired()) {
        shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
        *sh_ptr = 100;
        cout << wp.use_count() << endl;//2
    }

//delete memory

	system("pause");
	return EXIT_SUCCESS;
}

定制删除器

关于new和delete的补充

  • 如果A的析构函数没有显示写,这里不会报错也不会有内存泄漏,原因: new底层是用malloc开辟空间,delete底层是free,free不管你开辟多少空间,开多少释放多少空间
  • 如果A的析构函数显示写,这里就会出问题,原因 : new的时候如果有析构函数的情况下,假设一个对象是4字节,10个对象是40个字节,它不会只开40个字节,它还要在头部多开4个字节去存对象的个数,delete的时候,delete[]没有指明delete几个对象,它去头部取那4个字节,发现是10就调用10次析构函数

定制删除器的用法

(1)错误用法

  • 当智能指针对象的生命周期结束时,所有的智能指针默认都是以 delete 的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以 new 方式申请到的内存空间,智能指针管理的也可能是以 new[ ] 的方式申请到的空间,或管理的是一个文件指针
  • 这时当智能指针对象的生命周期结束时,再以 delete 的方式释放管理的资源就会导致程序崩溃,因为以 new[ ] 的方式申请到的内存空间必须以 delete[ ] 的方式进行释放,而文件指针必须通过调用 fclose 函数进行释放
struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
 
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10]);   //error
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error
 
	return 0;
}

(2)正确用法

我们来看 C++ 是如何解决的

unique_ptr类模板原型:

//non-specialized	
template <class T, class D = default_delete<T>>
class unique_ptr;
//array specialization	
template <class T, class D>
class unique_ptr<T[],D>;

可以看到,这里提供了一个模板参数 class D = default_delete<T> ,这就是删除器,它支持传入仿函数类型,可以由我们自己定制。

shared_ptr类模板原型:

template <class U, class D>
class unique_ptr<U* p ,D del>;

①参数

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

②当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入

③因此当智能指针管理的资源不是以 new 的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器

template<class T>
struct DelArr
{
	void operator()(const T* ptr)
	{
		cout << "delete[]: " << ptr << endl;
		delete[] ptr;
	}
};
 
int main()
{
	std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>()); //仿函数
	std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
		cout << "fclose: " << ptr << endl;
		fclose(ptr);
	}); //lamba表达式
 
	return 0;
}

小结

  • 定制删除器,实际在平时的工作中使用有价值
  • 定制删除器的意义 : 默认情况,智能指针底层都是delete资源 ,那么如果你的资源不是new出来的呢?比如:new[]、malloc、fopen ,定制删除器 -- 传入可调用对象,自定义释放资源
posted @ 2023-01-03 17:03  一只少年AAA  阅读(575)  评论(2编辑  收藏  举报