👦个人主页:晚风相伴
👀如果觉得内容对你有所帮助的话,还请一键三连(点赞、关注、收藏)哦
如果内容有错或者不足的话,还望你能指出。
目录
智能指针的引入
内存泄漏
RAII
智能指针的使用及原理
std::auto_ptr
std::unique_ptr
std::weak_ptr
定制删除器
智能指针的引入
先看一下下面的代码
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{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;
}
分析上面的代码,我们会发现代码运行时可能会出现以下问题
- 如果p1那里new失败而抛异常
- 如果P2那里new失败而抛异常
- 如果div调用那里发生除0错误而抛异常
在上面代码中如果出现以上抛异常的情况则会导致资源得不到释放从而导致内存泄漏的问题,那么有什么好的办法可以解决吗?
别说还真有一个好办法,那就是使用智能指针。
在C++中,我们经常会使用new和delete来动态分配和释放内存。但是这种手动管理的方式如果我们忘记在最后调用delete或者在delete抛异常的话就很容易出现一些内存泄漏等问题,另外,如果我们多次释放同一块内存或者在释放内存后继续使用这块内存,就会引起运行时错误。所以为了解决这里的这些问题,C++就引入了智能指针的概念。
智能指针的概念
智能指针是一种特殊的数据类型,其目的是管理动态分配的资源,尤其是堆内存。智能指针是用类模板的方式实现的并且使用RAII(资源获取即初始化)技术来确保资源的正确释放,从而避免内存泄漏和野指针的问题。
内存泄漏
在介绍智能指针之前先来了解一下什么是内存泄漏以及内存泄漏的危害。
内存泄漏是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该内存的控制,从而造成了内存的浪费。
内存泄漏的危害:如果长期运行的程序出现内存泄漏,影响会很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
可以下面这段代码直观体会一下内存泄漏的危害
int main() {char* p = new char[1024 * 1024 * 1024];cout << (void*)p << endl;return 0; }
在代码没有运行起来时,我的内存是这么多。
在代码运行起来后,一瞬间我的内存就少了1个G。
可见如果代码中有内存泄漏的问题将会发生很严重的后果。
👍RAII
RAII(Resource Acquisition Is Initialization 资源获取即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的声明周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给一个对象。这种做法有两大好处:
- 不需要显示地释放资源。
- 采用这种方式对象所需的资源在其生命周期内始终保持有效。
采用RAII的思想来设计一个SmartPtr
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "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> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl;}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
这样上面的抛异常所带来的内存泄漏问题就能迎刃而解了
智能指针的使用及原理
在上面利用RAII思想设计的SmartPtr还不能称作上智能指针,因为上面的SmartPtr只是实现了构造和析构函数,还没有做到像一个指针一样支持解引用和->访问。
因此基于这样的需求我们就需要使用operator重载来实现相关功能。
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}//解引用T& operator*(){return *_ptr;}//->引用T* operator->(){return _ptr;}
private:T* _ptr;
};int main()
{SmartPtr<pair<string, int>> sp1(new pair<string, int>("print", 1));cout << sp1->first << ":" << sp1->second << endl;return 0;
}
👊std::auto_ptr
auto_ptr文档介绍
auto_ptr是C++98版本提供的智能指针,但是auto_ptr设计的并不好,导致很多人对它的意见很大以及很多公司都明确规定不去使用它。
auto_ptr的原理其实是使用了一个管理权转移的思想。先来看看库里面auto_ptr是怎么回事,然后我们就自己动手来简单实现一下auto_ptr。
class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};void test_auto_ptr()
{std::auto_ptr<A> ap1(new A);ap1->_a1++;ap1->_a2++;std::auto_ptr<A> ap2(ap1);ap1->_a1++;ap1->_a2++;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;std::auto_ptr<A> ap3(new A);ap2 = ap3;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;
}
可以看出auto_ptr在进行拷贝时其实是用了一个管理权转移的思想,将ap1对A的管理权转移到了ap2,之后再将ap1置空,所以上面程序再往下运行就会崩溃。它的赋值操作也是采用同样的思想。
知道了怎么回事之后下面我们自己就动手实现一下吧
namespace hjx
{//auto_ptr拷贝和赋值时本质就是管理权转移//被拷贝的对象出现悬空问题template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap)//不能是自己{if (_ptr)//判断一下是否为空{cout << "Delete:" << _ptr << endl;//方便观察结果delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;//方便观察结果delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
🔥std::unique_ptr
unique_ptr文档介绍
unique_ptr对象拥有对动态分配的内存资源的唯一所有权,不能进行拷贝构造和赋值操作。
下面就简单模拟实现一下unique_ptr来了解它的原理吧
namespace hjx
{//unique_ptr不支持拷贝和赋值template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//使用C++11中的delete将拷贝构造和赋值禁掉unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(unique_ptr<T>& up) = delete; ~unique_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
🔥std::shared_ptr
shared_ptr文档介绍
shared_ptr原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享,在对象被销毁时,其引用计数减一。如果引用计数减为0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果还没减为0,就说明除了自己还有其它对象在使用该资源,不能使用该资源,否则其它对象就被成野指针了。
下面我们就简单模拟实现一下shared_ptr吧
namespace hjx
{//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)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;delete _pCount;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//考虑自己给自己赋值的情况if (_ptr != sp._ptr){//减减被赋值对象的计数,如果是最后一个对象,要释放资源Release();//共同管理新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};
}
但是shared_ptr也并不是完美的,它如果碰到循环引用的情况就会出现问题了
class Node
{
public:int _val;std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;}
};void test_shared_ptr()
{//循环引用问题std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}
从打印结果来看,它并没有完成析构的一个操作
下面几幅图就很好的诠释了循环引用的问题
❔该如何解决这里的问题呢?那就需要用到下面即将要介绍的weak_ptr了
🔥std::weak_ptr
weak_ptr文档介绍
weak_ptr不是常规的智能指针,没有RAII,不支持直接管理资源,它被设计出来主要是和shared_ptr搭配使用,解决循环引用的问题。
使用weak_ptr时不会增加引用计数,所以我们就可以将上面问题中的_prev和_next换成weak_ptr就能解决问题了。
class Node { public:int _val;std::weak_ptr<Node> _next;std::weak_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;} };void test_shared_ptr() {std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl; }
下面就来简单模拟实现一下weak_ptr吧
namespace hjx
{//weak_ptr是个辅助型智能指针,用来解决shared_ptr循环引用问题template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}//拷贝和赋值不增加引用计数weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>& operator=(const weak_ptr<T>& wp){_ptr = wp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
👀定制删除器
当我们动态开辟一块数组空间时(如new int[10]),我们必须得使用delete[]来释放空间,否则程序将会崩溃。
例如下面的例子
class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};int main()
{std::shared_ptr<A> sp2(new A[10]);//程序崩溃return 0;
}
因为shared_ptr默认使用的是delete来释放空间
其实当我们使用new来动态开辟一块数组空间时,会多开辟4个字节的空间用来存放这块空间的大小,而我们的sp2正好是指向的这4个字节的空间,当使用delete来释放空间时sp2不会跳过这4个字节的空间,所以导致类型不匹配,进而导致程序就崩溃了。
所以我们的shared_ptr还要设计一个仿函数删除器来解决这里的问题。
template<class T>struct Delete{void operator()(T* ptr){delete ptr;}};//shared_ptr采用引用计数的方式template<class T, class D = Delete<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)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){/*cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;*/D()(_ptr);delete _pCount;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//考虑自己给自己赋值的情况if (_ptr != sp._ptr){//减减被赋值对象的计数,如果是最后一个对象,要释放资源Release();//共同管理新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};//仿函数删除器template<class T>struct DeleteArray{void operator()(T* ptr){//cout << "delete[]" << ptr << endl;//可不打印delete[] ptr;}};template<class T>struct Free{void operator()(T* ptr){//cout << "free[]" << ptr << endl;//可不打印free(ptr);}};