内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 如果Func函数抛异常导致 delete[] p3未执行,p3没被释放,就形成了内存泄漏
delete[] p3;
}
内存泄漏类型
堆内存泄漏(Heap leak)
- 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
- 指程序使用系统分配的资源,如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
潜藏的内存泄漏问题久而久之危害严重,长期的内存泄漏可能会导致服务器挂掉。那么我们怎么保证,自己申请的空间都被释放了呢?即使程序员很仔细地释放了自己申请的内存资源,哪如果程序抛异常了呢?C++中有一种管理资源、避免内存泄露的机制,就是RAII(Resource Acquisition Is Initialization),获得资源初始化就可以防止内存泄漏。
智能指针
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术,这个对象可以是我们接下来讲的智能指针。
在申请了资源之后,拿申请的资源的指针初始化智能指针对象,这个智能指针对象构造时候将申请的内存资源保存起来(智能指针对象有一个私有的模板指针成员指向申请的空间),在这个智能指针对象析构时候去释放掉智能指针管理的资源。
RAII的好处
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
auto_ptr
智能指针是通过私有成员T*ptr
来管理内存空间,智能指针构造函数中,这个ptr是指向一个对象的指针,智能指针析构函数中将这个T*ptr
所指向的对象空间释放。从而保证了被管理对象的合理释放。
既然能叫指针,这个智能指针就要想指针一样去调用它所管理的对象。就需要实现operator*
和operator->
,而对于拷贝构造和operator=,auto_ptr使用了管理权转移的方式。
#include<iostream>
#include<vector>
#include<exception>
using namespace std;
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
public:
void Print()
{
cout << "I am a func of class A" << endl;
}
};
template<class T>
class Auto_Ptr
{
public:
Auto_Ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~Auto_Ptr()
{
if (_ptr != nullptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//拷贝构造,用sp2(sp1),让sp2指向sp1指向的空间,sp1置空,Bug
Auto_Ptr(Auto_Ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
Auto_Ptr<T>& operator=(Auto_Ptr<T>& ap)//赋值运算符重载:s2=s1
{
if (_ptr != ap._ptr)
{
if (_ptr)
delete _ptr;//先释放s2之前的空间
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
void Func()
{
A* a = new A;
Auto_Ptr<A> sp(a);
(*sp).Print();
sp->Print();
Auto_Ptr<A> p2(sp);//拷贝构造
Auto_Ptr<A> p3;
p3 = p2;//赋值运算符
p2->Print();
(*sp).Print();
}
int main()
{
Func();
system("pause");
}
A()
I am a func of class A
I am a func of class A
I am a func of class A
I am a func of class A
~A()
请按任意键继续. . .
虽然看似编译器没有报错,但是已经非法访问了。在用sp去拷贝构造s2之后,让sp._ptr=nullptr;
,让s3=s2,又将s2置空,也就是说,最后只有s3有合法访问权。这里拷贝后把ap和s2对象的指针赋空了,导致ap对象和s2悬空通过ap和s2对象访问资源时就会出现非法访问问题
unique_ptr
unique_ptr比较靠谱一点了,思路就是不让赋值和拷贝,其余思想和auto_ptr完全相同。
private:
// C++98防拷贝的方式:只声明不实现+声明成私有
UniquePtr(UniquePtr<T> const &);
UniquePtr & operator=(UniquePtr<T> const &);
// C++11防拷贝的方式:delete
UniquePtr(UniquePtr<T> const &) = delete;
UniquePtr & operator=(UniquePtr<T> const &) = delete;
shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数(引用
_pCount
类型使用int*
),用来记录该份资源被几个对象共享。 - 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源,如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源。
#include<iostream>
#include<mutex>
using namespace std;
class A
{
public:
A(){cout << "A()" << endl;}
~A(){cout << "~A()" << endl;}
public:
void Print(){cout << "I am a func of class A" << endl;}
};
template<class T>
class Shared_Ptr
{
public:
Shared_Ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pCount(new int(1))
,_mt(new mutex)
{
if (_ptr == nullptr)
*_pCount = 0;
}
~Shared_Ptr()
{
if (SubCountlock() == 0&&_ptr)//如果引用计数减1之后等于0,释放资源
{
delete _ptr;
delete _pCount;
delete _mt;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int AddCountlock()
{
_mt->lock();
(*_pCount)++;
_mt->unlock();
return *_pCount;
}
int SubCountlock()
{
_mt->lock();
(*_pCount)--;
_mt->unlock();
return *_pCount;
}
Shared_Ptr(Shared_Ptr<T>& ap)
:_ptr(ap._ptr)
,_pCount(ap._pCount)
,_mt(ap._mt)
{
//拷贝构造,当_ptr=nullptr不能拷贝
if (_ptr)
AddCountlock();
}
Shared_Ptr<T>& operator=(Shared_Ptr<T>& ap)//赋值运算符重载:s2=s1
{
if (_ptr != ap._ptr)
{
if (_ptr == nullptr&&SubCountlock()==0)
{
delete _ptr;//先释放s2之前的空间
delete _pCount;
delete _mt;
}
_ptr = ap._ptr;
_pCount = ap._pCount;
_mt = ap._mt;
AddCountlock();
}
return *this;
}
int UseCount(){ return *_pCount; }
T* Get(){ return _ptr; }
private:
T* _ptr;
int* _pCount;
mutex* _mt;
};
void Func()
{
A* a = new A;
Shared_Ptr<A> sp(a);
cout << sp.UseCount() << endl;
Shared_Ptr<A> p2(sp);
cout << sp.UseCount() << endl;
Shared_Ptr<A> p3;
p3 = p2;
cout << sp.UseCount() << endl;
}
int main()
{
Func();
system("pause");
}
A()
1
2
3
~A()
请按任意键继续. . .
-
引用计数是临界资源,会存在线程安全问题,对引用计数的操作,不是一条指令能够完成的任务(不是原子操作)。所以一个智能指针对应管理一块空间
_ptr
,一块空间需要一个引用计数_pCount
,而一个引用计数需要一个锁_mt
。 -
如果临界资源不是原子操作,1)两个线程中同时拷贝构造,可能导致引用计数出错,2)时间片可能导致引用计数错误。
-
上述操作,使用引用计数是线程安全的,但是它管理的资源不是线程安全的。意思就是
_ptr
指向的堆上的数据并不是线程安全的,如果让多线程去操纵_ptr
指向的数据,又会存在非原子性操作。
智能指针大PK
auto_ptr | unique | shared_ptr |
---|---|---|
管理权转移 | 防拷贝 | 引用计数实现拷贝 |
不建议使用,存在漏洞 | 可以使用,缺陷是不能拷贝和赋值 | 可以拷贝,存在循环引用的问题,配合使用weak_ptr就好 |
C++11和boost中智能指针的关系
- C++ 98 中产生了第一个智能指针auto_ptr.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
shared_ptr的辅助weak_ptr
shared_ptr特定场景的缺陷:循环引用
如上图所示,在双向链表中node1->next=node2;node2->prev=node1;node1资源是释放以来与node2,node2的释放依赖于node1,最终两个节点都不会释放,这就是循环引用的问题。
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
void Func()
{
shared_ptr<ListNode> node1(new ListNode);
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;
}
int main()
{
Func();
system("pause");
return 0;
}
解决这个问题C++11给出了weak_ptr这样一个辅助,解决循环引用问题,weak_ptr(不会实现RAII),专门辅助解决shared_ptr的循环引用问题,它允许用shared_ptr对象去拷贝构造weak_ptr,也允许shared_ptr赋值给weak_ptr。p1->_next=p2;p2->_pre,引用计数不增加。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
void Func()
{
shared_ptr<ListNode> node1(new ListNode);
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;
}
int main()
{
Func();
system("pause");
return 0;
}
1
1
1
1
~ListNode()
~ListNode()
请按任意键继续. . .
shared_ptr的删除器
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题,这个删除器是通过仿函数实现的,至于仿函数,不同的类中实现了不同的operator()
,不同的释放方式。
#include<iostream>
#include<memory>
using namespace std;
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
void Func()
{
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
}
int main()
{
Func();
system("pause");
return 0;
}
RAII----守卫锁
RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。
锁守卫的思想类似于一个unique_ptr,值得注意的是,这个锁守卫的类中的成员锁应该使用引用mutex& pMutex
,这是为了保证锁守卫的锁和对象的锁是同一个锁。
#include<iostream>
#include <thread>
#include <mutex>
using namespace std;
// C++11的库中也有一个lock_guard,下面的LockGuard造轮子其实就是为了学习他的原理
template<class Mutex>
class LockGuard
{
public:
LockGuard(Mutex& mtx)
:_mutex(mtx)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
LockGuard(const LockGuard<Mutex>&) = delete;
private:
// 注意这里必须使用引用,否则锁的就不是一个互斥量对象
Mutex& _mutex;
};
mutex mtx;
static int n = 0;
void Func()
{
for (size_t i = 0; i < 1000000; ++i)
{
LockGuard<mutex> lock(mtx);
++n;
}
}
int main()
{
int begin = clock();
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
int end = clock();
cout << n << endl;
cout << "cost time:" << end - begin << endl;
return 0;
}