一文带你详细了解C++智能指针

目录

 

智能指针的介绍及作用:

自己实现智能指针:

智能指针的代码扩充

不带引用计数器的智能指针:

1.auto_ptr

2.scoped_ptr

3.unique_ptr

带引用计数的智能指针:

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

多线程访问共享对象问题

自定义删除器


智能指针的介绍及作用:

当用户在使用裸指针的时候,由于这样或者那样的原因,让已申请的内存没有释放或者多次释放,从而引起程序的崩溃。而智能指针可以让用户不在关注资源的释放。

c++提供了带引用计数的智能指针和不带引用计数的智能指针:auto_ptr,scoped_ptr,unique_ptr,shared_ptr,weak_ptr.

自己实现智能指针:

template<typename T>
class CSmartPtr
{
public:
	CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
	~CSmartPtr() { delete mptr; }
private:
	T *mptr;
};

智能指针主要应用到以下两点内容:

1)智能指针体现在把裸指针进行了一次面向对象的封装,在构造函数中初始化资源,在析构函数中释放资源。

2)利用栈上的对象出作用域自动释放这个特点,在智能指针的析构函数中保证释放资源。

由以上两点可以知道,智能指针必定是定义在栈上的。如CSmartPtr<int> *p  = new CSmartPtr<int>(new int()),这里的p虽让是一个智能指针类型,但实质上还是一个裸指针,因此还是需要我们来进行手动的释放。

智能指针的代码扩充

template<typename T>
class CSmartPtr
{
public:
	CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
	~CSmartPtr() { delete mptr; }

	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }

	T* operator->() { return mptr; }
	const T* operator->()const { return mptr; }
private:
	T *mptr;
};

上面的这个智能指针,使用起来就和一般的指针非常的相似了,但是它还是有很大的问题:

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

这段代码会直接的崩溃掉,问题出在在执行默认构造函数的时候执行的是浅拷贝,两个对象在执行析构函数的时候会释放同一块资源空间,从而引起了错误。

不带引用计数器的智能指针:

1.auto_ptr

我们先来分析以下auto_ptr的源码:

template<typename _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持有资源地址
	*/
    auto_ptr(auto_ptr& _Right) noexcept
	: _Myptr(_Right.release())
	{	// construct by assuming pointer from _Right auto_ptr
	}
		
    _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,因此只有最后一个智能指针是持有资源的。例如:


int main()
{
	auto_ptr<int> p1(new int);
	/*
	经过拷贝构造,p2指向了new int资源,
	p1现在为nullptr了,如果使用p1,相当于
	访问空指针了,很危险
	*/
	auto_ptr<int> p2 = p1;
	*p1 = 10;
	return 0;
}

基于以上的属性,可以得知auto_ptr是不能应用在容器之中的。具体原因请看以下代码:

int main()
{
	vector<auto_ptr<int>> vec;
	vec.push_back(auto_ptr<int>(new int(10)));
	vec.push_back(auto_ptr<int>(new int(20)));
	vec.push_back(auto_ptr<int>(new int(30)));
	// 这里可以打印出10
	cout << *vec[0] << endl;
	vector<auto_ptr<int>> vec2 = vec;
	/* 这里由于上面做了vector容器的拷贝,相当于容器中
	的每一个元素都进行了拷贝构造,原来vec中的智能指针
	全部为nullptr了,再次访问就成访问空指针了,程序崩溃
	*/
	cout << *vec[0] << endl;
	return 0;
}

所以,不要再容器中使用auto_ptr,c++建议最好不要使用auto_ptr,除非是在非常简单的场景下。

2.scoped_ptr

首先来看一下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 &);
 
    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也不能应用在容器之中,因为它不能进行拷贝和赋值。auto_ptr会任意的转移资源的所有权,但scoped_ptr不会转移资源的所有权。

3.unique_ptr

要了解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;

	/*提供了右值引用的拷贝构造函数*/
	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和scoped_ptr做的一样,都是将拷贝构造函数和赋值函数进行私有化,从根本上防止智能指针的浅拷贝的发生。

但unique_ptr提供了带右值引用参数的拷贝构造和赋值,也就是说unique_ptr智能指针可以同过右值引用来进行拷贝构造和赋值操作,或者在产生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()
{
	/*
	此处调用test_uniqueptr函数,在return ptr1代码
	处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr
	*/
	unique_ptr<int> ptr = test_uniqueptr();
	return 0;
}

unique_ptr还提供了reset方法来重置资源,swap方法交换资源。unique_ptr最终只有一个指针来引用资源,建议在不使用带引用计数的智能指针的情况下,可以使用unique_ptr.

带引用计数的智能指针:

这里主要介绍shared_ptr和weak_ptr两个智能指针。

那么,什么是带引用计数的智能指针呢?

允许多个智能指针同时指向同一资源,每一个智能指针都会给资源的引用计数器加1,同样,当一个智能指针析构的时候,会使资源的引用计数器减1,当资源的引用计数器从1减到0的时候,就表明没有智能指针指向该资源了,该资源就可以进行释放了。

要对资源的引用个数来进行计数,++和--并不是线程安全的操作,因此shared_ptr和weak_ptr底层的引用计数已经通过CAS操作,保证了引用计数器加减的原子特性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针。

由shared_ptr的源码分析可以得出,shared_ptr的引用计数器是存放在堆山的,因此它被称为强指针,weak_ptr称为弱智能指针,它们有下面两个重要的引用场景需要注意。

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

#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对象的智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_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;
}

代码打印结果:

A()

B()

2

2

分对析可得,两个两个对象是没有办法来进行析构得。解决方法:在对象定义得时候用强智能指针,在对象得其他引用得地方用弱智能指针。

弱智能指针和强智能指针得区别之处在于:

1)weak_ptr并不会改变资源得引用计数器,而是以一个观察者的身份来观察shared_ptr是否存在。

2) weak_ptr持有的引用计数器,不是资源的引用计数器,而是资源呃观察者的引用计数器。

3)weak_ptr并没有提供常用的指针操作,无法直接访问资源。需要通过lock方法将weak_ptr提升为shared_ptr,才能对资源进行访问。

因此上述的代码就可以改为:

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

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	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
	// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptra->_ptrb = ptrb;
	// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptrb->_ptra = ptra;

	cout << ptra.use_count() << endl; // 打印结果:1
	cout << ptrb.use_count() << endl; // 打印结果:1

	/*
	出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
	B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
	被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
	*/
	return 0;
}

程序打印结果为:

A()

B()

1

1

~B()

~A()

 

可以看到A和B正常析构。

多线程访问共享对象问题

设有线程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;
	}
	// 该show会在另外一个线程中被执行
	void show()
	{
		cout << *_ptr << endl;
	}
private:
	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->show();
}
int main()
{
	// 在堆上定义共享对象
	Test *p = new Test();
	// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
	std::thread t1(threadProc, p);
	// 在main线程中析构Test共享对象
	delete p;
	// 等待子线程运行结束
	t1.join();
	return 0;
}

分析代码得:main主线成在析构掉对象以后,子线程才去访问对象得成员方法。无法打印出*_ptr的值。可以用shared_ptr和weak_ptr来解决多线程访问共享对象的安全问题。上面的代码可修改如下;

#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();
	return 0;
}

通过上述的方法,main主线程调用了join等待子线程结束。pw弱指针通过lock方法成功提升等级为强指针,通过强指针就可以访问对象的成员方法了。

如果将线程设置为分离线程,main程序就会析构对象,因此在线程中弱指针提升等级就会失败!

int main()
{
	// 在堆上定义共享对象
	shared_ptr<Test> p(new Test);
	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
	std::thread t1(threadProc, weak_ptr<Test>(p));
	// 在main线程中析构Test共享对象
	// 设置子线程分离
	t1.detach();
	return 0;
}

以上是在多线中shared_ptr和weak_ptr的一个典型的应用。

自定义删除器

我们经常使用的智能指针管理的是堆资源,当智能指针出作用域的时候,在其析构函数中会delete释放堆资源,但是除了堆资源,智能指针还可以管理其他资源,比如打开的文件,此时文件对象的关闭,就不能用delete了。这时我们需要自定义智能指针释放资源的方式,首先我们来分析以下智能指针unique_ptr的析构函数,如下:

~unique_ptr() noexcept
{	// destroy the object
if (get() != pointer())
	{
	this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
	}
}

从上述代码可以看出,删除器其实就是在析构函数中 调用一个函数对象而已。因此我们可以来实现自己的删除其:

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

在用模板实例化的时候,将

此函数对象当作模板参数传入即可。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LVS(Linux Virtual Server)是一种基于 Linux 系统的负载均衡集群技术,它主要用于将网络流量分发到多个服务器上,以提高系统的可靠性、可扩展性和性能。 LVS 集群一般包括四个组件:调度器(LVS 调度器)、前端服务器(负载均衡器)、后端服务器(真实服务器)和存储服务器(用于共享数据)。首先,调度器接收来自客户端的请求,然后根据配置的调度算法(如轮询、加权轮询、最小连接数等)将请求分发到多个前端服务器。前端服务器接收到请求后,通过相应的负载均衡算法将请求转发到后端的真实服务器上进行处理。在整个过程中,存储服务器用于存放共享的数据,以确保所有的真实服务器都能获取到相同的数据,并提供一致的服务。 LVS 集群的优点是能够提高网站的稳定性和可靠性,当某一台服务器出现故障时,调度器会自动将请求分发到其他可用的服务器上,从而保证服务的连续性。同时,LVS 集群还能够通过增加前端服务器和后端服务器的数量来提高系统的性能和吞吐量,以满足不断增长的用户需求。 在实际应用中,LVS 集群需要合理配置,包括选择合适的调度算法、调整每台服务器的权重、选择适当的硬件设备等。此外,还需要及时监控集群的运行状态,及时发现和解决故障,以确保整个系统的正常运行。 总的来说,LVS 负载均衡集群是一种强大而高效的集群技术,能够帮助企业提高系统的可靠性和性能,是现代互联网应用中不可或缺的重要组成部分。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值