在传统的C++中,我们通常使用new和delete来手动分配和释放内存。但是,手动管理内存非常容易出错,容易导致内存泄漏或者悬空指针的问题。智能指针的出现就解决了这个问题。
除了常规的忘记释放内存外,在C++引入了异常后,内存泄露的问题愈发严重,大家可以看看下面这段代码,当我输出第二个参数为0,产生整数除以0的异常后,会发生什么问题:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
cout << "p1、p2释放" << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
乍一看好像没什么问题,异常被捕获了,并且输出了我们想要的错误信息。
但是p1、p2指向的空间因为异常的跳转,其指向的空间没有得到释放,引发了经典的内存泄漏问题。
什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏是指针丢了还是内存丢了?
内存泄漏是指在程序运行过程中,动态分配的内存没有被正确释放,导致无法再被使用或者回收。一般情况下,内存泄漏是指程序中的指针丢失了对应的内存地址,从而导致无法释放这块内存。
C/C++程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
检测工具内部原理:
申请内存用一个容器记录下来,释放内存时,从容器中删除掉。程序结束时,或者没有任务时,容器中的资源可能就是内存泄漏的。
总结一下:
内存泄漏非常常见,解决方案分为两种:
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
即在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
例如现在,我们使用RAII思想设计释放资源的类:SmartPtr,来解决刚刚的异常跳转问题。
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
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 (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
正常输入的结果:
出现 了异常,出了作用域可以正常调用析构函数来释放空间
这便是RAII的设计思路,也是智能指针核心设计思想之一,即使用对象管理指针,出作用域自动调用析构函数来释放空间。
但上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->
去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将*
、->
重载下,才可让其
像指针一样去使用。
//1、使用RAII思想设计delete资源的类
//2、像指针一样的行为
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//运算符重载两个操作符 -- 模拟指针的操作
T& operator* ()
{
return *_ptr; //返回_ptr的指向
}
T* operator->() //有两个->,其中一个被省略了。
{
return _ptr; //返回指针
}
private:
T* _ptr;
};
总结一下智能指针的原理:
operator*
和opertaor->
,具有像指针一样的行为。上面我们基本实现了smartPtr,但是smartPtr的一大痛点就是拷贝,现在使用smartPtr默认的拷贝构造函来看看smart能不能进行拷贝:
直接就出现了报错,原因如下:
因为我们没有编写构造函数,所以编译器使用默认构造函数,这就导致其中的_ptr指向了同一空间,在出作用域时,会调用析构函数,这就导致对同一空间的二次释放,导致了报错。
现在我们就再来解决智能指针拷贝构造的问题:
来看看C++98版本的库中的auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
关于auto_ptr的文档解释:std::auto_ptr文档
auto_ptr的实现原理:
接下来我们使用库中的auto_ptr来实现拷贝,来观察库中的auto_ptr的拷贝构造做了什么事:
我们发现:
这里给出auto_ptr拷贝构造函数的实现:
//auto_ptr拷贝的实现--简单的赋值
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr=nullptr
}
而auto_ptr的赋值运算符重载同样也是资源转移,例如以下代码:
而auto_ptr的赋值运算符重载的实现是怎样的呢?
auto_ptr的赋值,会先将被赋值对象指向的内容进行释放,然后进行赋值操作,再将赋值对象的指向置为空,实现如下:
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr,接下来我们看看常用的三种智能指针。
在C++98中推出auto_ptr,一直饱受诟病,在C++11中推出了三种功能较完善的智能指针,这也是实际中使用比较频繁的三种智能指针。
C++11版本的库开始提供更靠谱的unique_ptr。
unique_ptr的实现原理:
使用场景:
以下是C++11中对unique_ptr对于拷贝构造函数与赋值重载函数的处理:
那C++98没有delete,scoped_ptr(unique_ptr的前身)是如何实现的呢?
C++98中对于scoped_ptrde 拷贝构造函数和赋值重载函数的处理:声明但不实现+私有化
简易版的unique_ptr的实现步骤如下:
*
和->
运算符进行重载,使unique_ptr对象具有指针一样的行为。=delete
,防止外部调用。代码如下:
namespace dianxia
{
template<class T>
class unique_ptr
{
public:
//RAII
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//防拷贝
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr& operator=(unique_ptr<T>& up) = delete;
private:
T* _ptr; //管理的资源
};
}
引用计数
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。
++
,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--
。0
时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:
int main()
{
cl::shared_ptr<int> sp1(new int(1));
cl::shared_ptr<int> sp2(sp1);
*sp1 = 10;
*sp2 = 20;
cout << sp1.use_count() << endl; //2
cl::shared_ptr<int> sp3(new int(1));
cl::shared_ptr<int> sp4(new int(2));
sp3 = sp4;
cout << sp3.use_count() << endl; //2
return 0;
}
说明一下: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。
shared_ptr的模拟实现
简易版的shared_ptr的实现步骤如下:
++
。--
(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++
。--
,如果减为0则需要将该资源释放。*
和->
运算符进行重载,使shared_ptr对象具有指针一样的行为。代码如下:
namespace dianxia
{
template<class T>
class shared_ptr
{
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr) //管理同一块空间的对象之间无需进行赋值操作
{
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr; //与sp对象一同管理它的资源
_pcount = sp._pcount; //获取sp对象管理的资源对应的引用计数
(*_pcount)++; //新增一个对象来管理该资源,引用计数++
}
return *this;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
};
}
为什么引用计数需要存放在堆区?
首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
如下图:
其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
如下图:
而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
如下图:
但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。
shared_ptr看着很完美,但是其存在一个循环引用的问题,我们来看看循环引用是怎么出现的吧~
首先我们要引入一下双向链表中节点:
struct Node
{
int _val;
Node* _next;
Node* _prev;
~Node()
{
cout << "~Node()" << endl;
}
};
这是使用普通指针的写法,现在我们将其中的普通指针全部改为shared_ptr:
struct shared_Node
{
int _val;
std::shared_ptr<shared_Node> _next;
std::shared_ptr<shared_Node> _prev;
~shared_Node()
{
cout << "~shared_Node()" << endl;
}
};
这样一个由智能指针创建的双向链表节点就构建好了。
然后我们创建两个节点将其链接起来。
void test_shared_ptr2()
{
std::shared_ptr<shared_Node> sn1(new shared_Node);
std::shared_ptr<shared_Node> sn2(new shared_Node);
sn1->_next = sn2;
sn2->_prev = sn1;
}
此时没有任何问题,接下来我们运行一下观察结果:
发现,为什么我们使用shared_ptr在程序结束的时候没有调用shared_ptr的析构函数?按理来说控制台应该会输出两行 “~shared_Node()” 才对。
接下来我们画图来分析一下两个节点的关系:
然后当我们程序结束,sn1和sn2智能指针调用各自的析构函数后,其计数器的改变为:
所以,循环引用导致资源未被释放的原因:
而如果连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因。
解决循环引用问题
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。比如:
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//...
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。
weak_ptr的模拟实现
简易版的weak_ptr的实现步骤如下:
代码如下:
namespace dianxia
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
//可以像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr; //管理的资源
};
}
说明一下: shared_ptr还会提供一个get函数,用于获取其管理的资源。
定制删除器的用法
当智能指针对象的生命周期结束时,所有的智能指针默认都是以delete的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new方式申请到的内存空间,智能指针管理的也可能是以new[]的方式申请到的空间,或管理的是一个文件指针。比如:
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10]); //error
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r")); //error
return 0;
}
这时当智能指针对象的生命周期结束时,再以delete的方式释放管理的资源就会导致程序崩溃,因为以new[]的方式申请到的内存空间必须以delete[]的方式进行释放,而文件指针必须通过调用fclose函数进行释放。
这时就需要用到定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数:
template <class U, class D>
shared_ptr (U* p, D del);
参数说明:
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。
因此当智能指针管理的资源不是以new
的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器。比如:
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10], DelArr<ListNode>());
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
});
return 0;
}
定制删除器的模拟实现
定制删除器的实现问题:
delete
的方式释放资源。代码如下:
namespace dianxia
{
//默认的删除器
template<class T>
struct Delete
{
void operator()(const T* ptr)
{
delete ptr;
}
};
template<class T, class D = Delete<T>>
class shared_ptr
{
private:
void ReleaseRef()
{
_pmutex->lock();
bool flag = false;
if (--(*_pcount) == 0) //将管理的资源对应的引用计数--
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
_del(_ptr); //使用定制删除器释放资源
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;
}
}
//...
public:
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
, _del(del)
{}
//...
private:
T* _ptr; //管理的资源
int* _pcount; //管理的资源对应的引用计数
mutex* _pmutex; //管理的资源对应的互斥锁
D _del; //管理的资源对应的删除器
};
}
这时我们模拟实现的shared_ptr就支持定制删除器了,但是使用起来没有C++标准库中的那么方便。
template<class T>
struct DelArr
{
void operator()(const T* ptr)
{
cout << "delete[]: " << ptr << endl;
delete[] ptr;
}
};
int main()
{
//仿函数示例
cl::shared_ptr<ListNode, DelArr<ListNode>> sp1(new ListNode[10], DelArr<ListNode>());
//lambda示例1
cl::shared_ptr<FILE, function<void(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
});
//lambda示例2
auto f = [](FILE* ptr){
cout << "fclose: " << ptr << endl;
fclose(ptr);
};
cl::shared_ptr<FILE, decltype(f)> sp3(fopen("test.cpp", "r"), f);
return 0;
}
智能指针常见考点:
忘记释放/异常跳转安全
将资源交给对象去管理
智能指针的发展历史
auto_ptr、unique_ptr、shared_ptr、weak_ptr之间的区别和使用场景
模拟实现各种智能指针
什么是循环引用,如何解决?解决的原理是什么?
为什么要使用定制删除器,写个demo。