首页 > 其他分享 >智能指针

智能指针

时间:2024-06-14 22:59:28浏览次数:12  
标签:对象 智能 unique pointer ptr 指针

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删除指针

标签:对象,智能,unique,pointer,ptr,指针
From: https://www.cnblogs.com/aloneqing/p/18248671

相关文章

  • 愿带你拿捏指针(1)
    前面带你们讲了讲函数,接下来带大家学习一下指针。从这三方面带大家认识指针。一.什么是指针我们前面说过整形,整形是存放整数的,那指针是存放什么的呢?我们往下看1.内存和地址我们知道在一个小区里面找到一个人的家需要知道人家再几栋几单元几零几,那我们怎么再计算机中找......
  • RPA-UiBot6.0控制与运行机器人 —工作任务智能调度自动运行
      前言     来也产品文档中心来也产品文档中心(laiye.com)https://documents.laiye.com/    友友们你们是否曾因为例行性工作的繁琐而苦恼?是否想要让机器人帮你自动执行这些任务?小北的这篇博客将为友友们揭示其中的奥秘,让我们一起学习如何通过RPA控制与......
  • 华为OD机试 C++ - 智能成绩表
    智能成绩表前言:本专栏将持续更新互联网大厂机试真题,并进行详细的分析与解答,包含完整的代码实现,希望可以帮助到正在努力的你。关于大厂机试流程、面经、面试指导等,如有任何疑问,欢迎联系我,wechat:steven_moda;email:[email protected];备注:CSDN。题目描述小明来到某学校当老......
  • 高考志愿专业选择:计算机人才需求激增,人工智能领域成热门
    随着2024年高考的落幕,数百万高三学生站在了人生新的十字路口,面临着一个重要的抉择:选择大学专业。这一选择不仅关乎未来四年的学习生涯,更可能决定一个人一生的职业方向和人生轨迹。在众多专业中,计算机相关专业因其广泛的就业前景和不断变化的行业需求,一直是学生和家长们关注的焦点......
  • 夏季城市环境卫生挑战多:TSINGSEE青犀智慧环卫方案助力城市垃圾站智能管理
    一、背景分析夏季,随着气温的攀升,城市垃圾的数量和种类也随之增加,这给环卫工作带来了极大的挑战。环卫垃圾站点作为城市垃圾处理的重要一环,其管理效率直接关系到城市环境的整洁与卫生。近年来,随着视频监控技术的不断发展,其在环卫垃圾站点的应用也逐渐受到重视,为夏季环卫工作带来了......
  • 夏季河湖防溺水新举措:青犀AI视频智能监控系统保障水域安全
    近日一则新闻引起大众关注,有网友发布视频称,假期在逛西湖时,发现水面上“平躺”漂浮着一名游客在等待救援。在事发3分钟内,沿湖救生员成功将落水游客救到了岸边。随着夏季的到来,雨水增多,各危险水域水位大幅上升,加上天气炎热,偷偷下湖游泳者屡禁不止,尤其是在水库、深水池塘、湖泊、河......
  • 夏季城市环境卫生挑战多:TSINGSEE青犀智慧环卫方案助力城市垃圾站智能管理
    一、背景分析夏季,随着气温的攀升,城市垃圾的数量和种类也随之增加,这给环卫工作带来了极大的挑战。环卫垃圾站点作为城市垃圾处理的重要一环,其管理效率直接关系到城市环境的整洁与卫生。近年来,随着视频监控技术的不断发展,其在环卫垃圾站点的应用也逐渐受到重视,为夏季环卫工作带......
  • 智能指针的思路
    目录前言一、智能指针是什么?二、智能指针代码步骤1.创建一个基本的类,这个类有你想实现的功能2.智能指针类3.整体代码总结前言    在日常的类应用场景中,我们会很多时候涉及到申请内存new关键词和清空内存delete的使用,而我们在很多时候会在申请内存后,忘记......
  • 传统行业龙头企业与定制化AI智能名片S2B2C商城小程序:构建数字化领导地位
    一、引言随着数字化浪潮的推进,传统行业龙头企业正面临着前所未有的挑战与机遇。在社交零售电商平台蓬勃发展的背景下,如何有效利用这些平台构建自己的数字化领导地位,成为传统行业龙头企业亟待解决的问题。本文将探讨企业定制开发AI智能名片S2B2C商城小程序的重要性,并分析其如何......
  • 数据质量守护者:数据治理视角下的智能数据提取策略
    一、引言在信息化和数字化高速发展的今天,数据已成为企业决策、运营和创新的核心要素。然而,随着数据量的快速增长和来源的多样化,数据质量问题逐渐凸显,成为制约企业数据价值发挥的关键因素。数据治理作为确保数据质量、提升数据价值的重要手段,其核心任务之一就是实现高效、准确......