智能指针

1.原因

智能指针的出现主要是用来解决在实际的开发过程中程序员使用裸指针而导致的一系列问题。当然裸指针本身是安全的,只是会由于开发者的不规范使用而导致出现各类问题:

  • 申请的资源在程序运行结束后忘记释放了。
  • 对申请了的资源做了重复的释放
  • 由于程序的代码逻辑使得程序在中途就直接return退出了,导致没有执行后面的释放操作。
  • 程序运行过程中产生了异常,由于异常栈的展开导致资源释放的代码没有执行。

智能指针的出现就是为了解决上述的这些问题,利用栈上的对象出作用域自动调用析构函数来释放申请的资源。c++11库里面主要提供了两类智能指针,带引用计数的智能指针不带引用计数的智能指针

template<typename T>
class CSmartPtr
{
public:
    // 类构造来管理申请的资源
	CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
    // 自动调用析构函数来释放资源
	~CSmartPtr() { delete mptr; }
private:
	T *mptr;
};

int main()
{
	CSmartPtr<int> ptr(new int);
	/*代码段*/
    // 出了作用域自动析构,及时在中途退出也没有问题
	return 0;
}

智能指针就是把裸指针做了一次面向对象的封装,在构造函数中初始化资源的地址,在析构函数中释放地址对应的资源。智能指针利用的就是栈上的对象出作用域自动调用析构的特点,不能将智能指针定义在堆空间中,因为本质上还是成了一个裸指针。

同时智能指针还需要提供*和->运算符的重载,这样就可以像使用裸指针一样使用智能指针。

template<typename T>
class CSmartPtr
{
public:
	CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
	~CSmartPtr() { delete mptr; }
    
    // 运算符的重载
    // 非const可以通过返回值修改成员变量,也可以直接修改返回值
	T& operator*() { return *mptr; }
    T* operator->() { return mptr; }
    // const即不可以修改返回值,也不可以通过返回值修改成员变量。
	const T& operator*()const { return *mptr; }
	const T* operator->()const { return mptr; }
private:
	T *mptr;
};
int main()
{
	CSmartPtr<int> ptr(new int);
	*ptr = 20;
	cout << *ptr << endl;
	return 0;
}

但是这样的指针指针还存在一个问题:

int main()
{
	CSmartPtr<int> ptr1(new int);
	CSmartPtr<int> ptr2(ptr1);
	return 0;
}

当我们用一个智能指针对象去初始化另一个智能智能对象时,由于做的是浅拷贝,只是对指向的指针做了一次赋值,但是指针指向的内存资源缺并没有做复制,这就导致了两个智能指针都指向同一块资源,这样在析构的时候就会对同一资源析构两次,导致程序报错。

因此智能指针主要解决一下两个问题:

  • 智能指针浅拷贝的问题。
  • 多个智能指针指向同一资源的时候,怎么保证资源只做唯一一次的释放,而不是多次释放。

注:被delete后的指针p的值(地址值)并非就是NULL,而是随机值。也就是被delete后,如果不再加上一句p=NULL,p就成了“野指针”,在内存里乱指一通。同时在delete之前会自动检查p是否为空(NULL),如果为空(NULL)就不再delete了

2.不带引用计数的智能指针

2.1 auto_ptr

auto_ptr的源码如下:

template<class _Ty>
	class auto_ptr
	{	// wrap an object pointer to ensure destruction
public:
	typedef _Ty element_type;
    
    // 构造函数初始化资源的地址
	explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
		: _Myptr(_Ptr)
		{	// construct from object pointer
		}

	/*这里是auto_ptr的拷贝构造函数,
	_Right.release()函数中,把_Right的_Myptr
	赋为nullptr,也就是换成当前auto_ptr持有资源地址
	*/

    // 拷贝构造函数,这里直接是调用的一个内部的release函数
	auto_ptr(auto_ptr& _Right) noexcept
		: _Myptr(_Right.release())
		{	// construct by assuming pointer from _Right auto_ptr
		}
	// 从release函数中可以看出,这是做的就是把原来的指针指向置为空,然后用当前的智能指针对象进行管理
	_Ty * release() noexcept
		{	// return wrapped pointer and give up ownership
		_Ty * _Tmp = _Myptr;
		_Myptr = nullptr;
		return (_Tmp);
		}
private:
	_Ty * _Myptr;	// the wrapped object pointer
};

从上述源码中可以看出,auto_ptr的浅拷贝实现就是把原来的智能指针置空,用当前的智能指针来管理,也就是只有最后一个auto_ptr智能指针持有内存空间资源,之前的都被置为nullptr了

注:auto_ptr不能用在容器中,当做容器的拷贝的时候,会导致原来容器中的元素全部都为nullptr了。

2.2 scoped_ptr

scoped_ptr的源码如下:

template<class T> class scoped_ptr // noncopyable
{
private:
    T * px;
    // 这里直接私有化了拷贝构造函数和赋值构造函数,因为对象不能访问类的私有成员函数,也就直接杜绝了智能指针浅拷贝的发生。
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);
    typedef scoped_ptr<T> this_type;
    // 同时私有化了比较运算符重载函数,使得scoped_ptr也不支持智能智能指针的比较操作
    void operator==( scoped_ptr const& ) const;
    void operator!=( scoped_ptr const& ) const;
public:
    typedef T element_type;
    explicit scoped_ptr( T * p = 0 ): px( p ) // never throws
    {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_constructor_hook( px );
#endif
    }
#ifndef BOOST_NO_AUTO_PTR
	/*支持从auto_ptr构造一个scoped_ptr智能指针对象,
	但是auto_ptr因为调用release()函数,导致其内部指
	针为nullptr*/
    explicit scoped_ptr( std::auto_ptr<T> p ) BOOST_NOEXCEPT : px( p.release() )
    {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_constructor_hook( px );
#endif
    }
#endif
	// 析构函数,释放智能指针持有的资源
    ~scoped_ptr() // never throws
    {
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
        boost::sp_scalar_destructor_hook( px );
#endif
        boost::checked_delete( px );
    }
};

上述源码可以看到scoped_ptr私有化了拷贝构造函数和赋值构造函数,直接从根本上杜绝了浅拷贝的发生。即scoped_ptr一旦被初始化之后,其就永远的管理了那块内存资源,不会被剥夺,直到析构函数释放该内存资源。

2.3 unique_ptr

unique_ptr的源码如下:

template<class _Ty,
	class _Dx>	// = default_delete<_Ty>
	class unique_ptr
		: public _Unique_ptr_base<_Ty, _Dx>
	{	// non-copyable pointer to an object
public:
	typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
	typedef typename _Mybase::pointer pointer;
	typedef _Ty element_type;
	typedef _Dx deleter_type;

    // 提供了带右值引用的拷贝构造函数
    // noexcept关键字的含义是表明该函数不会发生异常,这样有助于编译器对其进行一些特殊的优化处理加速函数的执行
	unique_ptr(unique_ptr&& _Right) noexcept
		: _Mybase(_Right.release(),
			_STD forward<_Dx>(_Right.get_deleter()))
		{	// construct by moving _Right
		}
	
	// 提供了带右值引用的operator=赋值重载函数
	unique_ptr& operator=(unique_ptr&& _Right) noexcept
		{	// assign by moving _Right
		if (this != _STD addressof(_Right))
			{	// different, do the move
			reset(_Right.release());
			this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
			}
		return (*this);
		}
	
    // 交换两个unique_ptr智能指针对象的底层指针和删除器
	void swap(unique_ptr& _Right) noexcept
		{	// swap elements
		_Swap_adl(this->_Myptr(), _Right._Myptr());
		_Swap_adl(this->get_deleter(), _Right.get_deleter());
		}

	// 通过自定义删除器释放资源
	~unique_ptr() noexcept
		{	// destroy the object
		if (get() != pointer())
			{
			this->get_deleter()(get());
			}
		}
	
	/*unique_ptr提供->运算符的重载函数*/
	_NODISCARD pointer operator->() const noexcept
		{	// return pointer to class object
		return (this->_Myptr());
		}

	// 返回智能指针对象底层管理的指针
	_NODISCARD pointer get() const noexcept
		{	// return pointer to object
		return (this->_Myptr());
		}

	/*提供bool类型的重载,使unique_ptr对象可以
	直接使用在逻辑语句当中,比如if,for,while等*/
	explicit operator bool() const noexcept
		{	// test for non-null pointer
		return (get() != pointer());
		}
    
    /*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
	pointer release() noexcept
		{	// yield ownership of pointer
		pointer _Ans = get();
		this->_Myptr() = pointer();
		return (_Ans);
		}

	/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
	void reset(pointer _Ptr = pointer()) noexcept
		{	// establish new pointer
		pointer _Old = get();
		this->_Myptr() = _Ptr;
		if (_Old != pointer())
			{
			this->get_deleter()(_Old);
			}
		}
	/*
	删除了unique_ptr的拷贝构造和operator=赋值函数,
	因此不能做unique_ptr智能指针对象的拷贝构造和
	赋值,防止浅拷贝的发生
	*/
	unique_ptr(const unique_ptr&) = delete;
	unique_ptr& operator=(const unique_ptr&) = delete;
	};

从源码可以看出,unique_ptr直接删除了拷贝构造函数和赋值构造函数,这样就可以避免浅拷贝的产生。同时提供了带右值引用的拷贝构造函数和赋值构造函数,即unique_ptr可以通过右值引用进行拷贝和赋值,或者是一些产生临时对象的地方。

// 代码示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数

// 代码示例2
// 返回一个临时对象
unique_ptr<int> test_uniqueptr()
{
	unique_ptr<int> ptr1(new int);
	return ptr1;
}
int main()
{
    // 调用右值引用的拷贝构造函数来接收一个临时对象。
	unique_ptr<int> ptr = test_uniqueptr();
	return 0;
}

同样,unique_ptr也是只能有一个该智能指针引用资源,同时unique_ptr还提供了reset、swap等资源交换函数。

3.带引用计数的智能指针

当多个智能指针指向同一内存资源的时候,每一个智能指针都会给该资源的引用计数加1,当一个智能指针对象析构的时候,同样会使智能指针的引用计数减1,当引用计数减到0的时候就可以释放该块内存资源了。这里要对智能指针引用计数进行++和--操作的时候,为了保证一个线程的安全,在shared_ptr和weak_ptr底层的引用计数采用的是CAS原子操作,这样使得引用计数是互斥访问的,保证了一个线程安全的问题。

shared_ptr的引用计数是存在于堆上的,new出来一块专门存放智能指针引用计数的资源空间,同时使用_Rep来指向new的这篇空间。一般而言shared_ptr我们称之为强智能指针,而weak_ptr我们称之为弱智能指针。针对于强弱智能指针使用的两个特殊场景。

3.1 智能指针的交叉引用问题

如下示例代码:

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

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> _ptrb; // 指向B对象的智能指针

    // 修改方式
    weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> _ptra; // 指向A对象的智能指针
    
    // 修改方式
    weak_ptr<A> _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
	shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
	ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2
	ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2

	cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
	cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2

	/*
	出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
	B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是
	A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,
	导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”
	*/
	return 0;
}

对于上述强智能指针的交叉引用问题,可以看出由于引用计数无法减到1导致双方管理的资源都无法得到释放,这样的解决方式就是:定义对象的时候,使用强智能指针shared_ptr而在其他地方引用对象的时候使用弱智能指针weak_ptr。

这里针对于weak_ptr和shared_ptr的区别主要在于:

  • weak_ptr不会改变资源的引用计数,它是作为一个观察者的角色用来判断管理的资源是否存在。
  • weak_ptr持有的引用计数并不是资源的引用计数,而是对同一资源的观察者的引用计数
  • weak_ptr不提供常用的一些指针的操作,也无法访问观察的资源,如果想操作weak_ptr只能通过lock方法把它提升为强智能指针。

3.2 多线程访问共享变量的问题:

思想如下一种场景:线程A和线程B访问一个共享的对象,此时线程A正在析构这个对象,而线程B在调用该对象的成员方法,如果此时线程A析构完了,而线程B去调用成员方法就会发生错误。
示例代码如下:

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

class Test
{
public:
	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
	Test() :_ptr(new int(20)) 
	{
		cout << "Test()" << endl;
	}
	// 析构Test对象,释放_ptr指向的堆内存
	~Test()
	{
		delete _ptr;
		_ptr = nullptr;
		cout << "~Test()" << endl;
	}
	// 在线程B中执行show成员函数
	void show()
	{
		cout << *_ptr << endl;
	}
private:
    // volatile关键字的作用就是防止编译器对其做优化,使其每次读取该值都需要从内存中重新进行读取。
	int *volatile _ptr;
};
void threadProc(Test *p)
{
	// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
	std::this_thread::sleep_for(std::chrono::seconds(2));
	/* 
	此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
	此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
	存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
	*/
    // 此时调用的时候,对象p已经被析构掉了,这样调用就会发生错误
	p->show();
}
int main()
{
	// 在堆上定义共享对象
	Test *p = new Test();
	// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
	std::thread t1(threadProc, p);
	// 在main线程中析构Test共享对象
	delete p;
	// 等待子线程运行结束
	t1.join();
	return 0;
}

上述代码,以为主线程已经delete了对象p,而子线程是毫不知情的,此时子线程还有对象p去调用成员函数就会出现错误。这里的解决方法就是通过强弱智能指针来解决:

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

class Test
{
public:
	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
	Test() :_ptr(new int(20)) 
	{
		cout << "Test()" << endl;
	}
	// 析构Test对象,释放_ptr指向的堆内存
	~Test()
	{
		delete _ptr;
		_ptr = nullptr;
		cout << "~Test()" << endl;
	}
	// 该show会在另外一个线程中被执行
	void show()
	{
		cout << *_ptr << endl;
	}
private:
	int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
	// 睡眠两秒
	std::this_thread::sleep_for(std::chrono::seconds(2));
	/* 
	如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
	为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
	的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
	已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
	*/
    // 弱智能指针提升为强智能指针,通过返回值来判断管理的资源是否已经被释放了。
	shared_ptr<Test> ps = pw.lock();
	if (ps != nullptr)
	{
		ps->show();
	}
}
int main()
{
	// 在堆上定义共享对象
	shared_ptr<Test> p(new Test);
	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
	std::thread t1(threadProc, weak_ptr<Test>(p));
	// 在main线程中析构Test共享对象
	// 等待子线程运行结束
	t1.join();
    // 如果这里改为t1.detach那么成员函数show就不会被调用。
	return 0;
}

注1:使用shared_ptr来管理内存资源的时候,管理的空间资源和引用计数资源是分开开辟的,这样会导致问题,如果引用计数的空间资源开辟出错,那么就是进一步导致了管理的内存资源无法释放。而使用make_shared来创建智能指针的时候,空间是一起开辟的,要么都成功要么都失败,但是也会带来额外的问题,第一就是无法自定义删除器,要等到weaks引用计数为0才释放资源。

注2:如果shared_ptr管理的资源不是new分配的内存,才考虑自定义删除器,这也是为什么make_shared不支持自定义删除器的原因,因为make_shared就是通过new分配内存资源。

4.自定义删除器

一般而言我们使用智能指针管理的都是堆内存空间(也就是new开辟的空间),当智能指针出作用域时就是自动调用析构函数来释放这块堆内存空间(也就是调用delete)。但是对于一些特殊的情况,就不能调用delete了,比如打开一个文件夹,这时候就需要自己来自定删除器来关闭打开的文件夹。

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

这里也可以使用lambda表达式来进行处理

int main()
{
	// 自定义智能指针删除器,关闭文件资源
	unique_ptr<FILE, function<void(FILE*)>> 
		filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});
	// 自定义智能指针删除器,释放数组资源
	unique_ptr<int, function<void(int*)>>
		arrayPtr(new int[100], [](int *ptr)->void {delete[]ptr; });
	return 0;
}
  • function<void(FILE*)>:指定了自定义删除器的类型
  • 自定义删除器是一个lambda表达式 [](FILE pf)->void { fclose(pf); },它接受一个FILE参数,并在unique_ptr超出作用域时调用fclose。

参考文档:
C++11智能指针
volatile关键字
noexcept关键字
使用delete删除指针

posted @ 2024-06-14 22:55  alone_qing  阅读(80)  评论(0)    收藏  举报