前言:
在我们之前学习异常的时候,讲到过异常安全的问题,会有内存泄露的问题。
- 内存泄露这个问题对程序员的要求很高,申请的空间就必须要手动释放,不像Java这种语言自带垃圾回收器(gc)。
- 就算是我们手动释放了空间,也有可能存在内存泄露的问题(异常安全),抛异常时会乱跳,有可能就会导致即使手动释放了,也没会内存泄露。
- 上节在异常种我们可以通过拦截异常手动释放掉,但是防不胜防并不是所有的都能拦截到,于是C++就引入了智能指针。
内存泄漏是指针丢了还是内存丢了?
答:所有的内存泄露都是指针丢了。。
1.内存还在,进程正常结束,内存也会释放
2.僵尸进程有内存泄露,比较可怕。
3.服务器都是长期运行的。
目录
1 RAII思想:
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
- 在对象构造时获取资源
- 在对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
> 注意:RAII只是智能指针的一种思想,切不可说RAII就是智能指针
1.1 拦截异常解决不了的内存泄漏:
在上节中我们讲到了在抛异常时会出现内存泄漏的问题,即使是手动释放掉内存也会出现内存泄漏,原因时抛异常时直接跳到了捕获的地方,所以会泄露。
- 对于上述问题我们也给出了对应的解决办法,那就是拦截异常的方式
- 为了避免内存泄漏,我们先将异常拦截下来,先释放掉再将捕获的异常抛出
void func()
{
int* p1 = new int[10]; //这里亦可能会抛异常
int* p2 = new int[10]; //这里亦可能会抛异常 -- 这里抛异常,p1就没释放掉
int* p3 = new int[10]; //这里亦可能会抛异常
int* p4 = new int[10]; //这里亦可能会抛异常
try
{
div();
}
catch (...)
{
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
delete[] p4;
}
假设我们每个new出来的空间非常大,我们也不确定到底是哪个new失败了
所以,C++就提供了智能指针
- 利用对象生命周期的特性:出了作用域之后自动调用对象的析构函数,通过析构函数来释放空间。
- 无论如何都会正常释放资源,抛异常也好,中间抛异常也好,或者是正常结束,出了作用域就调用对象析构函数。
2. 智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将 、->重载下,才可让其像指针一样去使用*.
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为
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;}
private:
T* _ptr;
};
3 直接拷贝带来的问题:
智能指针衍生的问题~
智能指针管理资源的方式我们不难理解,但是智能指针的拷贝却是个令人头疼的问题
- 我们知道我们只是将指针封装了一层
- 如果是简单的只拷贝的话,会出两个指针指向同一块资源
- 在释放的时候会发生同一块空间释放多次的问题
智能指针最大的问题:在于拷贝构造问题**(拿一个已经有的,来可瓯北另外一个)**
3.1 auto_ptr:
核心思想:
- 管理权转移,被拷贝的对象悬空。
namespace Joker
{
template<class T>
class auto_ptr
{
public:
//RAII思想
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
//sp2(sp1) -- 拷贝构造(简直就是神人)
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
//将sp1置空,交给sp2管了,sp1不管了
sp._ptr = nullptr;
}
//ap1=ap2
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if(this!=&ap)
{
if(_ptr)
{
delete _ptr;
}
_ptr=ap._ptr;
ap._ptr=nullptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
};
}
思路:
- auto_ptr支持拷贝,但是方式很挫
- 拷贝之后直接将原来的指针给置空了
- 这要是不知情的人使用了原来指针,直接就造成非法访问
3.2 unique_ptr:
我们再来看C++11给的解决办法:
核心思想:
- 不让拷贝 / 防拷贝 — 拷贝编译就报错。
namespace Joker
{
//不能拷贝用unique_ptr
template<class T>
class unique_ptr
{
public:
//RAII思想
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
//C++98的方式:
//private:
//sp2(sp1)
//1、只声明,不实现(不声明会默认生成一个)
//2、声明成私有
//不过还是有问题,在类里面可以调用
//unique_ptr(const unique_ptr<T>& sp);
//C++11的方式:防拷贝
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
}
思想:
- unique直接不给拷贝,防止拷贝,但是功能不全
3.3 shared_ptr:
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr:
核心思想:
- 核心原理就是引用计数,记录几个对象管理这块资源,析构的时候 - -(减减)计数,最后一个析构的对象释放资源。
> 思路:
- 用到引用计数的方式,拷贝就计数++,析构就计数- -
- 最后一个析构的对象释放资源
这时,对计数这个变量(count)就有要求了,要求共同管理同一个对象的时候要做到对同一个count ++ 或 - -
- 直接定义成成员变量(不行,对象内存空间独立)
- 定义一个static 的成员变量,管理多个资源的时候,就会出问题。(静态成员变量为所有类共享,地址都是一样的)
- 在堆上开辟一段空间,引用计数放在堆上(int* count)—构造时,给count在堆上申请空间。
private:
T* _ptr;
int* _pcount;
shared_ptr的代码实现:
namespace joker
{
template<class T>
class shared_ptr
{
public:
//RAII思想
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new mutex)
{}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pmtx(sp._pmtx)
{
AddRef();
}
//引用计数释放资源专用函数
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr) %考虑 _ptr为nullptr时,不要delete了。
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pRefCount;
}
//析构函数
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx; %多线程使用shared_ptr,会出现线程安全问题
};
注意:shared_ptr会存在多线程安全问题,所以要使用互斥锁。
3.4 循环引用的问题:
我们先来看一段代码:
struct ListNode
{
Joker::shared_ptr<ListNode> _prev = nullptr;
Joker::shared_ptr<ListNode> _next = nullptr;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
Joker::shared_ptr<ListNode> p1(new ListNode);
Joker::shared_ptr<ListNode> p2(new ListNode);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
为了更好理解上面的问题,看下图:
3.5 weak_ptr:
weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源。
weak_ptr主要是接收shared_ptr构造,用来解决shared_ptr的循环引用问题。
- 其他的智能指针的构造函数可以传一个指针;
- weak_ptr构造函数不支持接收指针,不管理资源;
- 它接收一个shared_ptr,可以通过shared_ptr来构造weak_ptr
- 可以指向一块空间,但是不参与空间的管理
构造函数:
- 可以说 weak_ptr是shared_ptr的小弟 — 不是传统的智能指针
- 专门 用来辅助解决shared_ptr循环引用的问题
// 简化版本的weak_ptr实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
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
struct ListNode
{
Joker::weak_ptr<ListNode> _prev = nullptr;
Joker::weak_ptr<ListNode> _next = nullptr;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
% _next 和_prev是weak_ptr时,不参与资源释放管理,可以访问和修改到资源,但是不增加计数,不存在循环引用问题。
int main()
{
Joker::shared_ptr<ListNode> p1(new ListNode);
Joker::shared_ptr<ListNode> p2(new ListNode);
p1->_next = p2;
p2->_prev = p1;
return 0;
}
3.6 定制删除器:
通过给智能指针unique_ptr和shared_ptr传递一个可调用对象,来定制析构的具体行为
构造函数
注意:
传递给智能指针的定义删除器可调用对象,可以是仿函数,lambda表达式,函数指针等
尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦