当前位置: 首页> 教育> 幼教 > C++ 智能指针

C++ 智能指针

时间:2025/7/26 21:02:32来源:https://blog.csdn.net/2301_77934192/article/details/139610131 浏览次数:0次

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:C++修炼之路

🚚代码仓库:C++高阶🚚

🌹关注我🫵带你学习更多C++知识
  🔝🔝

目录

引言

 1. 为什么需要智能指针?

 2.智能指针的使用及原理

2.1 RAII

2.1.1 C++98 auto_ptr 

2.1.2 unique_ptr

2.1.3 shared_ptr

shared_ptr的循环引用  

2.1.4 weak_ptr

 3.定制删除器


引言

通过前面的异常学习,我们知道捕捉到异常会直接导致代码跳转执行到catch进行处理,如果这段异常代码涉及到内存管理,那么就会造成内存泄漏,整个工程最后申请不到内存资源。为了解决异常跳转执行而引发的其他问题,C++98最早推出了auto_ptr。但是这个指针在设计出来时就留下了很的多坑,所以在C++11后推出全新的智能指针。

 1. 为什么需要智能指针?

我们模拟一个异常的场景

#include <iostream>
using namespace std;
int Div()
{int a, b;cin >> a >> b;if (b == 0){throw invalid_argument("除0错误");}return a / b;
}
void Func()
{int* ptr1 = new int;int* ptr2 = new int;cout << Div() << endl;delete ptr1;delete ptr2;
}int main()
{try{Func();}catch(exception &e){cout << e.what() << endl;}return 0;
}

这里Func如果出现除0错误,那么就会导致后面的delete无法执行,从而导致内存泄漏。这时有人就会想到在出现异常地方从新 try throw catch进行重新抛出。

就比如下面这段代码

#include <iostream>
using namespace std;
int Div()
{int a, b;cin >> a >> b;if (b == 0){throw invalid_argument("除0错误");}return a / b;
}
void Func()
{int* ptr1 = new int;int* ptr2 = nullptr;try //ptr2 出现异常{ptr2 = new int;}catch(...){delete ptr1;throw;}try //Div出现除0异常{cout << Div() << endl;}catch (...){delete ptr1;delete ptr2;throw;}delete ptr1;delete ptr2;
}int main()
{try{Func();}catch(exception &e){cout << e.what() << endl;}return 0;
}

这段代码确实可以解决内存泄漏的问题,但是如果再来一个ptr3一直到ptrn?,那我们都像上面的try throw catch 这样?这代码看着也烦,而且一点也不优雅。于是大佬们利用ARII的思想来解决这个问题。

 2.智能指针的使用及原理

2.1 RAII

RAII Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在
对象析构的时候释放资源 。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效

这里和互斥锁那里是一样的。

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "~SmartPtr():delete" << _ptr << endl;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()
{SmartPtr<int> p1(new int(1));SmartPtr<int> p2(new int(2));cout << Div() << endl;}
int main()
{try{Func();}catch(exception &e){cout << e.what() << endl;}return 0;
}

  

通过我们编写的smartPtr这个类,利用成员的函数特性,自动调用析构函数。也确实在除0异常出现要跳转时,先调用了析构函数。但是话说智能指针还是指针,我们的类也需要想指针一样能使用。

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}T& operator*() {return *_ptr;}T* operator->(){return _ptr;}~SmartPtr(){if (_ptr){cout << "~SmartPtr():delete" << _ptr << endl;delete _ptr;}}
private:T* _ptr;
};

 这样对象也能解引用了,对于自定义类型的我们也可以用->。这些都比较简单。问题的关键是如何写拷贝和赋值重载?

比如下面一段代码?

SmartPtr<int> p3(p2);

p3拷贝p2 我们看看运行结果

  

代码就直接崩溃了,原因很简单,p3和p2同时指向了同一块空间,p2先析构,等p3再析构时,野指针了。

既然指向同一块空间,深拷贝?深拷贝不行,问题是指针本身就是要浅拷贝。STL的容器都是浅拷贝。迭代器为什么不报错?迭代器本身自己就不涉及资源的管理,而智能指针涉及资源的管理,所以不能单纯的浅拷贝

到这里就要说说一下智能指针的发展历史

2.1.1 C++98 auto_ptr 

既然指向同一块空间,那被拷贝对象的资源直接转移给拷贝对象。C++98版本的库中就提供了auto_ptr的智能指针。

下面演示的auto_ptr的使用及问题。

	template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}~auto_ptr(){if (_ptr){cout << "delete" << _ptr << endl;delete _ptr;}}private:T* _ptr;};

这样确实没有问题。如果有人不知道auto_ptr 会把 p1置空?就像下面这段代码

 直接就崩了,所以说auto_ptr的管理权转移是个失败品。

由于auto_ptr的失败C++11推出了unique_ptr

2.1.2 unique_ptr

这个思路也是简单,既然拷贝要出事,那干脆直接就禁掉拷贝和赋值。

template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;~unique_ptr(){if (_ptr){cout << "~SmartPtr():delete" << _ptr << endl;delete _ptr;}}private:T* _ptr;};

这样做确实也能防止拷贝的问题,有些场景就是需要拷贝怎么办?后面C++11又推出了shared_ptr。利用引用计数的思想来解决。

2.1.3 shared_ptr

 . shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共

2. 对象被销毁时 ( 也就是析构函数调用 ) ,就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是 0 ,就说明自己是最后一个使用该资源的对象, 必须释放该资源
4. 如果不是 0 ,就说明除了自己还有其他对象在使用该份资源, 不能释放该资源 ,否则其他对
象就成野指针了。

template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr) //防止自己给自己赋值{Release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}void Release(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}~shared_ptr(){Release();}private:T* _ptr;int* _pcount; //如果是static成员变量,那么属于所有对象。};void test_shared(){shared_ptr<int> sp1(new int(1));shared_ptr<int> sp2(sp1);shared_ptr<int> sp3(sp2);shared_ptr<int> sp4(new int(10));//sp1 = sp4;sp4 = sp1;sp1 = sp1;sp1 = sp2;}

 

到这里就完了吗? 如果是多个线程执行这个shared肯定会有线程安全的问题,_pcount是new出来的,是堆资源、++ --又不是原子操作。 所以对shared的成员_pcount需要加锁,当然我们也可以对成员直接变成原子的。

加锁版本

	template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex) {}~shared_ptr(){Release();}void Release(){int flag = false;if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;unique_lock<mutex> lck(*_pmtx);//在这里对--加锁flag = true;}if (flag)delete _pmtx;}void AddCount(){unique_lock<mutex> lck(*_pmtx);++(*_pcount);}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){AddCount();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp._pmtx;AddCount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;          // 指向管理对象的指针int* _pcount;     // 引用计数mutex* _pmtx; // 互斥锁,用于同步对引用计数的访问};

原子版本

template<class T>class shared_ptr{public:shared_ptr(T* ptr): _ptr(ptr), _pcount(new std::atomic<int>(1)) {}// 复制构造函数shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount) {SubAdd();}// 赋值操作符shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (this != &sp) {// 先递减当前对象的引用计数if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1){delete _ptr;delete _pcount;}// 然后复制新对象的指针和引用计数_ptr = sp._ptr;_pcount = sp._pcount;// 递增新对象的引用计数SubAdd();}return *this;}void SubAdd(){	// 自动递增引用计数_pcount->fetch_add(1, std::memory_order_relaxed);}// 解引用操作符T& operator*() { return *_ptr; }// 成员访问操作符T* operator->() { return _ptr; }// 析构函数~shared_ptr() {if (_pcount->fetch_sub(1, std::memory_order_acq_rel) == 1){delete _ptr;delete _pcount;}}T* get(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;std::atomic<int>* _pcount;};

 

结果是1 原子版本也没有问题。

这里需要说明的是为什么用ref()这个函数,原因很简单,

智能指针的参数和锁的参数都是引用,但是我们是以线程调用的,而线程构造其函数的参数,是禁止拷贝的。 因为引用本身不是一个对象,而是一个指向对象的别名。如果尝试直接传递引用,编译器无法为其创建一个副本,因为引用不具有复制或移动语义。

使用 std::ref() 的目的在于告诉 std::thread 构造函数:“我知道我要传递的是一个引用,并且我希望你以引用的方式来处理它。”

智能指针是安全的,智能指针管理的对象是安全的吗?

 

结果来看我们也是需要对对象涉及的临界资源进行加锁 

 

shared_ptr的循环引用  

我先来一段简单代码看看运行结果

	struct ListNode{ListNode* _next;ListNode* _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}};//循环引用void test_shared_cycle(){ListNode* n1 = new ListNode;ListNode* n2 = new ListNode;n1->_next = n2;n2->_prev = n1;delete n1;delete n2;}

 

我们调用test_shared_cycle() 函数能够正常析构。如果我把上面的代码改成用shared_ptr来管理list类会发生什么?

struct ListNode{gx::shared_ptr<ListNode> _next;gx::shared_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}};//循环引用void test_shared_cycle(){/*ListNode* n1 = new ListNode;ListNode* n2 = new ListNode;*/gx::shared_ptr<ListNode> n1 = new ListNode;gx::shared_ptr<ListNode> n2 = new ListNode;n1->_next = n2;n2->_prev = n1;}

运行的结果并没有运行ListNode的析构函数。

如果我们把n1和n2链接任何一个取消都会得到正常的释放

那么上面的问题是如何产生的?

n1n2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上 一个节点。
也就是说_next析构了,n2就释放了。
也就是说_prev析构了,n1就释放了。
但是_next属于n1的成员,n1释放了,_next才会析构,而n1_prev管理,_prev
属于n2成员,所以这就叫循环引用,谁也不会释放。

为了解决这个问题,C++11又推出了weak_ptr。 

2.1.4 weak_ptr

如何解决shared_ptr的循环引用?只要n1和n2在链接的过程中,不增加引用计数就行。

template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}private:T* _ptr;
};

 

运行结果来看weak_ptr解决这里引用循环的问题。 我们打印引用计数看看。

这里需要强调的是,我们实现的都是智能指针最核心的部分,库里的源代码不是我们这样实现的,库里要考虑的场景更多,比如内存碎片,库源代码要复杂的更多。

 3.定制删除器

struct Date{int _year = 0;int _month = 0;int _day = 0;~Date() {};};
void test_shared_delete(){gx::shared_ptr<Date> spd(new Date[10]);}

 

这时我们用shared_ptr就会出现报错的原因。

核心原因:是因为我们写了析构函数,在实例化对象时,会多开4个字节,而shared_ptr的析构函数释放位置就会从多开的4个字节这里开始释放。释放的位置不对,程序崩溃是必然。内存错误。 

针对上面的问题我们需要用到定制删除器

 从官方文档的构造函数来看,定制删除器,是模板只要是能调用的对象都能传参。比如lambda、仿函数、函数、函数指针。

先自己写一个仿函数,然后我们用库的shared_ptr。

	template<class T>struct DeleteArry{void operator(T* ptr){cout << "void operator(T* ptr)" << endl;delete[] ptr;}};
void test_shared_delete(){	//仿函数std::shared_ptr<Date> spd0(new Date[10],DeleteArry<Date>());//lambdastd::shared_ptr<Date> spd1(new Date[10], [](Date* ptr) {	cout << "Lambda delete[]" << endl;delete[] ptr;});//文件指针std::shared_ptr<FILE> spd2(fopen("Test.cpp","r"), [](FILE* ptr){	cout << "Lambda fclose:" << endl;fclose(ptr);});}

 

那我们如何在自己的shared_ptr实现这一功能?

库里的定制删除器是个模板,传过去时,库里是存起来的。所以我们也需要写一个存储定制删除器的构造函数。

template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex), _del(del){}

         

template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _pmtx(new mutex), _del(del){}~shared_ptr(){Release();}void Release(){_pmtx->lock();int flag = false;if (--(*_pcount) == 0){//cout << "delete:" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount;flag = true;}_pmtx->unlock();if (flag)delete _pmtx;}void AddCount(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){AddCount();}// sp1 = sp4// sp1 = sp1;// sp1 = sp2;shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp._pmtx;AddCount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}T* get() {return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;mutex* _pmtx;// D _del; //如果是这样就不行,因为这个D是属于定制删除器构造成员函数的,析构是用不了的//包装器function<void(T*)> _del = [](T* ptr) {cout << "lambda delete:" << ptr << endl;delete ptr;};
};

 

 总结:

智能指针根据自己的需要到底是使用unique、shared、weak。

不考虑拷贝,unique

涉及拷贝  shared

如果是list map set unorderdmap unorderdset 这种需要用到weak

关键字:C++ 智能指针

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: