0
点赞
收藏
分享

微信扫一扫

C++11之智能指针(万字长文详解)

C++11之智能指针

为什么需要智能指针

#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()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果div调用这里又会抛异常会如何?
int* p1 = new int[10];
cout << div() << endl;//如果div抛出异常那么就会导致内存泄漏!
//因为后面的delete无法执行!会直接跳出这个函数
delete[] p1;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0
}

解决办法 一

void Func()
{
int* p1 = new int[10];
try
{
cout << div() << endl;
}
catch (...)
{
delete[]p1;
throw;
}
delete[]p1;
}

==但是这样子虽然解决了,但是首先这个代码不整洁和美观,其次,这个代码还有一个问题==

void Func()
{
int* p1 = new int[10];
int* p2 = new int[10];
try
{
cout << div() << endl;
}
catch (...)
{
delete[]p1;
delete[]p2;
throw;
}
delete[]p1;
delete[]p2;
}

这个为什么有问题?——因为new本身也有可能会抛出异常!如果p1抛出异常还好!==但是如果抛出异常的是p2呢?——那么p1内存就无法被释放导致内存泄漏!因为p2抛出异常是直接回到main函数的!既不会执行下面的delete代码,也不会进入下面的catch!==

void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int[10];
int* p2 = nullptr;
try
{
p2 = new int[10];
try
{
cout << div() << endl;
}
catch (...)
{
delete[]p1;
delete[]p2;
throw;
}
}
catch (...)
{
delete[]p1;
throw;
}
delete[]p1;
delete[]p2;
}

但是这还只是两个new,如果是三个呢,四个呢?

==所以为了解决这种类似的情况!于是有了智能指针!==

智能指针的原理

==智能指针的原理其实很简单!——我们不要手动释放!让指针出了作用域后自动的释放!==

那么如何做到的?——写一个类就好了!

template<class T>
class SmartPtr
{
public:
//构造函数在保存资源!
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}

//析构函数在释放资源!
~SmartPtr()
{
if (_ptr)
delete _ptr;
cout << ~SmartPtr() << endl;
}
//重装* 让智能指针具有像一般指针一样的行为
T& operator*()
{
return *_ptr;
}

T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};

==我们都知道出了作用域后,类会自动的去调用析构函数去释放资源!我们就可以通过这一特性来实现智能指针!==

#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* p1 = new int[10];
SmartPtr<int> sp1(p1);
int* p2 = new int[10];
SmartPtr<int> sp2(p2);
*p1 = 10;
p1[0]--;
cout << div() << endl;
}

int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}

image-20230523120918530

如果p2抛异常,p1出了作用域也会自动释放!不用担心!

RAII

上面的这种思想我们称之为RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。

STL中的智能指针

STL库中给我们提供了四种不同类型的智能指针!分别是auto_ptr,unique_ptr,weak_ptr,shared_ptr接下来将会一一介绍

都在momory这个头文件里面!

std::auto_ptr

auto_ptr是STL库中最早引入的智能指针!也是最臭名昭著的智能指针!——它具有十分的多的缺陷!

template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}

T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
//这是我上面写的智能指针!——它有一个很大的问题!——拷贝!
int main()
{
SmartPtr<int> sp(new int);
SmartPtr<int> sp2(sp);
return 0;
}

image-20230524144632999

因为默认生成的拷贝构造是一个浅拷贝!这就是导致了sp与sp2都是维护的是同一个的地址!所以一旦出了作用域!就会导致同一块空间被释放两次!

解决办法很多——例如写一个计数器!

auto_ptr的底层实现
namespace MySTL 
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
delete _ptr;
}

auto_ptr(auto_ptr<T>& ap)//不可以加上const,这会导致ap._ptr = nullptr;编译不通过
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}

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

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

private:
T *_ptr;
};
}

==auto_ptr的实现很简单,就是一个简单的置空就好了==

unique_ptr

这个智能指针的前身是boost库里面的scope_ptr,后续进入了STL库中改名为unique_ptr

==unique_ptr对于解决拷贝的方式非常的简单粗暴!==

int main()
{
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1);
up2 = up1;
return 0;
}

image-20230524152324937

它直接禁掉了拷贝构造和赋值!不让拷贝构造也不让赋值!——就像是它的名字一样unique(唯一)

unique_ptr的底层实现
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}

~unique_ptr()
{
if (_ptr)
delete _ptr;
}

//直接全部禁掉!
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

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

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

private:
T *_ptr;
};

shared_ptr

不让拷贝终究不能解决问题,所以就有了shared_ptr——这个智能指针的解决办法就是使用==引用计数!==

int main()
{
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1);
(*sp1)++;
cout << *sp1 << endl;
cout << &(*sp1) << endl;
cout << &(*sp2) << endl;
return 0;
}

image-20230524153527765

==我们可以看到就可以进行正常的拷贝了!==

引用计数的实现

首先我们要先看一下引用计数应该如何实现

template<class T>
class shared_ptr
{
//...
//
private:
T *_ptr;
size_t count;//这样写就是一个典型的错误!
};

==为什么我们不能怎么写!因为这样子计数器就是互相独立的!==

image-20230524155235384

==所以我们需要一个统一的计数器!==

template<class T>
class shared_ptr
{
//...
//
private:
T *_ptr;
static size_t count;//既然如此我们搞一个静态的行不行?
};

image-20230524161154842==这就导致了只能管理一个内存块!而不是多个内存块!我们如果释放sp1,sp2后但是因为count不为0所以就不会释放!它们指向的内存块!这就已经造成了内存泄漏!==

那么我们应该怎么解决这个问题?一个资源必须匹配一个引用计数!

shared_ptr的底层实现
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr),
_pcount(new int(1))
{}

shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount)
{
++(*_pcount);
}
~shared_ptr()
{
if(--*(_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T &operator*()
{
return *_ptr;
}

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

private:
T *_ptr;
int* _pcount;
};
int main()
{
MySTL:: shared_ptr<int> sp1(new int(10));
MySTL::shared_ptr<int> sp2(sp1);

MySTL::shared_ptr<int> sp3(new int(10));
return 0;
}

image-20230524173724292

==shared_ptr的难点是赋值重装!==

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._count;
++(*_pcount);
return *this;
}

==这样写就是出现问题了!——已经出现内存泄漏了!==

image-20230524180254335

shared_ptr的缺点

线程安全问题

==如果出现shared_ptr管理的内存被两个线程同时管理!==

class shared_ptr
{
public:
//.....
//我们可以写一个函数来获取_pcount的值
int use_count()
{
return *_pcount;
}
private:
T *_ptr;
int* _pcount;
};

void test_shared_ptr()
{
int n = 1000;
shared_ptr<int> sp(new int(1));
thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
shared_ptr<int> sp2(sp);
}
})
;

thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
shared_ptr<int> sp3(sp);
}
})
;
t1.join();
t2.join();
}
int main()
{
test_shared_ptr();
}

image-20230525170718526

==上面程序运行的结果!——什么都有可能!甚至还有可能报错!==

image-20230525172025808

总结

shared_ptr本身是线程安全的!(拷贝和析构,进行引用计数的++/--是线程安全的!)但是==shared_ptr管理资源的访问不是线程安全的!用的时候我们要自己手动加锁保护!==

std库里面的实现的比我们更加的复杂但是也是一样的!

循环引用

shared_ptr的另一个死穴就是循环引用的问题!

struct ListNode
{
int _data;
ListNode* _next;
ListNode* _prev;

~ListNode()//写这个意义在于看节点有没有释放!
{
cout << ~ListNode() << endl;
}
};
void test_shared_ptr_tow()
{
/* ListNode* n1 =new(ListNode);
ListNode* n2 =new(ListNode);
n1->_next = n2;
n1->_prev = n1;
delete n1;
delete n2;*/


//我们如何将上面的改成智能指针呢?
shared_ptr<ListNode> spn(new ListNode);
shared_ptr<ListNode> spn2(new ListNode);

spn->_next = spn2;
spn2->_prev = spn;
//这样写是错误的!因为spn是只能指针类型!
//但是spn->_next/_prev都是原生指针类型!类型不匹配!
}

image-20230527170619759

//我们可以修改一下_prev和_next的类型!
struct ListNode
{
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;

~ListNode()//写这个意义在于看节点有没有释放!
{
cout << ~ListNode() << endl;
}
};
void test_shared_ptr_tow()
{
shared_ptr<ListNode> spn(new ListNode);
shared_ptr<ListNode> spn2(new ListNode);

spn->_next = spn2;
spn2->_prev = spn;
}

image-20230527171047998

==我们发现了一个问题!——为什么没有释放?==

void test_shared_ptr_tow()
{
shared_ptr<ListNode> spn(new ListNode);
shared_ptr<ListNode> spn2(new ListNode);

/*spn->_next = spn2;
spn2->_prev = spn;*/

//如果删掉这两句就可以了!
}

image-20230527171315325

==因为刚刚那两句造成了循环引用!(而循环引用则导致了内存泄露的发生!)==

image-20230527173957782

==这就形成了一个死结!_next要被销毁!就要先让 _prev先被销毁! _prev要被销毁首先要 _next先被销毁!==

weak_ptr

==weak_ptr的作用就是解决shared_ptr的循环引用的问题!==

image-20230529111226352

==weak_ptr不支持的指针的构造!说明了weak_ptr不能独立进行管理!——weak_ptr是不支持RAII的!==

但是是支持shared_ptr的构造!

weak_ptr是可以进行指向资源,也可以访问资源!但是不进行管理资源!——不会增加引用计数!

struct ListNode
{
int _data;
//std::shared_ptr<ListNode> _next;
//std::shared_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()//写这个意义在于看节点有没有释放!
{
cout << ~ListNode() << endl;
}
};
void test_shared_ptr_tow()
{
std::shared_ptr<ListNode> spn(new ListNode);
std::shared_ptr<ListNode> spn2(new ListNode);

spn->_next = spn2;
spn2->_prev = spn;

cout << spn.use_count() << endl;
cout << spn2.use_count() << endl;
}

image-20230529112100607

weak_ptr的底层实现
template<class T>
class shared_ptr
{
public:
//....
T* get()const
{
return _ptr;
}
//...
private:
T *_ptr;
int* _pcount;
mutex* _pmut;
};

template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}

weak_ptr(const shared_ptr<T>& sp)//这个是重点!
:_ptr(sp.get())//因为不在同一个的类里面所我们要在shared_ptr里面写一个get
{
}

weak_ptr<T> operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}

weak_ptr<T> operator=(const shared_ptr<T>& sp)//这个也是重点
{
_ptr = sp.get();
return *this;
}
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}

private:
T *_ptr;
//库里面的也是需要计数的!因为要记录这个weak_ptr是否失效!
//我们这里只是简略的实现一个!
};

==样子最简单的一个weak_ptr就完成了!==

定制删除器

==上面的我们都没有考虑到一个问题==

share_ptr<int> sp(new int[10]);
//遇到这种情况我们应该怎么办?
//我们里面调用的是delete!但是对于数组我们一般要求delete[] 如果不匹配对于内置类型!
//那么也没有什么问题!如果是自定义类型那么就会导致内存泄漏!甚至会程序崩溃!
//所以我们该如何解决这个问题?

image-20230529130434081

==所以我们需要使用定制删除器来解决这个问题我们可以看一下STL库中的定制删除器是是什么样子==

image-20230529131510343

//定制删除器
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
delete[] ptr;
cout << delete[] << endl;//这个是方便我们用来看的
}
};

==其实这个定制删除器听上去很高大上!但是本质就是一个仿函数!==

int main()
{
std::shared_ptr<int> sp1(new int[10],DeleteArray<int>());
std::shared_ptr<std::string> sp2(new std::string[10],DeleteArray<std::string>());
std::shared_ptr<std::string> sp3(new std::string[10],
[](const string* ptr)
{delete[] ptr; cout << delete[] << endl;});//或者我们可以使用lambda表达式!(lambda表达式的底层也是一个仿函数)
return 0;
}

image-20230529131414992

std::shared_ptr<FILE> sp3(fopen(test.txt, wb), [](FILE* fp) {fclose(fp); });

==出了上面的的释放数组!还可以去关闭文件!——这就是定制删除器的意义==

定制删除器的实现

==我们在自己的shared_ptr中的是不能像库中在构造函数的时候去传入定制删除器的!因为我们的自己实现的仅仅只是一个类,而库里面其实是由数个类去实现share_ptr的!==

template<class T>
class shared_ptr
{
public:
//....
template<class D>
shared_ptr(T* ptr, D del)//这里有一个很大的问题!
:_ptr(sp._ptr),
_pcount(sp._pcount),
_pmut(sp._pmut),
_del(del)//这样写看上去没有问题!但是!这个D是属于构造函数的!类成员中如果我们用了D类型就会报错!
{

_pmut->lock();
++(*_pcount);
_pmut->unlock();
}
//而这个删除器是析构函数要去使用的!
//所以我们没有办法在构造函数里面传入del
//库里面是使用另一个类来解决这个问题的!

//....
private:
T *_ptr;
int* _pcount;
mutex* _pmut;
D _del;//这个会报错!因为D仅仅属于构造函数!我们无法定义_del
};

==所以我们只能怎么实现==

namesapce MySYL
{
template<class T>
struct Defult_delete
{
void operator(T* ptr)
{
delete ptr;
}
}//我们可以写一个默认的模板参数!这样子就可以不用每次都传入了!
//需要释放数组的时候再传入!而不是用默认的!

template<class T,class D = Defult_delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr),
_pcount(new int(1)),
_pmut(new mutex)
{}

shared_ptr(const shared_ptr<T,D>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount),
_pmut(sp._pmut)
{
_pmut->lock();
++(*_pcount);
_pmut->unlock();
}

void release()
{
bool flag = false;
_pmut->lock();
if(--(*_pcount) == 0)
{
D del;
del(_ptr);
delete _pcount;
flag = true;
}
_pmut->unlock();
if (flag)
{
delete _pmut;
}
}
~shared_ptr()
{
release();
}
shared_ptr<T,D>& operator=(const shared_ptr<T,D>& sp)
{
if(_ptr != sp._ptr)//要考虑到地址相同的情况赋值!
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmut = sp._pmut;
_pmut->lock();
++(*_pcount);//将新指向的代码块的引用计数++
_pmut->unlock();
}
return *this;
}

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

T *operator->()
{
return _ptr;
}
int use_count()const
{
return *_pcount;
}
T* get()const
{
return _ptr;
}
private:
T *_ptr;
int* _pcount;
mutex* _pmut;
};
}
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
delete[] ptr;
cout << delete[] << endl;
}
};//手动写一个定制删除器

int main()
{
MySTL::shared_ptr<int,DeleteArray<int>> sp1(new int[10]);
MySTL::shared_ptr<std::string, DeleteArray<std::string>> sp2(new std::string[10]);
// MySTL::shared_ptr<FILE,[](FILE* ptr) {fclose(ptr); } > sp3(fopen(test.cpp, r));
//这样写就是不可以的!因为我们要传的是类型!而lambda表达式其实是一个匿名对象!
// MySTL::shared_ptr < FILE, decltype([](FILE* ptr) {fclose(ptr); }) > sp3(fopen(test.cpp, r));
//decltype能够推导表达式的类型!那么我们能不能怎么写呢?——不行!因为decltype还在运行的时候推导的!
//但是模板要求要编译时期就传入类型!
//所以我们这样实现有很多的缺陷
return 0;
}

image-20230529224548336

库里面的unique_ptr也是和我们一样使用这种方式来实现定制删除器!同样的也有一样的问题!

image-20230529225500093

struct FClose
{
void operator()(FILE* ptr)
{
fclose(ptr);
cout << fclose << endl;
}
};
int main()
{
//std::unique_ptr<FILE, decltype([](FILE* ptr) {fclose(ptr);})> up(fopen(test.cpp,r));//这样写是错误的!
std::unique_ptr<FILE, FClose> up2(fopen(test.cpp, r));//这样写是可以的!
return 0;
}

举报

相关推荐

0 条评论