目录
一、为什么要有智能指针?
二、说一下内存泄露?
1.什么是内存泄露,内存泄露的危害
2.内存泄露的分类
3.如何避免内存泄露?
三、智能指针的基本原理?
1.RAII思想
2.智能指针的原理
四、各种智能指针的介绍,基本实现原理以及存在的缺陷?
1.C++98当中的auto_ptr的智能指针。
2.C++11提供的unique_ptr
五、智能指针的更新迭代
C++没有内存回收的机制,所以我们开辟出来的空间需要自己手动的进行释放。
有时申请的空间我们会忘记释放,也有时会存在doubiefree的问题存在。
如果malloc/new出来的空间没有进行free/delete就会造成内存泄露的问题。
什么是内存泄露:内存泄露是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄露并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成内存的浪费。
内存泄露的危害:长期运行的程序出现内存泄露,影响很大,如操作系统、后台服务等等,出现内存泄露会导致响应越来越慢,最终卡死。
堆内存泄露:
堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存, 用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那 么以后这部分空间将无法再被使用,就会产生 Heap Leak 。
系统资源泄露:
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统 资源的浪费,严重可导致系统效能减少,系统执行不稳定。
总结:(1)事前预防型,智能指针等。(2)事后查错型,泄露检测工具
RAII ( Resource Acquisition Is Initialization )是一种 利用对象生命周期来控制程序资源 (如内存、文件句 柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制资源的访问使之在对象的生命周期内时钟保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理亦非资源的责任托管给了一个对象。这种做法有两大好处:(1)不需要显式的释放资源(2)采用这种方式,对象所需的资源在其生命期内时钟保持有效。
(1)RAII特性:在构造函数中进行资源的申请,析构函数中进行资源的释放
(2)指针的特性:重载operator*和operator->,具有指针一样的行为。
实现原理:管理权限转移的思想(它的构造函数和赋值运算符的重载实现了管理权限转移)
// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr& ap)
: _ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr& operator=(AutoPtr& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
存在的问题:
int main()
{
AutoPtr ap(new Date);
// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
// 通过ap对象访问资源时就会出现问题。
AutoPtr copy(ap);
ap->_year = 2018;
return 0;
}
我们在使用ap来构造copy之后,如果再次使用ap访问对象的资源时就会出现问题。
实现原理:防拷贝
模拟实现
class UniquePtr
{
public:
UniquePtr(T * ptr = nullptr)
: _ptr(ptr)
{}
~UniquePtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr const &);
UniquePtr & operator=(UniquePtr const &);
// C++11防拷贝的方式:delete
UniquePtr(UniquePtr const &) = delete;
UniquePtr & operator=(UniquePtr const &) = delete;
private:
T * _ptr;
};
实现原理:通过引用计数的方式实现多个shared_ptr对象之间共享资源
内部实现:
模拟实现:
template
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
~SharedPtr() { Release(); }
SharedPtr(const SharedPtr& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
// sp1 = sp2
SharedPtr& operator=(const SharedPtr& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
int UseCount() { return *_pRefCount; }
T* Get() { return _ptr; }
void AddRefCount()
{
// 加锁或者使用加1的原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release()
{
bool deleteflag = false;
// 引用计数减1,如果减到0,则释放资源
_pMutex.lock();
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
deleteflag = true;
}
_pMutex.unlock();
if (deleteflag == true)
delete _pMutex;
}
private:
int* _pRefCount; // 引用计数
T* _ptr; // 指向管理资源的指针
mutex* _pMutex; // 互斥锁
};
shared_ptr存在线程安全的问题
1.智能指针对象中引用计数时多个智能指针对象共享的,两个线程中智能指针的引用计数器同时++或--操作,这个操作并不是原子性的,假设引用技术原来是1,++了两次,可能还是1,这样引用计数器就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++,--操作是需要加锁的,也就是说引用计数的操作是线程安全的。
2.智能指针管理的对象放在堆上,两个线程同时去访问会导致线程安全的问题。
shared_ptr的循环引用
#include
#include
using namespace std;
struct ListNode
{
int _data;
shared_ptr _prev;
shared_ptr _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr 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;
}
运行结果:
由上面的运行结果我们可以知道:node1和node2都没有调用析构函数,这是为什么?
循环引用分析:双方都在等对方先释放
循环引用的解决方案:
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr _prev;
weak_ptr _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr node1(new ListNode);
shared_ptr 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;
}