关于智能指针,你想要的都在这里


智能指针介绍

什么是智能指针,智能指针相比于传统的裸指针而言,其智能之处在哪里?关于这个问题的解释,首先我们来看一下传统的裸指针在使用的时候常常会 出现的几种问题:

1、通过malloc/new开辟的内存,在函数执行到某处的时候return掉了,以至于没有执行相应的free/delete语句,造成了内存泄漏;

2、通过malloc/new开辟的内存,忘记写free/delete语句,导致内存泄漏;

3、对同一个资源多次释放,导致释放野指针,程序发生崩溃错误;

智能指针的智能之处就在于写程序的人不用关心资源的释放,利用了栈上的对象出作用域自动析构的特性,自动释放指针所指向的资源,避免内存泄漏。关于他的底层实现其实就是对裸指针的封装,提供了运算符重载函数而言。下面我们将实现一个智能指针。

实现不带引用计数的智能指针

#include<iostream>
using namespace std;

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

int main() {
	Smart<int> p1(new int);
	Smart<int> p2(p1);
	return 0;
}

 可以看到,正如上述所说,智能指针实际上就是将裸指针进行了一次面向对象的封装,利用了栈上对象出作用域自动析构的特点实现其智能之处。分析一下main函数的执行结果是什么,没错程序最终会崩溃,原因是p1和p2都指向同一块资源,在对象出作用域析构的时候,一块资源被释放两次,发生了浅拷贝问题。下面我们来看看C++库里所提供的智能指针是如何解决这个问题的?

不带引用计数的智能指针

不带引用计数的智能指针主要有auto_ptr,scoped_ptr,unique_ptr关于上述问题,我们将一个一个进行分析:

<1>auto_ptr,首先我们看一下auto_ptr的底层实现:

 auto_ptr(auto_ptr<_Other>& _Right) noexcept
        : _Myptr(_Right.release()) { // construct by assuming pointer from _Right

在其构造函数下,调用了release方法,再来看一下release方法的具体实现是什么?

_Ty* release() noexcept { // return wrapped pointer and give up ownership
        _Ty* _Tmp = _Myptr;
        _Myptr    = nullptr;
        return _Tmp;
    }

看到这里我们明白了auto_ptr是如何解决浅拷贝问题了,它实际上是进行了资源的转移,简单来说,就是只有最后一个对象拥有该资源,前面的对象都不在拥有(将自己置nullptr)。所以一定不能对进行资源转移过的对象进行操作

说到这里,试问auto_ptr能在容器中使用吗?答案肯定是不行的,当容器之间进行拷贝构造时,相当于容器的元素进行拷贝构造,那么原来容器中的元素都被置nullptr,再次访问会出现程序崩溃错误,所以切记容器中不可使用auto_ptr。

<2>scoped_ptr,一样我们来看一下其底层实现:

template<class T> class scoped_ptr // noncopyable
{
private:
    T * px;
	
	/*
	私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
	对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
	*/
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);

};

 scoped_ptr直接将拷贝构造函数和赋值运算符重载函数私有化,从根本上杜绝了浅拷贝的发生,对于容器而言,scoped_ptr也是不能使用在其中的,原因就在于当容器之间进行拷贝构造时,其元素之间之间也会进行拷贝构造,由于将拷贝构造函数私有化,因此会发生编译错误。

<3>unique_ptr,首先观察一下其底层实现

template <class _Ty,class _Dx> // = default_delete<_Ty>
class unique_ptr { 
public:
    //提供右值引用的拷贝构造函数
    unique_ptr(unique_ptr&& _Right) noexcept
        : _Mypair(_One_then_variadic_args_t(), _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {}
    //提供右值引用的赋值运算符重载
    unique_ptr& operator=(unique_ptr&& _Right) noexcept {
        if (this != _STD addressof(_Right)) {
            reset(_Right.release());
            _Mypair._Get_first() = _STD forward<_Dx>(_Right._Mypair._Get_first());
        }
        return *this;
    }
    //删除左值引用的拷贝构造函数和赋值运算符重载函数
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

private:
    template <class, class>
    friend class unique_ptr;
    _Compressed_pair<_Dx, pointer> _Mypair;
};

从上面看到,unique_ptr有一点和scoped_ptr做的一样,就是去掉了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生

但是unique_ptr提供了带右值引用参数的拷贝构造和赋值,也就是说,unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时进行拷贝构造和赋值操作。

带引用计数的智能指针shared_ptr、weak_ptr

 什么是带引用计数的智能指针,通俗来说当允许多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针的析构函数来处理资源的释放问题,这就是引用计数的概念。

要对资源的引用个数进行计数,我们知道i++,i--操作在多线程环境下不是线程安全的操作,带引用计数的的智能指针底层的引用计数已经通过了CAS操作保证了其原子性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针

实现带引用计数智能指针

template<typename T>
class Share_ptr {
public:
	//构造函数
	Share_ptr(T *ptr = T()) :_ptr(ptr) {
		if (_ptr != nullptr) {
			_cnt = new int(1);
		}
		else {
			_cnt = new int(0);
		}
	}
    //*号运算符重载函数
	T& operator*() {
		if (this->_ptr) {
			return *this->_ptr;
		}
	}

	T* operator->() {
		if (this->_ptr) {
			return this->_ptr;
		}
	}
	const T& operator*()const {
		if (this->_ptr) {
			return *this->_ptr;
		}
	}
	const T* operator->() const{
		if (this->_ptr) {
			return this->_ptr;
		}
	}
	//拷贝构造函数
	Share_ptr(const Share_ptr& src) {
		if (this != &src) {
			this->_ptr = src._ptr;
			this->_cnt = src._cnt;
			(*this->_cnt)++;
		}
	}
	//赋值运算符重载函数
	Share_ptr& operator=(const Share_ptr& src) {
		if (this->_ptr == src._ptr) {
			return *this;
		}
		if (this->_ptr) {
			(*this->_cnt)--;
			if ((*this->_cnt) == 0) {
				delete this->_ptr;
				this->_ptr = nullptr;
				delete this->_cnt;
				this->_cnt = nullptr;
			}
		}
		this->_ptr = src._ptr;
		this->_cnt = src._cnt;
		(*this->_cnt)++;
		return *this;
	}
	//析构函数
	~Share_ptr() {
		(*this->_cnt)--;
		if ((*this->_cnt) == 0) {
			delete _ptr;
			_ptr = nullptr;
			delete _cnt;
			_cnt = nullptr;
		}
	}
    //获得引用计数
	int getCnt() {
		return (*this->_cnt);
	}
private:
	T* _ptr;
	int* _cnt;
};

int main() {
	Share_ptr<int> ptr(new int());
	cout << ptr.getCnt() << endl;
	Share_ptr<int> ptr1(ptr);
	cout << ptr1.getCnt() << endl;
	Share_ptr<int> ptr2;
	ptr2 = ptr;
	cout << ptr2.getCnt() << endl;
	return 0;

智能指针的交叉引用(循环引用)问题

什么是指针指针的交叉引用问题:首先列出一下代码:

#include<iostream>
#include<memory>
using namespace std;

class B;
class A {
public:
	A() {cout << "constructA!" << endl;}
	~A() {cout << "deleteA" << endl;}
	shared_ptr<B> _ptrb;
};
class B{
public:
	B() { cout << "constructB!" << endl; }
	~B() { cout << "deleteB" << endl; }
	shared_ptr<A> _ptra;
};

int main(){
	shared_ptr<A> _ptr1(new A);
	shared_ptr<B> _ptr2(new B);

	_ptr1->_ptrb = _ptr2;
	_ptr2->_ptra = _ptr1;

	cout << _ptr1.use_count() << endl;
	cout << _ptr2.use_count() << endl;
	return 0;
}

观察其结果:

可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,如图所示:

那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr

弱智能指针weak_ptr区别于shared_ptr之处在于

  1. weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
  2. weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  3. weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源

 对上述代码进行修改:

#include<iostream>
#include<memory>
using namespace std;

class B;
class A {
public:
	A() {cout << "constructA!" << endl;}
	~A() {cout << "deleteA" << endl;}
	weak_ptr<B> _ptrb;
};
class B{
public:
	B() { cout << "constructB!" << endl; }
	~B() { cout << "deleteB" << endl; }
	weak_ptr<A> _ptra;
};

int main(){
	shared_ptr<A> _ptr1(new A);
	shared_ptr<B> _ptr2(new B);

	_ptr1->_ptrb = _ptr2;
	_ptr2->_ptra = _ptr1;

	cout << _ptr1.use_count() << endl;
	cout << _ptr2.use_count() << endl;
	return 0;
}

观察其结果:

这下我们发现资源成功进行释放,其原理如下图所示:

多线程访问共享对象问题

什么是多线程访问共享对象问题呢?解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。

代码如下:

#include<iostream>
#include<memory>
#include<thread>
using namespace std;

class test {
public:
	test() {}
	~test() {}
	void func() {cout << "this is good function!" << endl;}
};

void threadworker(test *p) {
	this_thread::sleep_for(std::chrono::seconds(2));
	p->func();
}

int main(){
	test* ptr = new test();
	thread t1(threadworker,ptr);
	delete ptr; //析构这个对象
	t1.join();
	return 0;
}

程序运行结果如下:

 按照正常逻辑而言,子线程睡眠两秒,那么主线程在析构对象后,子线程不应该能访问到该对象的资源,这可能会导致不可预期的错误。可以借助share_ptr和weak_ptr来解决这个问题,代码如下:

#include<iostream>
#include<memory>
#include<thread>
using namespace std;

class test {
public:
	test() {}
	~test() {}
	void func() {cout << "this is good function!" << endl;}
};

void threadworker(weak_ptr<test> p) { //引用处用弱智能指针
	this_thread::sleep_for(std::chrono::seconds(2));
	/*
	如果想访问对象的方法,先通过p的lock方法进行提升操作,把weak_ptr提升为
	shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
	的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
	已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
	*/
	if (shared_ptr<test> ptr =  p.lock()) {
		ptr->func();
	}
}

int main(){
	shared_ptr<test> ptr(new test()); //定义处用强智能指针
	thread t1(threadworker,ptr);
	t1.join();
	return 0;
}

代码可以正常的输出结果,因为主线程调用join方法等待子线程结束,子线程中p调用lock方法提升成功。 

那么思考一下如果将子线程处理为分离线程结果会是怎样呢?注意处理为分离线程,那么主线程先于子线程结束,析构了对象的资源,p调用lock方法提升失败,无法执行成员方法。

自定义删除器

 关于智能智能指针的删除器我自己认为他是一个极其强大的功能。对于正常情况下,我们常常管理的是堆资源,当智能指针出作用域的时候,会调用析构函数释放资源。那么删除一个文件资源,或者希望资源按照我们所希望的方式进行回收等等操作,该如何处理呢?自定义智能指针的删除器就可以实现。

我们来看一下unique_ptr的类声明:

template <class _Ty,
    class _Dx> // = default_delete<_Ty>
//除了传入所开辟资源的类型_ty外,还能传入一个函数对象类型删除删除_ty类型的资源

 再来看一下unique_ptr的析构函数:

 ~unique_ptr() noexcept {
        if (_Mypair._Myval2 != pointer()) {
            _Mypair._Get_first()(_Mypair._Myval2);
        }
    }
    //返回用于析构被管理对象的删除器
    _NODISCARD _Dx& get_deleter() noexcept {
        return _Mypair._Get_first();
    //返回指向被管理对象的指针
    _NODISCARD pointer get() const noexcept {
        return _Mypair._Myval2;
    }

可以看出来实现智能指针的删除器需要定义一个函数对象:

下面看一个删除文件资源的例子:

class FileDeleter
{
public:
	// 删除器负责删除资源的函数
	void operator()(FILE *pf)
	{
		fclose(pf);
	}
};
int main()
{
    // 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
	unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
	return 0;
}

上述方法虽然可行,但是需要创建额外的类,我并不热衷于此做法。function和lambda表达式可以更好的实现了智能指针的删除器。

// 自定义智能指针删除器,关闭文件资源
	unique_ptr<FILE, function<void(FILE*)>> 
		filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值