引言
智能指针是RAII思想的体现,有时候程序抛异常导致指针指向的内存资源未释放,造成内存泄漏,这时就需要用到智能指针,它可以出作用域自动调用析构函数释放内存资源
内存泄漏
什么是内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
解决方案
1.事前预防:采用RAII即智能指针
2.事后补救:用内存泄漏检测工具。
智能指针
template<class T>class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~SmartPtr(){delete _ptr;}private:T* _ptr;
};int div()
{int a, b;std::cin >> a >> b;if (b == 0)throw std::exception("division zero error");return a / b;
}void Func()
{SmartPtr<int> sp1(new int);//把申请的资源交给sp管理SmartPtr<int> sp2(new int);std::cout << div() << std::endl;
}int main()
{try{Func();}catch (std::exception& e){std::cout << e.what() << std::endl;}return 0;
}
智能指针的拷贝构造
auto_ptr
采用管理权转移的方式避免浅拷贝同一空间被析构两次
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
问题:
采用管理权转移方式,看似没问题,只让对象析构一次,另一个指针指向空。但会存在拷贝对象悬空问题,即再对拷贝对象解引用是对空指针解引用。
unique_ptr
采用delete关键直接禁止拷贝
shared_ptr
采用引用计数的方式,支持拷贝
template<class T>class SharedPtr
{
public:SharedPtr(T* ptr):_ptr(ptr),_pcount(new int(1)){}SharedPtr(const SharedPtr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){AddCount();}SharedPtr<T>& operator=(const SharedPtr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;AddCount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}void Release(){if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;delete _pcount;}}void AddCount(){++(*_pcount);}~SharedPtr(){Release();}private:T* _ptr;int* _pcount;
};void test1()
{SharedPtr<int> sp1(new int(1));SharedPtr<int> sp2(sp1);SharedPtr<int> sp4(new int(10));//sp4 = sp1;sp1 = sp4;
}
int main()
{test1();return 0;
}
多线程实现shared_ptr
template<class T>class SharedPtr
{
public:SharedPtr(T* ptr):_ptr(ptr),_pcount(new int(1)),_lock(new std::mutex){}SharedPtr(const SharedPtr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_lock(sp._lock){AddCount();}SharedPtr<T>& operator=(const SharedPtr<T>& sp){if (_ptr != sp._ptr){Release();_ptr = sp._ptr;_pcount = sp._pcount;_lock = sp._lock;AddCount();}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}void Release(){_lock->lock();bool deleteflag = false;if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;delete _pcount;deleteflag = true;}_lock->unlock();if (deleteflag){delete _lock;}}void AddCount(){_lock->lock();++(*_pcount);_lock->unlock();}T* Get(){return _ptr;}int use_count(){return *_pcount;}~SharedPtr(){Release();}private:T* _ptr;int* _pcount;std::mutex* _lock;
};void test1()
{SharedPtr<int> sp1(new int(1));SharedPtr<int> sp2(sp1);SharedPtr<int> sp4(new int(10));//sp4 = sp1;sp1 = sp4;
}struct Date
{int _year;int _month;int _day;
};void Func(SharedPtr<Date>& sp, size_t n, std::mutex& mtx)
{std::cout << sp.Get() << std::endl;for (size_t i = 0; i < n; i++){SharedPtr<Date> copy(sp);mtx.lock();sp->_year++;sp->_day++;sp->_month++;mtx.unlock();}}
void test2()
{SharedPtr<Date> p(new Date);std::cout << p.Get() << std::endl;const size_t n = 10000;std::mutex mtx;std::thread t1(Func, std::ref(p), n, std::ref(mtx));std::thread t2(Func, std::ref(p), n, std::ref(mtx));t1.join();t2.join();std::cout<<p.use_count()<<std::endl;
}
shared_ptr的循环引用问题
看下面这段代码
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);n1->_next=n2;
n2->_prev=n1;
当node2析构时,引用计数变为1,因为还有node1的next指向它,node1析构时,其引用计数变为1,因为还有node2的_prev指向它。为何此时node2 的prev还在呢?因为node2的引用计数不为0,node2的prev要等node2析构才会释放,node2的析构又由node1的next决定。从而造成了循环引用,导致内存泄漏。
解决方案
1.把prev去掉,只保留next。即把双向链表改为单链表
2.weak_ptr
weak_ptr
不支持RAII,专门设计来辅助解决shared_ptr的循环引用问题
把ListNode类内对象定义为weak_ptr,不增加引用计数。就能完成对应的析构。
自定义删除器
struct Date
{int _year=0;int _month=0;int _day=0;~Date(){}
};
void test4()
{std::shared_ptr<Date> spa(new Date[10]);
}
new[]开空间时编译器会在数组开头多开4个字节,存要调用析构函数的个数。如果delete不加[]就会造成程序崩溃,因为删除位置不对
可调用对象
函数指针,函数对象,lambda表达式都可以。
这样就不是默认的delete释放,而是调用自定义的delete方法。
通过智能指针打开文件也会出现错误,因为shared_ptr默认调用的是delete方法,而关闭文件是fclose,因此也必须自定义删除器。