目录
1. 为什么需要智能指针?
我们先看一看下面这段代码会不会出现什么问题?
如果p1的new抛异常,那么不会有什么问题,直接main函数捕获即可,因为其他的语句还没开始执行;如果p2的new抛异常,那么会直接跳转到main函数执行流中找匹配类型的catch,由于没有匹配的类型,那么异常程序会终止;如果div函数内部抛异常,那么它会直接跳转到main函数中寻找该异常匹配的类型,然后去执行catch里的内容,而由于抛异常导致执行流跳过Func函数后面的部分,所以p1,p2指针指向的空间没有被释放,导致了内存泄漏。
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;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2. 内存泄漏
2.1 什么是内存泄漏,内存泄漏的危害
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
- 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
2.2 内存泄漏分类
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一
块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分
内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
- 系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放
掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
2.3 如何避免内存泄漏
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄
漏检测工具。
3.智能指针的使用及原理
3.1 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。也就是说资源的生命周期和对象的生命周期绑定了。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
我们就可以用这种方式去解决刚开始提出的问题。
template<class T>
class SmartPtr
{
public:
//RAII
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << _ptr << endl;
if(_ptr)
delete _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](const int n)
{
return *(_ptr + n);
}
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);
*sp2 = 10;
sp2[0]--;
cout << *sp2 << endl;
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。
我们这里虽然实现了智能指针,但是有一个严重的问题需要处理,如果我们将一个智能指针对象拷贝给另一个对象,因为我们没有实现拷贝构造,编译器会默认生成,他们两个指向同一块空间,析构时就会析构两次,那么就会出错。
那么有人说我们自己实现一个拷贝构造不就完了,答案是不行,如果只是深拷贝就简单了。这里我们的智能指针本就是模拟指针,那么两个指针指向同一块空间不是很正常吗?怎么办呢?
3.2 std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
那么auto_ptr是怎么解决这里的问题呢?他这里是资源管理权转移的思想,也就是说,本来两个智能指针对象不能同时指向一块空间,那么好,本来这块空间是我管理的,你把我拷贝给你,那么你就指向这块空间,我不指向了,我变成空了。这里如果不熟悉的人使用就会有空指针解引用的问题。
这个实现很简单,将外部传来的对象内的指针置空即可。
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
我们用调试的方式去看,sp1指向的空间给了sp2后,sp1内的指针就成空指针了。
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
3.3 std::unique_ptr
在C++11中又提供了几个智能指针。
那么unique_ptr是怎么解决这个问题的呢?它直接简单粗暴的防拷贝。
直接使用C++11中的关键字delete。delete:禁止生成默认函数。
unique_ptr(const unique_ptr<T>& up) = delete;
3.4 std::shared_ptr
那么shared_ptr是怎么解决这个问题的呢?是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1.shared_ptr在其内部,给每个资源都维护了着一份计数器,用来记录该份资源被几个对象共
享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class shared_ptr
{
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
//sp1 = sp2
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (up._ptr != _ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](const int n)
{
return *(_ptr + n);
}
private:
T* _ptr;
int* _pcount;
};
通过这种方式解决了指针拷贝的问题,但是又存在了新的问题,如果多个线程同时使用一个智能指针,就会出现数据不一致问题。
看下图,我们用两个线程对sp1进行拷贝,析构,正常情况下引用计数应该是1,但是循环变大的情况下,引用计数出现了异常,甚至会出现报错情况,这是由于线程引起的线程安全问题,那么怎么处理呢?
那么就需要线程在对引用计数++--的时候是互斥的,也就是说当前只能有一个线程进行++或--,那么就需要互斥锁对引用计数进行保护。当然我们要想让两个线程看到同一个锁,就需要让锁像引用计数一样开辟空间,让线程同时指向这个锁。线程会有一篇文章专门讲,这里就大概说一说。
template<class T>
class shared_ptr
{
public:
//RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmutex(new mutex)
{}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
, _pmutex(sp._pmutex)
{
_pmutex->lock();
++(*_pcount);
_pmutex->unlock();
}
//sp1 = sp2
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (sp._ptr != _ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmutex = sp._pmutex;
_pmutex->lock();
++(*_pcount);
_pmutex->unlock();
}
return *this;
}
void release()
{
bool flag = false;
_pmutex->lock();
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
flag = true;
}
_pmutex->unlock();
if (flag)
{
delete _pmutex;
}
}
int Pcount()const
{
return *_pcount;
}
T* getPtr()const
{
return _ptr;
}
//像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](const int n)
{
return *(_ptr + n);
}
private:
T* _ptr;
int* _pcount;
mutex* _pmutex;
};
那么此时相同的代码就不会出现之前的问题了。
那么shared_ptr智能指针是线程安全的吗?
shared_ptr本身是线程安全的,这里的指的是在拷贝和析构的时候引用计数++--是线程安全的,但是它管理资源的访问不是线程安全的,需要使用的地方自行保护。
加上锁后问题就解决了。
但是shared_ptr的问题还没结束,还有一个致命的问题。
std::shared_ptr的循环引用
这个问题就是shared_ptr的循环引用,shared_ptr对象出了作用域调用析构函数本该释放对应的空间,但是却没有释放导致内存泄漏。
那么究竟是什么原因导致的呢?
正常的链表中有两个原生指针指向前一个节点和后一个节点,我们释放某个节点也就会将该节点和它的两个原生指针都从链表中拿走,不会有痕迹存在链表中。
而shared_ptr中,我们不能直接拿原生指针指向sp1或sp2,那么就需要prev和next都为shared_ptr对象才能像链表指针一样,才能去指向前一个或者后一个节点,那么问题就出现在这里,由于prev和next都为shared_ptr对象,那么sp1的next指向sp2时,sp2的引用计数就会变成2,同理sp2的prev指向sp1,sp1的引用计数也会变成2,等到析构sp1和sp2,只会将他们两个的引用计数--,因为引用计数并不是0,所以空间不会被释放。
由于next是sp1的成员,只有sp1的空间被释放了,next才会被析构,那么要想sp1的空间被释放,就必须让sp2的prev被析构,sp2的prev要想被析构,就必须释放sp2的空间,要想sp2的空间被释放,就必须析构sp1的next.......这样就陷入一个死循环中了,sp1和sp2的空间就都不会被释放了。
那么怎么解决呢?
我们把shared_ptr的_prev和_next改成weak_ptr就可以了。
这里还有一个问题需要注意,我们new一个对象数组,会发现程序崩溃了,这是因为啥?
因为底层基本都是delete去释放,拿delete去释放数组空间(应该delete[]),由于不匹配当然会崩溃。
那么怎么解决?
其实shared_ptr设计了一个定制删除器来解决这个问题,我们自己去显示的传入仿函数对象或者lambda表达式,这样就可以通过我们的方法去释放空间。
不过这种方式只有库里的shared_ptr可以这样使用,我们自己实现的没有库里的那么复杂,所以要想实现这种方法,需要换一种方式。
需要给shared_ptr多加一个模板参数,定义一个D类型的成员,再把release中释放空间的地方一换,当然需要提供一个默认的释放方式,不然的话,不显示传递释放方式这个shared_ptr就用不了。如果需要传递就传递,不需要就可以不传递。
//默认的释放方式
template<class T>
class defualt_delete
{
public:
void operator()(T* ptr)
{
delete ptr;
cout << "delete :" << ptr << endl;//方便观看结果
}
};
//需要给shared_ptr多加一个模板参数
template<class T,class D = defualt_delete<T>>
class shared_ptr
{
....//其他的可以不动,修改一下release
void release()
{
bool flag = false;
_pmutex->lock();
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);//可以用我们传的方式释放空间
delete _pcount;
flag = true;
}
_pmutex->unlock();
if (flag)
{
delete _pmutex;
}
}
private:
T* _ptr;
int* _pcount;
mutex* _pmutex;
D _del;
};
这样就都可以很好的使用了,不过需要传递自己的释放方式就得在参数的地方传了,那么lambda表达式就用不了。
unique_ptr的方式是跟我们一样的。
4.C++11和boost中智能指针的关系
1. C++ 98 中产生了第一个智能指针auto_ptr.
2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost
的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
完整代码:智能指针/智能指针/SmartPtr.h · 晚风不及你的笑/作业库 - 码云 - 开源中国 (gitee.com)