作者:@阿亮joy.
专栏:《吃透西嘎嘎》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。注:内存泄漏是指针丢了,并不是内存丢了,指针丢了就无法释放申请的内存了。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
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;
return 0;
}
C / C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
- 在 Linux 下内存泄漏检测:Linux 下几款内存泄漏检测工具
- 在 Windows 下使用第三方工具:VLD 工具说明
- 其他工具:内存泄漏工具比较
- 注:检测工具的原理:申请的内存用一个容器记录下来,释放内存时,从容器中删除掉。程序结束前或者没有任务跑时,容器中的资源可能就是内存泄漏。
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。这个是理想状态,但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
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()
{
// 不管Func正常结束还是抛异常,sp1和sp2都会调用析构函数释放资源
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过
->
去访问所指空间中的内容,因此,SmartPtr 模板类中还得需要将*
、->
重载下,才可让其像指针一样去使用。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
智能指针的原理总结: 利用 RAII 思想设计释放资源的类,该类重载了
*
和->
,其具有像指针一样的行为。
C++98版本的库中就提供了 auto_ptr 的智能指针,其在 memory 头文件中。以下是 auto_ptr 的演示使用。
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
auto_ptr<A> ap1(new A);
++ap1->_a1;
++ap1->_a2;
SmartPtr<A> sp1(new A);
++sp1->_a1;
++sp1->_a2;
return 0;
}
智能指针看起来完美无瑕,那真的是这样吗?其实智能指针最怕的就是拷贝了,因为拷贝的话可能会存在对一个指针delete
两次的问题。
我们自己写的 SmartPtr 没有写拷贝构造函数,编译器会默认生成拷贝构造函数。默认生成的拷贝构造函数对内置类型完成值拷贝,对应自定义类型则调用该类型的拷贝构造。这样就会导致一个问题:sp1 和 sp2 指向同一块空间,程序结束会对同一块空间析构两次,从而导致程序崩溃。
那怎么解决呢?那是不是深拷贝呢?其实深拷贝并不能解决这个问题,因为智能指针是保管指针的。深拷贝违背了功能需求,智能指针需要的就是浅拷贝。那 auto_ptr 是如何解决拷贝构造的问题呢?
通过上图可以看到:auto_ptr 通过管理权转移的方式来解决拷贝问题的。也就是说 ap2 拷贝了 ap1,那么 ap1 会将指针的管理权交给 ap2,ap 变成空指针。这是一种非常挫的解决方法,这样会导致被拷贝对象悬空,是非常危险的做法,只要解引用就会报错。所以,很多公司都明确地要求不能使用 auto_ptr。
auto_ptr 是 C++98 就已经有了的,而新的智能指针到了 C++11 才更新。那么在 C++11 出来之前,大家都是在使用 boost 中设计的scoped_ptr / shared_ptr / weak_ptr
,C++11 将 boost 库中智能指针精华部分吸收了过来,设计出了C++11 的unique_ptr / shared_ptr / weak_ptr
。
auto_ptr 的模拟实现
namespace Joy
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// auto_ptr的拷贝构造和赋值运算符重载
// 的实现原理都是管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
// 先释放原来的资源
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
// 再指向新资源并将ap._ptr置为nullptr
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
unique_ptr 的实现原理:简单粗暴的防拷贝。
unique_ptr 的模拟实现
namespace Joy
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// C++98的防拷贝做法:将拷贝构造和赋值运算符重载弄成私有,只声明不实现
// 设置成私有的原因是防止别人在类外实现
// C++11的防拷贝做法:delete
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
unique_ptr 非常简单粗暴,就是不让你拷贝。但是 unique_ptr 还是没有从根本上解决问题,其只适用于一些不需要拷贝的场景。
shared_ptr 是更靠谱的并且支持拷贝的智能指针。shared_ptr 的原理:是通过引用计数的方式来实现多个shared_ptr 对象之间共享资源。
- shared_ptr 在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是调用析构函数),就说明自己不使用该资源了,对象的引用计数减 一。
- 如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果引用计数不是 0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
share_ptr 的演示使用
注:以下的实现方式是错误的。
namespace Joy
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
{
++_count;
}
~shared_ptr()
{
if (--_count == 0 && _ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
shared_ptr(shared_ptr<T>& sp)
: _ptr(sp._ptr)
{
++_count;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int* GetCount() const
{
return &_count;
}
private:
T* _ptr;
static int _count; // 引用计数
};
template<class T>
int shared_ptr<T>::_count = 0;
void SharedPtrTest1()
{
Joy::shared_ptr<A> sp1(new A);
Joy::shared_ptr<A> sp2(sp1);
Joy::shared_ptr<A> sp3(sp2);
Joy::shared_ptr<int> sp4(new int);
Joy::shared_ptr<A> sp5(new A);
Joy::shared_ptr<A> sp6(sp5);
}
}
shared_ptr 的模拟实现
namespace Joy
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
++(*_pCount);
}
void Release()
{
// 减减对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 通过_ptr来判断管理的资源是否是同一个。如果是同一个,
// _ptr相等,直接返回即可
//if(this == &sp)
if (_ptr == sp._ptr)
{
return *this;
}
Release(); // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int* GetCount() const
{
return _pCount;
}
private:
T* _ptr;
int* _pCount; // 引用计数
};
void SharedPtrTest2()
{
Joy::shared_ptr<A> sp1(new A);
Joy::shared_ptr<A> sp2(sp1);
Joy::shared_ptr<A> sp3(sp2);
cout << sp1.GetCount() << endl;
cout << sp2.GetCount() << endl;
cout << sp3.GetCount() << endl;
cout << "--------" << endl;
Joy::shared_ptr<int> sp4(new int);
cout << sp4.GetCount() << endl;
cout << "--------" << endl;
Joy::shared_ptr<A> sp5(new A);
Joy::shared_ptr<A> sp6(sp5);
cout << sp5.GetCount() << endl;
cout << sp6.GetCount() << endl;
cout << "--------" << endl;
}
}
注:shared_ptr 的赋值运算符重载需要防止自己给自己赋值的情况。只要两个对象管理的资源是一样的,那么这两个对象就不要相互赋值,直接 return 即可。我们可以通过 _ptr 或者 _pCount 是否相等来判断是否管理同一个资源。
std::shared_ptr的循环引用
struct Node
{
int _val;
// 注:Node*和std::shared_ptr不是同一个类型
std::shared_ptr<Node> _prev;
std::shared_ptr<Node> _next;
~Node()
{
cout << "~Node()" << endl;
}
};
// shared_ptr的循环引用
void SharedPtrTest3()
{
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
node1->_next = node2;
node2->_prev = node1;
}
- node1 和 node2 两个智能指针对象指向两个节点,引用计数变成 1,我们不需要手动 delete。
- node1 的 _next 指向 node2,node2 的_prev 指向node1,引用计数变成 2。
- node1 和 node2 析构,引用计数减到 1,但是 _next 还指向下一个节点,_prev 还指向上一个节点。
- 也就是说 _next 析构了,node2 才能释放。
- 也就是说 _prev 析构了,node1 才能释放。
- 但是 _next 属于 node1 的成员,node1 释放了,_next 才会析构。而 node1 由 _prev 管理,_prev 属于node2 成员,node2 释放了,_prev 才会行。所以这就叫循环引用,谁也不会释放。
shared_ptr 的循环引用问题通过 weak_ptr 来解决。weak_ptr 并不是常规的智能指针,没有 RAII,不支持直接管理资源。weak_ptr 主要用 shared_ptr 来构造,用来解决 shared_ptr 的循环引用问题。
weak_ptr 调用拷贝构造和赋值运算符重载是不会增加资源的引用计数的,它不参与资源的管理。
struct Node
{
int _val;
std::weak_ptr<Node> _prev;
std::weak_ptr<Node> _next;
~Node()
{
cout << "~Node()" << endl;
}
};
// shared_ptr的循环引用
void SharedPtrTest4()
{
std::shared_ptr<Node> node1(new Node);
std::shared_ptr<Node> node2(new Node);
// 注:use_count函数返回的是引用计数的大小
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
cout << "--------" << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
cout << "--------" << endl;
}
weak_ptr 的简单模拟实现
namespace Joy
{
// weak_ptr是辅助型智能指针,主要用于解决shared_ptr的循环引用问题
template <class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
: _ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct Node
{
int _val;
// 注:Node*和std::shared_ptr不是同一个类型
Joy::weak_ptr<Node> _prev;
Joy::weak_ptr<Node> _next;
~Node()
{
cout << "~Node()" << endl;
}
};
// shared_ptr的循环引用
void SharedPtrTest5()
{
Joy::shared_ptr<Node> node1(new Node);
Joy::shared_ptr<Node> node2(new Node);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
cout << "--------" << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
cout << "--------" << endl;
}
}
shared_ptr 是有线程安全问题的,需要加锁保护。这部分内容等学习完线程部分再来补充。
如果不是 new 出来的对象如何通过智能指针管理呢?或者是 new[ ] 出来的对象如果通过智能指针管理呢?那么就需要给 shared_ptr 设计定制删除器来解决这个问题。
new 内置类型,内置类型不会去调用构造函数。new[ ] 调用的是 operator new[ ],operator new[ ] 调用的是 malloc;delete[ ] 调用的是 oprator delete[ ],operator delete[ ] 调用的是 free。因为 delete[ ] 和 delete 都是调用 free 来释放空间,所以对于内置类型不会存在太大的问题(可能一些编译器会进行是否匹配使用的检查)。而对于自定义类型,就需要看该类型有没写析构函数。如果写了析构函数,使用 new[ ] 时,VS 会在最前面多开四个字节来存储对象的个数。当你使用 delete 来释放 new[ ] 申请的空间时,就会存在释放的位置不对且析构函数少调用了的问题。因为 VS 还给你多开了四个字节的空间,来存储对象的个数。而如果你没有写析构函数,那么编译器会自动生成析构函数。编译器自己生成析构函数的话,就不会在前面多开一个字节来存储申请空间的个数,也不会去调用析构函数,所以程序就没有报错(与内置类型不报错的原因类似)。
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]:" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
// 定制删除器
void SharedPtrTest7()
{
std::shared_ptr<Node> n1(new Node[5], DeleteArray<Node>());
std::shared_ptr<Node> n2(new Node);
std::shared_ptr<int> n3(new int[5], DeleteArray<int>());
std::shared_ptr<int> n4((int*)malloc(sizeof(int)), Free<int>());
}
注:在拷贝构造时传给 shared_ptr 的定制删除器也可以是 lambda 表达式,传给 unique_ptr 的定制删除器是定制删除器的类型。
我们的改造 unique_ptr 和 shared_ptr 都是在类模板里传定制删除器的类型。
namespace Joy
{
template<class T>
struct Delete
{
void operator()(T* ptr)
{
//cout << "delete:" << ptr << endl;
delete ptr;
}
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "delete[]:" << ptr << endl;
delete[] ptr;
}
};
template<class T>
struct Free
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T, class D = Delete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// C++98的防拷贝做法:将拷贝构造和赋值运算符重载弄成私有,只声明不实现
// C++11的防拷贝做法:delete
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
D()(_ptr);
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T, class D = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
++(*_pCount);
}
void Release()
{
// 减减对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
//cout << "Delete:" << _ptr << endl;
D()(_ptr);
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 通过_ptr来判断管理的资源是否是同一个。如果是同一个,
// _ptr相等,直接返回即可
//if(this == &sp)
if (_ptr == sp._ptr)
{
return *this;
}
Release(); // 减减被赋值对象的计数,如果是最后一个对象,要释放资源
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int* GetCount() const
{
return _pCount;
}
T* get() const
{
return _ptr;
}
int use_count()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount; // 引用计数
};
void SharedPtrTest8()
{
Joy::shared_ptr<Node, DeleteArray<Node>> n1(new Node[5]);
Joy::shared_ptr<Node> n2(new Node);
Joy::shared_ptr<int, DeleteArray<int>> n3(new int[5]);
Joy::shared_ptr<int, Free<int>> n4((int*)malloc(sizeof(int)));
}
}
智能指针重点内容
- C++ 98 中产生了第一个智能指针 auto_ptr。
- C++ boost 给出了更实用的 scoped_ptr、shared_ptr 和 weak_ptr。
- C++ TR1 引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版。
- C++ 11 引入了 unique_ptr、shared_ptr 和 weak_ptr。需要注意的是 unique_ptr 对应 boost 的scoped_ptr,并且这些智能指针的实现原理是参考 boost 中的实现的。
本篇博客主要讲解了什么是内存泄漏、内存泄漏的危害及分类、什么是智能指针和 RAII、auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 以及定制删除器等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!❣️