[c++]——智能指针

智能指针

智能指针听名字就知道他应该是一个指针或者类似指针的东西,那么他到底是干什么的?让我们一起来看看

1.为什么需要智能指针?

    我们在谈异常的时候,有下面这么一段代码,这段代码中有一个异常重新抛出的动作,这里我们说过其实是为了防止没有释放array的空间造成内存泄漏。

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";//此处抛出异常
	}
	return (double)a / (double)b;
}
void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
	// 重新抛出去。
	int* array = new int[10];
	try {
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)//捕获异常处理释放问题,重新抛出
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}	
	delete[] array;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)//捕获第二次抛出的异常
	{
		cout << errmsg << endl;
	}	
	return 0;
}

    这里截获异常,处理内存等问题后我们在将异常重新抛出,其实这样的处理的方式对我们维护代码造成了很大的不便,而且一不小心就会造成内存泄漏,所以我们就引出了一个新名词智能指针,那么智能指针是怎么处理这个问题的呢?

    我们只看Func函数中的逻辑,这里我们发现我们使用了一个对象来管理我们new出来的空间,这块空间会随着对象的生命周期结束被释放掉。

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

void Func()
{
	int* array = new int[10];
	try {
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}	
	SmartPtr<int> p(array);
}

2.智能指针的使用以及原理

2.1RAII

RAII(Resource Acquisition Is Initialization -> 资源获得即初始化),是一种利用对象生命周期来控制程序资源的简单技术。
在对象构造时获得资源,接着控制对资源的访问在对象的生命周期内始终有效,最后在对象析构时释放资源。这样的好处在于:

  • 不需要显示的释放资源
  • 对象所管理的资源在其生命周期内始终有效

2.2智能指针的原理

我们知道普通的指针还需要支持 -> *等操作,所以智能指针为了模仿普通指针的行为我们需要重载他的 ->等符号

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	T& operator*()//重载*模仿普通指针的行为
	{
		return *_ptr;
	}

	T* operator->()//如果我们的初始化了结构体指针,我们还需要用重载->来访问他的成员
	{
		return _ptr;
	}

	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
int main()
{
	int* p = new int;
	SmartPtr<int> sp(p);
	*sp = 10;
	cout << *sp << endl;
	
	system("pause");
	return 0;
}

3.c++库中的智能指针

3.1auto_ptr

    c++库中智能指针都在memory的头文件中,这里我们一起来谈一谈不同的智能指针,首先来看看最原始的智能指针,auto_ptr

3.1.1auto_ptr的问题

    这里是一段auto_ptr使用的代码,运行结果奔溃,之所以程序奔溃是因为在ap被copy拷贝后成了空指针,解引用他导致奔溃,这里导致奔溃的行为叫做转移管理权,为什么会这样接着看模拟实现部分

int main()
{
	int* p = new int;
	auto_ptr<int> ap(p);
	auto_ptr<int> copy(ap);
	*ap = 10;//程序奔溃

	system("pause");
	return 0;
}

3.1.2auto_ptr的模拟实现

    auto_ptr的实现如下,我们上面说过,auto_ptr使用了一种管理权转移的方式来实现,所以当你进行拷贝构造之后原指针已经被置空

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<T>& operator=(Auto_Ptr<T>& ap)
	{
		if (this = &ap)//检查是否自己给自己赋值
		{
			if (_ptr)
				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;
};
  • 这里为什么不使用默认的构造函数?因为如果默认的拷贝构造将会是一个浅拷贝,俩个指针指向的同一块空间会被释放俩次
  • 那为什么不使用深拷贝呢?因为一个指针给另一个指针赋值实际意义就是为了让他们指向同一块空间,如果重新开一块空间让被赋值的指针指向它那么就完全违背了指针赋值的意义

3.2unique_ptr

3.2.1unique_ptr的问题

    上面我们研究了auto_ptr的问题,auto_ptr事实上最大的问题就在于他的拷贝,所以unique_ptr顶对拷贝构造的问题做了一件非常简单粗暴的事情,就是将拷贝构造和赋值函数都删除,这里unique_ptr智能指针的行为叫做防拷贝

Unique_Ptr(Unique_Ptr<T>& ap) = delete;
Unique_Ptr<T>& operator=(Unique_Ptr<T>& ap) = delete;

unique_ptr就如同他的名字一样,他被定义出来是唯一的,他不能发生被拷贝和赋值的行为,这里确实解决了指针悬空的问题,但是这里只算是一种回避问题的解决方式,所以从根本上来说我们还是没有解决拷贝构造的问题
在这里插入图片描述

3.2.2unique_ptr的模拟实现

这里仅仅在auto_ptr的基础上将拷贝赋值函数设定为delete函数,有的同学说将拷贝赋值函数只声名不定义并且设置为私有的行不行,其实理论上来说没问题,但是不要忘记c++的友元,他会突破类的封装如果在类外定义那就得不偿失了。

template<class T>
class Unique_Ptr {
public:
	Unique_Ptr(T* ptr = nullptr)
		: _ptr(ptr)
	{}

	Unique_Ptr(Unique_Ptr<T>& ap) = delete;
	Unique_Ptr<T>& operator=(Unique_Ptr<T>& ap) = delete;

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

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

	~Unique_Ptr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

3.3靠谱的shared_ptr

上面我们已经谈了俩种实现方式的智能指针,但是他们都有大大小小的缺陷,所以c++11中给出了一种叫shared_ptr的智能指针来弥补上面指针指针存在的缺陷

3.3.1shared_ptr解决拷贝问题

为了解决拷贝的问题,并且希望这里就是完成浅拷贝,所以我们设定了一个计数,每增加一个指针指向同一块空间,这块空间对应的计数就++,每释放一个指针就 - -计数,直到最后一个指针释放的同时释放这块空间
在这里插入图片描述

3.3.2怎么设置计数?

这样?

class Shared_Ptr//A
 {
private:
	T* _ptr;
	int _count;
};

这样?

class Shared_Ptr//B
 {
private:
	T* _ptr;
	static int _count;
};

还是这样?

class Shared_Ptr//C
 {
private:
	T* _ptr;
	int* _count;
};

我们来分析一下:A中的计数相当于存在每个对象中,好像并不能达到多个对象一个计数的要求,那B好像看起来是可以的,因为所有的对象都只有一个计数,但是。。

这样我们计数只有一个,违背了我们下图中希望一块空间一个计数的原则
在这里插入图片描述
所以C是最合适的,让每一个计数指针指向一个计数,相同类型的对象被拷贝也就不在需要新的指针。

3.3.3shared_ptr的模拟实现

析构函数:当每个对象中的计数被减到0的时候释放计数的指针

~Shared_Ptr()
	{
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
		}
	}

拷贝构造函数:每被拷贝一次就 ++计数

Shared_Ptr(Shared_Ptr<T>& ap)
		:_ptr(ap._ptr)
		, _count(ap._count)
	{
		++(*_count);
	}

赋值重载:1.判断是否给自己赋值 2.如果当前对象已经是最后一个指向目标空间的指针,释放资源 3.赋值给当前对象并且++计数

Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap)
	{
		if (_ptr != ap._ptr)
		{
			if (--(*_count) == 0)
			{
				delete _ptr;
				delete _count;
			}
			_ptr = ap._ptr;
			_count = ap._count;
			++(*_count);
		}
		return *this;
	}

简单的实现了上面的函数后我们的shared_ptr就算是完成了,但是实际上还是存在问题

如果在多线程中,我们发现 ++ 和 - - 操作不是原子的,经过测试,我们发现执行20000次(次数变大就会出错)后计数的大小并不是20000,这个值小于20000,根本原因就是不是原子的++,所以我们需要对++ - - 操作进行加锁,所以以下是最终版本

template<class T>
class Shared_Ptr {
public:
	Shared_Ptr(T* ptr = nullptr)
		: _ptr(ptr)
		, _count(new int(1))
		, _pmtx(new mutex)
	{}

	Shared_Ptr(Shared_Ptr<T>& ap)
		:_ptr(ap._ptr)
		, _count(ap._count)
		, _pmtx(ap._pmtx)
	{
		Add();
	}

	void Add()//加锁的++
	{
		_pmtx->lock();
		++(*_count);
		_pmtx->unlock();
	}

	void Release()//加锁的--
	{
		bool flag = false;
		_pmtx->lock();
		if (--(*_count) == 0)
		{
			delete _ptr;
			delete _count;
			flag = true;//判断是否释放锁
		}
		_pmtx->unlock();
		if (flag == true)
			delete _pmtx;
	}

	Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap)
	{
		if (_ptr != ap._ptr)
		{
			Release();
			_ptr = ap._ptr;
			_count = ap._count;
			Add();
		}
		return *this;
	}

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

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

	~Shared_Ptr()
	{
		Release();
	}
private:
	T* _ptr;
	int* _count;
	mutex* _pmtx;
};

3.3.4shared_ptr的问题

     上面我们已经说了shared_ptr的模拟实现,到目前为止我们好像只发现了他加加计数线程不安全的问题,但是进过我们的修改shared_ptr好像已经没什么问题了,但是记住这里只是解决了加加计数的线程安全的问题,如果我们管理int对象的指针解引用并且加加这个对象,如果在加加次数很多并且不加锁的情况下还是会有问题(加n次结果小于n),除了这个问题,shared_ptr看起来非常完美,但是事实上是这样的么?一起来看看

一段没有问题的代码,但是希望cur next能被智能指针所管理

struct ListNode//一段没有问题的代码
{
	int _data;
	ListNode* _next;
	ListNode* _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	ListNode* cur = new ListNode;
	ListNode* next = new ListNode;
	cur->_next = next;
	next->_prev = cur;
	return 0;
}

于是出现了下面的代码:但是报错了,因为节点中的指针不是智能指针类型

struct ListNode
{
	int _data;
	ListNode* _next;
	ListNode* _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> cur(new ListNode);
	shared_ptr<ListNode> next(new ListNode);
	cur->_next = next;//报错
	next->_prev = cur;//报错

	system("pause");
	return 0;
}

在这里插入图片描述
那好吧,我们修改节点中的指针:现在编过了,可是一运行,发现没有调用节点的析构函数

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

观察引用计数:好像也没有什么问题,但是我们通过调试发现问题出在了这
在这里插入图片描述
如下图:

  • 当cur生命周期结束时被释放,next也是同理,但是他们释放后计数为一
  • 也就是说_next析构了,node2就释放了。
  • 也就是说_prev析构了,node1就释放了。
  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2
    成员,所以这就叫循环引用,谁也不会释放
    在这里插入图片描述
    所以为了解决这个问题,我们引出了第四种指针weak_ptr,这种指针就是为了解决shared_ptr循环引用的问题的,可以说是shared_ptr的产物,将节点中的指针改成他就解决了问题,他的原理就是不让引用计数加加,所以我们不难理解解决问题的原理
    在这里插入图片描述
    weak_ptr是用来解决循环引用的问题的,所以他不能管理其他类型的指针,他只能管理shared_ptr类型和自己本身类型。

小结

  • auto_ptr:管理权转移行为,一种带有缺陷的早期设计,一般严禁使用
  • unique_ptr:简单的粗暴的防拷贝行为,效率高,但是功能不全,一般鼓励使用
  • shared_ptr:功能全,支持拷贝,利用了引用计数的计数,但是设计复杂,要考虑线程安全,循环引用需要weak_ptr解决

4.定制删除器

上面我们已经讲过了智能指针的问题和实现,但是我们在使用他们时依旧会存在一些问题:

下面这段代码会导致程序奔溃,因为shared_ptr在释放时是使用delete释放的,并且不仅仅在当前情况下,还有malloc出来的对象,或者是打开一个文件,需要关闭文件

std::shared_ptr<AA> sp1(new AA[10]);

所以我们需要学习一个新的东西,叫定制删除器,前面我们讲过仿函数,这里就使用的是仿函数的原理,重载operator(),并且将此类型作为智能指针的第二个参数就可以帮我们完成删除操作。

class AA
{
private:
	int a;
	int b;
};

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

int main()
{
	DeleteArrayFunc<AA> fun;
	std::shared_ptr<AA> sp1(new AA[10],fun);
	return 0;
}

4.1定制删除器完成文件的关闭

对管理文件的指针进行关闭:

struct Fclose
{
	void operator()(FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
		ptr = nullptr;
	}
};

int main()
{
	Fclose fun;
	std::shared_ptr<FILE> sp1(fopen("test.txt","w"),fun);
	return 0;
}

5.RAII拓展

智能指针其实可以说是c++中独特的东西,因为他没有垃圾回收的机制,但是RAII这种思想在所有的语言中都是有用的,来看下面的代码:

这样的情况下就造成了死锁的问题

int main()
{
	mutex mtx;
	mtx.lock();

	//抛异常

	mtx.unlock();
	system("pause");
	return 0;
}

所以既然智能指针能帮我们回收资源,那么我们就可以模仿智能指针的行为,来管理有关锁等一系列的问题:
c++库中也有uniquelock,他也是防拷贝的。

template<class Lock>
class UniqueLock
{
public:
	UniqueLock(Lock& lock)
		:_lock(lock)
	{
		_lock.lock();
	}

	~UniqueLock()
	{
		_lock.unlock();
	}
private:
	Lock& _lock;
};
int main()
{
	mutex mtx;
	UniqueLock<mutex> lock(mtx);

	system("pause");
	return 0;
}

总结

  1. 理解为什么需要智能指针?重点注意智能指针是一种预防型的内存泄漏的解决方案。智能指针在C++没
    有垃圾回收器环境下,可以很好的解决异常安全等带来的内存泄漏问题,
  2. 理解RAII.
  3. 理解auto_ptr、unique_ptr、shared_ptr等智能指针的使用及原理
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值