目录
前言:
1. 智能指针的使用及其原理
1. 1 智能指针的使用场景分析
1.2 RAII和智能指针的设计思路
1.3 C++标准库智能指针的使用
1.3 1 auto_ptr
1.3 2 unique_ptr
1.3 4 weak_ptr
1.3 5 模拟实现删除器
2.智能指针的原理
2.1 3 析构函数
3.2 weak_ptr
5.C++11和boost中智能指针的关系
6.内存泄漏
6.1什么是内存泄漏,内存泄漏的危害
前言:
在c/c++程序的开发中,一些项目需要向内存手动申请空间,而手动申请的空间在我们不用时,必须手动将其释放,先不说我们其他问题,这种特有的机制,容易让我们手动向内存申请空间而忘记释放的问题。再说其他问题,如某个局部函数内部申请了资源,在它的栈帧销毁之前抛出了异常,此局部函数立即被终止了,而我们申请的资源也没有释放,这就导致了内存泄漏。所以有人提出能否把我们申请的资源交给一个对象来管理,如果我们忘记释放资源让这个对象来释放。
1. 智能指针的使用及其原理
1. 1 智能指针的使用场景分析
下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本⾝也可能抛异常,连续的两个new和下⾯的Divide都可能会抛异常,让我们处理起来很⿇烦。智能指针放到这样的场景⾥⾯就让问题简单多了。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针,否则代码太戳了
int* array1 = new int[10];
int* array2 = new int[10];
// 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
下面引入一个新的概念——RAII。
1.2 RAII和智能指针的设计思路
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是
⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指
针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,
资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常
释放,避免资源泄漏问题。
智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀
样,重载 operator*/operator->/operator[] 等运算符,⽅便访问资源。
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的⾏为,⽅便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
1.3 C++标准库智能指针的使用
C++标准库中的智能指针都在<memory>这个头⽂件下⾯,我们包含<memory>就可以是使⽤了,
智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的⾏为,原理上⽽⾔主要是解
决智能指针拷贝时的思路不同:
1.3 1 auto_ptr
auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使⽤auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使用这个智能指针的。
假设我们有一个date类:
auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
auto_ptr<Date> ap2(ap1);
将ap1对象拷贝给ap2对象,这样做会让ap1变为空,在我们不知道的情况下,将ap1拷贝给另一个对象,自己变为空之后,还去访问作为空的ap1,程序就崩溃了,所以我们强烈不建议使用auto_ptr。
1.3 2 unique_ptr
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不⽀持拷
⻉,只⽀持移动。如果不需要拷⻉的场景就⾮常建议使⽤他。
unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
1.3 3 shared_ptr(重)
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是⽀持拷⻉,
也⽀持移动。如果需要拷⻉的场景就需要使用他了。底层是用引用计数的方式实现的。
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
1.3 4 weak_ptr
weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指
针,他不⽀持RAII,也就意味着不能⽤它直接管理资源,weak_ptr的产⽣本质是要解决shared_ptr
的⼀个循环引⽤导致内存泄漏的问题。具体细节下⾯我们再细讲。
下面内容请看完shared_ptr再看:
智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。
智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调⽤删除器去释放资源。
因为new[]经常使⽤,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(newDate[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
如果我们要使用shared_ptr来new[]类型的资源,一般是不被允许的,我们来看下面这行代码:
shared_ptr<Date> sp1(new Date[10]);
运行一下:
程序直接崩溃了,但是这样的代码我们可能要经常使用,所以系统直接特化了一份带[]的版本,只要在类型的后面加上[]就可以正常析构了:
可是,在实际使用场景中,我们不不能只会遇到[]的情况,如果我们遇到的是文件类型呢?看下面的代码:
shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
这样的代码又该怎么析构呢,难道系统在底层使用fclose实现了它的析构吗,答案是否定的,遇到这种不常见的场景,我们只能自己使用定制删除器来解决。定制删除器支持仿函数、函数指针、lambda等,目的是释放那些我们无法正常释放的资源,运行上面这行代码:
终端没有任何显示,说明这个FILE类型的资源没有正常释放,我们先来看文档中的shared_ptr :
它有一个del参数版本的,这个del就是我们传入的删除器,以上面这个文件类型的资源举例,写一个仿函数:
class Fclose
{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr;fclose(ptr);}
};
传给它看看:
shared_ptr<FILE> sp2(fopen("test.cpp", "r"),Fclose());
可以看到资源正常释放了 ,原理如下:
删除器还支持使用lambda:
使用这种方式也可以正常释放资源,再来看函数指针版本:
三种方法没有好坏,只有适合各自的场景,要使用哪一种需结合场景使用。
而unique_ptr与shared_ptr不同,shared_ptr传删除器是在构造时传,而unique_ptr则在类的声明时传:
这就导致两者的定制删除器使用方式不一样,unique_ptr传仿函数时更加方便:
unique_ptr<FILE,Fclose> sp2(fopen("test.cpp", "r"));
因为在<>中,只能传类型而不能传对象,lambda本质是一个对象,我们拿不到lambda的类型,所以仿函数是unique_ptr使用删除器最好的选择,在我们使用unique_ptr时,推荐使用仿函数来做为删除器的参数。而shared_ptr,三者都可以,相对建议传lambda,因为lambda的实现就在该行中,在这一行中,它的功能一览无余。
1.3 5 模拟实现删除器
实现删除器,我们只需要让它能支持下面这行代码就行了:
test::shared_ptr<Date> sp1(new Date[10], [](Date* ptr) {delete[] ptr; });
从它的构造函数入手,它要传入两个参数,我们的构造函数也要设置一个支持带删除器版本的才能实现删除器的功能,而如果我们这样写:
template<class D>
shared_ptr(T* ptr,D del):_ptr(ptr), _pcount(new int(1))
{}
我们可以多定义一个变量,但是这个D类型在类的内部无法使用,如果将它加到类模板上,那它的使用方法不就与unique_ptr一样要传类型了吗,所以我们在类的内部定义一个function(包装器)类型:
function<void(T*)> _del;
这样就完美解决了既不用传类型来使用删除器,又可以传两个参数的问题,我们的析构函数也要改为使用删除器来释放资源:
~shared_ptr()
{if (--(*_pcount) == 0){_del(_ptr);delete _pcount;}
}
构造函数为:
template<class D>
shared_ptr(T* ptr,D del):_ptr(ptr), _pcount(new int(1)),_del(del)
{}
test::shared_ptr<Date> sp1(new Date);
test::shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; });
我们运行运行一下:
可以看到下面带[]的对象正常释放了,而运行到不带[]的对象这里时却崩溃了 ,这是什么原因呢,因为我们的析构函数被改变了,从默认使用delete来释放资源变成了使用删除器释放,而上面那行我们没有传删除器,所以我们要给删除器加一个缺省参数:
function<void(T*)> _del = [](T* ptr) {delete ptr; };
这样就可以正常释放了:
(1)template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);
(2)shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
shared_ptr<Date> sp1(new Date(2024, 11, 19));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 11, 19);
这两句代码的作用是样的,但是make_shared有一个好处。我们知道,shared_ptr底层是用引用计数来维护的,而引用计数要向内存中开辟空间,一个int*类型是四个字节,shared_ptr本身就要开辟空间,引用计数与shared_ptr申请的资源是分开的,如果我们多次使用shared_ptr,就会在内存中开辟很多个小的空间,这样会引起空间碎片过多的问题,而make_shared则是将引用计数和shared_ptr本身开辟的空间放在了一起,比起shared_ptr,使用make_shared可以减少不必要的空间碎片,就可以提高效率。
(3)shared_ptr 和 unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断
是否为空。
(4)shared_ptr 和 unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
// 这样实现程序会崩溃
// unique_ptr<Date> up1(new Date[10]);
// shared_ptr<Date> sp1(new Date[10]);
// 解决⽅案1
// 因为new[]经常使⽤,所以unique_ptr和shared_ptr
// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
// 解决⽅案2
// 仿函数对象做删除器
//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>
());
// unique_ptr和shared_ptr⽀持删除器的⽅式有所不同
// unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的
// 这⾥没有使⽤相同的⽅式还是挺坑的
// 使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤
// 但是下⾯的函数指针和lambda的类型不可以
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
/ 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
// 报错
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
2.智能指针的原理
(1)下⾯我们模拟实现了auto_ptr和unique_ptr的核⼼功能,这两个智能指针的实现⽐较简单,⼤家了解⼀下原理即可。auto_ptr的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可的,也不建议使⽤。unique_ptr的思路是不⽀持拷贝。
namespace test
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(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;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针⼀样使⽤
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
}int main()
{
test::auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
test::auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
test::unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
test::unique_ptr<Date> up3(move(up1));
return 0;
}
(2) ⼤家重点要看看shared_ptr是如何设计的,尤其是引⽤计数的设计,主要这⾥⼀份资源就需要⼀个引⽤计数,我们知道一个静态成员属于该类的所有的对象,这就意味着,该类每实例化一个对象就会让这个静态成员加1,而我们的目的是让一份资源由多个对象共享,只有共享某份资源的对象才能让对应的引用计数加1,所以引用计数采用静态成员的方式是⽆法实现的,要使⽤堆上动态开辟的⽅式,构造智能指针对象时来⼀份资源,就要new⼀个引⽤计数出来。多个shared_ptr指向资源时就++引⽤计数,shared_ptr对象析构时就--引⽤计数,引⽤计数减到0时代表当前析构的shared_ptr是最后⼀个管理资源的对象,则析构资源。
2.1 shared_ptr模拟实现
先实现一个日期类,因为智能指针的设计就是用来管理类似向内存申请的资源:
struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};
我们在日期类的析构函数中加一行打印,方便我们观察我们申请的资源是否正常释放。
先使用库中的shared_ptr测试一下:
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);test::shared_ptr<Date> sp4(new Date);
创建了一个对象sp1,再用两个对象来共享sp1中的资源,接着由创建了sp4对象,它也申请了自己的资源,所以,在这里虽然由、有四个对象,但是只申请了两份资源,所以只有两次析构,我们运行程序测试运行一下:
我们的推测没有错,程序结束时只析构了两次,下面我们就来自己实现一个shared_ptr。
2.1 1 模拟实现shared_ptr的构造函数
使用一个模板类型的指针来接收我们要共享的资源,然后使用int类型的指针作为引用计数:
T* _ptr;
int* _pcount;
使用T类型的参数来接收我们要共享的类型,如果没有传参,就默认为空 ,设置一个为nullptr的缺省参数,引用计数默认为1:
shared_ptr(T* ptr=nullptr):_ptr(ptr),_pcount(new int(1))
{}
2.1 2 模拟实现shared_ptr的拷贝构造函数
拷贝构造函数接收一个shared_ptr类型的对象,将我们接收到的对象的T类型的指针通过浅拷贝的方法拷贝给我们新创建的对象,再把相应的引用计数复制给新对象,让引用计数加1;
shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount)
{++(*_pcount);
}
我们来测试一下:
可以看到构造函数和拷贝构造函数的模拟实现成功了, 这意味着shared_ptr完成了吗?
我们来看看给另一对象赋值的情况能不能正常使用,将sp4给sp1:
我们发现此程序只析构了一次,但是我们申请了两份资源, 说明我们目前的程序还有问题,还需要实现一个拷贝赋值函数。在赋值之前,我们要先将自己的资源析构掉,如果有其他对象共享了它的资源,则只需让它的引用计数减一,再执行赋值操作,让引用计数加一,值得注意的是,我们要小心自己给自己赋值的情况,如果一个对象自己给自己赋值,自己是最后一个拥有资源的对像,在判断引用计数减一之后为0,析构完了,再给自己赋值,程序就会崩溃,所以这种情况也要处理好:
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{//防止自己给自己赋值的错误出现if (_ptr != sp._ptr){// 接收资源之前先释放掉原来的资源// 如果不知一个智能指针管理这块资源//就让引用计数减一if (--(*_pcount) == 0){delete _ptr;delete _pcount;}//一切正常,就正常赋值_pcount = sp._pcount;_ptr = sp._ptr;++(*_pcount);}return *this;}
我们再测试一下:
这次就没有问题了。
2.1 3 析构函数
析构函数的逻辑与拷贝赋值的上半部分的逻辑一样,先减一再判断引用计数是否为0,如果是就使用delete释放申请的资源。
~shared_ptr()
{if (--(*_pcount) == 0){delete _ptr;delete _pcount;}
}
3. shared_ptr和weak_ptr
3.1 shared_ptr循环引⽤问题
•shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会
导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使
⽤weak_ptr解决这种问题。
• 如下图所述场景,n1和n2析构后,管理两个节点的引⽤计数减到1
1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
• ⾄此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏
• 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的
引⽤计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题
我们来演示一下这里的问题:有一个双链表,它的两个结点分别是两个ListNode对象,被两个shared_ptr管理着,如果要链接,只能使用shared_ptr来链接:
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
而不是ListNode结点:
ListNode* _next;
ListNode* _prev;
如果使用这种方式来链接,就会出现shared_ptr类型链接LIstNode类型的情况,造成类型不匹配的问题,那么我们只能都只使用shared_ptr类型来链接:
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
定义两个对象:
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
不将它们链接:
现在可以正常析构,将它们链接:
n1->_next = n2;
n2->_prev = n1;
再运行一下:
3.2 weak_ptr
• weak_ptr不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现weak_ptr构造时不⽀持绑定到资
源,只⽀持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引⽤计数,那么就可以
解决上述的循环引⽤问题。
• weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的
shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的
资源是否过期,use_count也可获取shared_ptr的引⽤计数,weak_ptr想访问资源时,可以调⽤
lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如
果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
没有正常析构,这就是循环引用,是什么原因导致的我们在上面已经讲清楚了,遇到上面的极端场景我们可以使用weak_ptr来管理两个结点:
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
将链表的结点交给weak_ptr来管理,再运行一下:
现在这两个结点正常释放了,引用计数也始终是1 。
4.shared_ptr的线程安全问题
• shared_ptr的引⽤计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷
⻉析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者
原⼦操作保证线程安全的。
• shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr
管,它也管不了,应该有外层使⽤shared_ptr的⼈进⾏线程安全的控制。
• 下⾯的程序会崩溃或者A资源没释放,bit::shared_ptr引⽤计数从int*改成atomic<int>*就可以保证
引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以。
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这⾥智能指针拷⻉会++计数
bit::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
5.C++11和boost中智能指针的关系
• Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员之⼀。在Boost库的开发中,Boost社区也在这个⽅向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
• C++ 98 中产⽣了第⼀个智能指针auto_ptr。
• C++ boost给出了更实⽤的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等.
• C++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。
• C++ 11,引⼊了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的
scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
上述类容了解一下即可。
6.内存泄漏
6.1什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。
int main()
{
// 申请⼀个1G未释放,这个程序多次运⾏也没啥危害
// 因为程序⻢上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
6.2如何避免内存泄漏
• ⼯程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下⼀条智能指针来管理才有保证。
• 尽量使⽤智能指针来管理资源,如果⾃⼰场景⽐较特殊,采⽤RAII思想⾃⼰造个轮⼦管理。
• 定期使⽤内存泄漏工具检测,尤其是每次项⽬快上线前,不过有些⼯具不够靠谱,或者是收费。
• 总结⼀下:内存泄漏⾮常常⻅,解决⽅案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
本章完。