c++智能指针

本文详细介绍了RAII思想,以及如何通过智能指针(如unique_ptr和shared_ptr)实现资源的自动管理和避免内存泄露。重点讲解了unique_ptr的无复制特性、shared_ptr的引用计数机制及其在循环引用中的解决方案。同时提到了weak_ptr的作用和定制删除器的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

智能指针,可以做到离开作用域自动销毁,不用我们手动去调用析构函数

1.RAII思想

1.1RAII思想介绍

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

这种做 法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。

1.2一份简单的RAII思想类型的代码: 

template<class T>
class myRAII {
public:
	myRAII(T* ptr=nullptr)://不能使用const--会导致权限放大
		_ptr(ptr)
	{}
	~myRAII()
	{
		if(_ptr){
			delete _ptr;
		}
	}
private:
	T* _ptr;
};

借助了类实例离开作用域时,会自动调用析构函数的特性。所以就可以有效解决因为忘记释放资源而造成内存泄露。

仅仅只有这个,我们还是无法使用的,因此我们需要它能提供接口,为我们提供访问资源的接口函数,使其能像使用指针一样使用 。

权限放大问题 --只针对指针来说的

2.smartptr

2.1智能指针原理:

这里只提供基础的原理,代码内仍存在许多问题需要我们去解决

代码:

template<class T>
class smartptr {
public:
	smartptr(T*ptr):
		_ptr(ptr)
	{}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	~smartptr()
	{
		
		delete _ptr;
		
	}
private:
	T* _ptr;
};

提供了两个接口:* 与 -> ,能让我们访问到其中的资源。

class Data {
public:
	int _year=1;
	int _month1;
	int _day=1;
};
int main() {
	Auto_ptr<Data> ap(new Data);
	ap->_day;//这里编译器给我们进行优化了
	return 0;
}

2.2在使用时的编译器的优化:

 Auto_ptr<Data> ap(new Data);
    ap->_day;//这里编译器给我们进行优化了

优化:
    ap->  这一步的操作是获取到Data*的地址,需要再次通过Data* ->去访问Data内的元素

        原本需要ap->->day才能获取到_day,但是这样写不便于阅读,所以编译器会进行优化。
  

3.auto_ptr

3.1拷贝问题--移交管理权

假如遇到拷贝会导致两个指针指向同一片资源,在释放的时候,如果不加以控制,那么在释放资源时,一个先释放,另一个再去释放时,就会访问到野指针,发生错误。

所以为了防止这种错误,我们可以采取策略:移交管理权

体现在代码上为: 

template<class T>
class smartptr {
public:
	smartptr(T*ptr):
		_ptr(ptr)
	{}
	smartptr(smartptr<T>&ap):
		_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	~smartptr()
	{
		if (_ptr) {
			delete _ptr;
		}
	}
private:
	T* _ptr;
};

3.2赋值导致的内存泄漏

赋值存在一种情况,那么就是这两个auto_ptr不能是同一份对象,如果是同一份对象的情况不存在(管理权转移)。

如下图中的情况

因为这两个auto_ptr<int>类型一样,所以允许赋值操作

所以我们还需要处理一下赋值重载问题。

将自己指向其他的资源,并将原来的资源释放掉

Auto_ptr& operator=(Auto_ptr<T>& ap) {
		//检查是否自己给自己赋值
		if (this!=&ap) {
			delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
template<class T>
class Auto_ptr {
public:
	Auto_ptr(T*ptr=nullptr):
		_ptr(ptr)
	{}
	Auto_ptr(Auto_ptr<T>&ap):
		_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	Auto_ptr& operator=(Auto_ptr<T>& ap) {
		//检查是否自己给自己赋值
		if (this!=&ap) {
			delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	~Auto_ptr()
	{
		if (_ptr) {
			delete _ptr;
		}
	}
private:
	T* _ptr;
};
class Data {
public:
	int _year=1;
	int _month1;
	int _day=1;
};
int main() {
	Auto_ptr<Data> ap(new Data);
	Auto_ptr<Data> cp;
	cp = ap;
	return 0;
}

3.2遗留问题

通过移交管理权,我们可以把之前的智能指针悬空,但这又带来了另一个问题,但是用的人并不知道它已经悬空了。因此不推荐使用auto_ptr 。

4.unique_ptr

unique_ptr的解决方法很简单,既然你的拷贝会出现问题,那么你就不要拷贝了,不拷贝就不会出现问题。所以unique_ptr用在不需要拷贝的场景下。

template<class T>
class unique_ptr {
public:
	unique_ptr(T* ptr):
		_ptr(ptr)
	{
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	~unique_ptr()
	{
		delete _ptr;
	}
private:
	unique_ptr(unique_ptr* ptr) = delete;
	unique_ptr& operator=(unique_ptr& p) = delete;
	T* _ptr;
};

5.shared_ptr

5.1原理

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

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享

2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。

3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;

4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。

template<class T>
class shared_ptr {
public:
	shared_ptr(T* ptr):
		_ptr(ptr),
		_count(new size_t(1))
	{

	}
	shared_ptr(const shared_ptr& sp):
		_ptr(sp._ptr),
		_count(sp._count)
	{
		_count++;
	}
	shared_ptr& operator=(const shared_ptr&sp) {
		if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
			//不是同一个资源  this = sp。
			//如果本资源引用计数为0,就释放资源
			if (--(*count) == 0) {
				delete _ptr;
				delete _count;
			}
		}
		_ptr = sp._count;
		_count = sp._count;
		*_count++;
        return *this;
	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
	~shared_ptr()
	{
		if (--(*count)== 0) {
			delete _ptr;
			_ptr = nullptr;
			delete _count;
			_count = nullptr;
		}
	}
private:
	T* _ptr;
	size_t* _count;//必须为指针类型,且不能使用static修饰
};

size_t *count 必须是指针类型,而且不能为static修饰

首先如果不是指针而是局部变量,那么这个引用计数不会被修改

其次,如果为 static修饰,就会让整个class类共享同一个计数器,那么现在有两份空间

我们分别用两个shard_ptr指向不同的空间,那么这个计数器就会变为2,不符合我们的要求,我们应该要求一份资源一个计数器。

所以应该使用指针,并且拷贝时,不要忘记指针的拷贝

5.2赋值重载

其中主要注意赋值重载--- 

针对第二种情况,需要先判断这两个share是不是指向同一个空间,如果不是的话,就更改被赋值的引用计数,并且判断是否需要清理掉该资源。 

再让它指向sp所指向的对象,且让sp的count引用计数+1

	shared_ptr& operator=(const shared_ptr&sp) {
		if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
			//不是同一个资源  this = sp。
			//如果本资源引用计数为0,就释放资源
			if (--(*count) == 0) {
				delete _ptr;
				delete _count;
			}
    		_ptr = sp._count;
		    _count = sp._count;
		    *_count++;
		}

	}

5.3线程安全问题 

shared_ptr需要加锁来保证自身的线程安全问题。

但是它内部管理的对象不一定是线程安全的,所以要想保证内部也是线程安全的,则需要另一把锁,将管理对象加锁。

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

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

template<class T>
class shared_ptr_ {
public:
	shared_ptr_(T* ptr):
		_ptr(ptr),
		_count(new size_t(1)),
		_cmux(new mutex)
	{
		cout << "shared->" << _ptr << endl;
	}
	shared_ptr_(const shared_ptr_<T>& sp):
		_ptr(sp._ptr),
		_count(sp._count),
		_cmux(sp._cmux)
	{
		countAdd();
	}
	shared_ptr_& operator=(const shared_ptr_<T>&sp) {
		if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
			//不是同一个资源  this = sp。
			//如果本资源引用计数为0,就释放资源
			Destory();
			_ptr = sp._ptr;
			_count = sp._count;
			_cmux = sp._cmux;
			countAdd();
		}
		return *this;

	}
	T& operator*() {
		return *_ptr;
	}
	T* operator->() {
		return _ptr;
	}
    T* get() const{
        return _ptr;
    }
	~shared_ptr_()
	{
		Destory();
	}
	void countAdd() {
		_cmux->lock();
		(*_count)++;
		_cmux->unlock();
	}
	void Destory() {
		_cmux->lock();
		if (--(*_count) == 0) {
			delete _ptr;
			delete _count;
			//delete _cmux;
			cout << "~shared->" << _ptr << endl;
		}
		_cmux->unlock();
	}
private:
	T* _ptr;
	size_t* _count;//必须为指针类型,且不能使用const修饰
	mutex *_cmux;
};

5.4循环引用问题

//循环引用
struct ListNode {
	shared_ptr_<ListNode> _next;
	shared_ptr_<ListNode> _prev;
};
int main() {
	shared_ptr_<ListNode> node1(new ListNode);
	shared_ptr_<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev=node1;
	return 0;
}

 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属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev 属于node2成员,所以这就叫循环引用,谁也不会释放。

 造成这个原因是因为引用计数无法被释放。

所以解决办法也很简单,既然是引用计数问题,那么我们就创建一个辅助的类,来帮助我们解决引用计数问题。

weak_ptr

1.不想常规的智能指针,不支持RAII

2.支持像指针一样

3.专门设计出来,辅助解决shared_ptr循环引用问题

template<class T>
class weak_ptr_ {
public:
    weak_ptr_():
        _spt(nullptr)
    {

    }
	weak_ptr_(shared_ptr_<T>* spt) :
		_spt(spt->get())
	{}
	T& operator*() {
		return *_spt;
	}
	T* operator->() {
		return _spt;
	}
private:
	T *_spt;
};

5.5 定制删除器

本文的所有智能指针都还不健全,假如我们使用shared_ptr sp(new int(10));又或者shared_ptr fp(fopen(""))...我们会发现在析构时,如果使用delete 释放资源,会出错。因此需要我们传递一个删除器以供它进行删除。在此我们就针对shared_ptr来进行修改。

	template<class Del>
	shared_ptr_(T* ptr = nullptr,Del del=_del) :
		_ptr(ptr),
		_count(new size_t(1)),
		_cmux(new mutex),
		_del(del)
	{
		cout << "shared->" << _ptr << endl;
	}

private:
    function<void(T* ptr)> _del = [](T* ptr) {
		delete ptr;
	};

int main() {
	shared_ptr_<int> sp(new int[10], [](int *ptr) {
		delete []ptr;
		});
}

在类内使用函数模板,(自动推导),我们在初始化时,给出我们自己的想要的删除方式。

auto_ptr在进行赋值重载时,会让原来的智能指针悬空

unque_ptr 不允许拷贝,解决了auto_ptr的缺点

shared_ptr 采用引用计数的方式,允许拷贝,但是会出现循环引用的问题,所以就需要引入新的方式来解决循环引用,即使用weak_ptr去辅助shared_ptr。另外shared_ptr需要注意本身的线程安全问题,需要加锁修改引用计数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蠢 愚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值