💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:C++修炼之路⏪
🚚代码仓库:C++高阶🚚
🌹关注我🫵带你学习更多C++知识
🔝🔝
目录
引言
通过前面的异常学习,我们知道捕捉到异常会直接导致代码跳转执行到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
这里和互斥锁那里是一样的。
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在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
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链接任何一个取消都会得到正常的释放
那么上面的问题是如何产生的?
为了解决这个问题,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>());
//lambda
std::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