【C++】智能指针

🌇个人主页:平凡的小苏
📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘

🛸C++专栏C++内功修炼基地
> 家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。 欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

一、为什么需要智能指针

因为异常层层嵌套,执行流乱跳导致内存泄漏

c 复制代码
int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");
    return a / b;
}
void Func()
{
    // 1、如果p1这里new 抛异常会如何?
    // 2、如果p2这里new 抛异常会如何?
    // 3、如果div调用这里又会抛异常会如何?
    int* p1 = new int;
    int* p2 = new int;
    cout << div() << endl;
    delete p1;
    delete p2;
}
int main()
{
    try
    {
        Func();
    }
    catch (exception& e)
    {
        cout << e.what() << endl;
    }
    return 0;
}

这里本来只想要div抛出异常,但是如果new出错也抛出异常的话,导致执行流乱跳导致资源没有释放,内存泄漏!

二、智能指针的使用

1、RAII

​ RAII是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。对象构造时获取资源,对象析构时自动释放资源。(可以利用局部对象出作用域自动调用析构函数完成对象资源的清理)

2、像指针一样

为了让智能指针像指针一样支持*、->等运算符,还需要在智能指针类中去重载这些运算符。

3、智能指针等于RAII+运算符重载

可以把new出来的资源交给智能指针的对象进行管理,对象出了作用域会调用析构函数对资源进行释放。

智能指针可以减少内存泄漏的风险。

c 复制代码
// 使用RAII思想设计的SmartPtr类
template<class T>
    class SmartPtr {
    public:
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
        {}
    ~SmartPtr()
    {
        if(_ptr)
            delete _ptr;
    }

    private:
    T* _ptr;
};
int div()
{
    int a, b;
    cin >> a >> b;
    if (b == 0)
        throw invalid_argument("除0错误");
    return a / b;
}
void Func()
{
    ShardPtr<int> sp1(new int);
    ShardPtr<int> sp2(new int);
    cout << div() << endl;
}
int main()
{
    try {
        Func();
    }
    catch(const exception& e)
    {
        cout<<e.what()<<endl;
    }
    return 0;
}

三、智能指针的原理

1、auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理

c 复制代码
using namespace std;
namespace sqy
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		//管理权转移
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}

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

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

	void test_ptr()
	{
		auto_ptr<int> ap(new int(1));
		auto_ptr<int> ap1(ap);
	}
}

由于管理权转移,这个auto_ptr会导致被拷贝对象悬空

2、unique_ptr和shared_ptr

2.1、unique_ptr(唯一指针)

unique_ptr直接将拷贝构造和赋值给禁用了。简单粗暴。

c 复制代码
unique_ptr(const unique_ptr&) = delete;
unique_ptr<T>& operator=(const unique_ptr&) = delete;

2.2、shared_ptr(共享指针)

使用构造函数构造一个shared_ptr的对象,这个对象将会获得一个指向资源的指针和一个指向堆区的计数器。

每当拷贝构造或赋值时,让这个计数器++。每当减少一个指向该资源的对象,计数器--,直到计数器为零才释放资源及计数器。

c 复制代码
namespace sqy
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}


		shared_ptr(const shared_ptr<T>& ap)//拷贝构造
			:_ptr(ap._ptr)
			,_pcount(ap._pcount)
		{
			++(*_pcount);
		}

		shared_ptr& operator=(shared_ptr<T>& ap)//赋值运算符重载
		{
			if (_pcount != ap._pcount)//赋值不同的对象时,引用计数才进行++
			{
				if (--(*_pcount) == 0)
				{
					delete _ptr;
					delete _pcount;
				}
				_ptr = ap._ptr;
				_pcount = ap._pcount;
				++(*_pcount);
			}
			return *this;
		}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
				delete _pcount;
			}
		}

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

		T* operator->()
		{
			return &_ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};

2.3、shared_ptr是否线程安全

说到计数器,就应该想到多线程场景。一旦有多个线程同时访问计数器,对其进行+±-操作,势必会发生计数紊乱的问题。需要使用互斥锁对计数器加锁保护:

c 复制代码
namespace sqy
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_mtx(new mutex)
		{}


		shared_ptr(const shared_ptr<T>& ap)//拷贝构造
			:_ptr(ap._ptr)
			,_pcount(ap._pcount)
			,_mtx(ap._mtx)
		{
			_mtx->lock();
			++(*_pcount);
			_mtx->unlock();

		}

		shared_ptr& operator=(shared_ptr<T>& ap)//赋值运算符重载
		{
			if (_pcount != ap._pcount)//赋值不同的对象时,引用计数才进行++
			{
				Release();
				_ptr = ap._ptr;
				_pcount = ap._pcount;
				_mtx->lock();
				++(*_pcount);
				_mtx->unlock();
			}
			return *this;
		}
		void Release()
		{
			bool flag = false;
			_mtx->lock();
			if (--(*_pcount) == 0)
			{
				flag = true;
				delete _ptr;
				delete _pcount;
			}
			_mtx->unlock();
			if (flag)//只有在其他资源释放的时候锁才要释放,使用标志进行判断释放
			{
				delete _mtx;
			}
		}
		~shared_ptr()
		{
			Release();
		}

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

		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _mtx;
	};
	
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
int main()
{
	int n = 1000000;
	sqy::shared_ptr<Date> sp1(new Date);
	mutex mtx;
	thread t1([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				sqy::shared_ptr<Date> sp2 = sp1;
				mtx.lock();
				(sp2->_day)++;
				(sp2->_month)++;
				(sp2->_year)++;
				mtx.unlock();
			}
		});
	thread t2([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				sqy::shared_ptr<Date> sp3 = sp1;
				mtx.lock();
				(sp3->_day)++;
				(sp3->_month)++;
				(sp3->_year)++;
				mtx.unlock();
			}
		});
	t1.join();
	t2.join();
	std::cout << (sp1->_day) << " " << (sp1->_month) << " " << (sp1->_year) << std::endl;//cout不明确
	return 0;
}

共享指针只对计数器来说是线程安全的,但是对于共享指针指向的资源,却是线程不安全的。如上方代码,将sp1赋值给sp2和sp3,并在lambda表达式中对date中的成员变量进行++,这时资源的是线程不安全的,多线程形式使用共享指针改动资源时需要额外申请一把互斥锁进行保护。

2.4、shared_ptr循环引用问题

使用shared_ptr一旦出现循环引用的现象,将会造成内存泄漏。为了解决这一问题,可以使用weak_ptr。

3、weak_ptr指针

弱指针通常和共享指针搭配使用,解决共享指针循环引用问题。

​ weak_ptr支持无参的构造、支持拷贝构造、shared_ptr的拷贝构造。但是不支持使用指针进行构造,不支持RAII。

使用weak_ptr解决shared_ptr的循环引用问题。weak_ptr本身支持shared_ptr的拷贝与赋值,且不会增加引用计数。(weak_ptr不参与资源的管理)

四、定制删除器

析构的方式多种多样,例如数组和文件指针的释放方式不一样。这个时候就需要使用定制删除器进行对象的析构。

c 复制代码
template <class T>
struct Delete
{
	void operator()(const T* ptr)
	{
		delete[] ptr;
	}
};
int mian()
{
	shared_ptr<int> sp1(new int[10], Delete<int>());
	shared_ptr<string> sp2(new string[10], Delete<string>());
 
	shared_ptr<string> sp3(new string[10], [](string* ptr) {delete[] ptr; });
	shared_ptr<FILE> sp4(fopen("test.txt", "r"), [](FILE* ptr) {fclose(ptr); });
	return 0;
}
c 复制代码
namespace sqy
{
	template<class T>
	class default_delete
	{
	public:
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};
	//default_delete<T>作为默认删除器
	template<class T, class D = default_delete<T>>
	class weak_ptr
	{
	public:
	};
}

外部有传入可调用对象就使用外部传入的,否则使用默认删除器。

相关推荐
tyler_download5 分钟前
golang 实现比特币内核:实现基于椭圆曲线的数字签名和验证
开发语言·数据库·golang
小小小~6 分钟前
qt5将程序打包并使用
开发语言·qt
hlsd#6 分钟前
go mod 依赖管理
开发语言·后端·golang
小春学渗透8 分钟前
Day107:代码审计-PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·安全·web安全·php·mvc
杜杜的man10 分钟前
【go从零单排】迭代器(Iterators)
开发语言·算法·golang
亦世凡华、11 分钟前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
神仙别闹18 分钟前
基于MFC实现的赛车游戏
c++·游戏·mfc
小c君tt25 分钟前
MFC中 error C2440错误分析及解决方法
c++·mfc
测试界的酸菜鱼25 分钟前
C# NUnit 框架:高效使用指南
开发语言·c#·log4j
GDAL25 分钟前
lua入门教程 :模块和包
开发语言·junit·lua