一、为什么需要智能指针?
下面我们先分析一下下面这段程序有没有什么内存方面的问题?
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
operator new
可能会抛std::bad_alloc
异常;div
可能会抛std::invalid_argument
异常。- 如果p1这里new 抛异常,由于p2还未申请空间,所以不会有什么问题。
- 如果p2这里new 抛异常,就必须中途捕获异常,释放p1,再重新抛出异常。
- 如果div调用这里抛异常,也必须中途捕获异常,释放p1,p2,再重新抛出异常。
- 以此类推,如果还要定义其他指针申请空间的话,需要在每一个new语句位置添加try/catch捕获异常,并将定义在前的,已经申请空间成功的指针释放。这样的话代码书写起来太过冗余难看了。
二、内存泄漏
2.1 什么是内存泄漏,内存泄漏的危害
- 什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。 - 内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致可用内存也来越少,系统响应越来越慢,申请内存失败等问题。
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;
}
2.2 内存泄漏分类(了解)
C/C++程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete释放内存。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比如:套接字、文件描述符、管道等。但是没有使用对应的函数释放掉,导致系统资源的浪费,严重的可能导致系统效能降低,系统执行不稳定。
2.3 如何检测内存泄漏(了解)
- 在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-优快云博客
- 在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库-优快云博客
- 其他工具:内存泄露检测工具比较 - 默默淡然 - 博客园 (cnblogs.com)
2.4 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这是理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要智能指针来管理才有保证。
- 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用自己实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总的来说,内存泄漏非常常见,解决方案分为两种:1、事前预防型,如智能指针等。2、事后排查型,如内存泄漏检测工具。
三、智能指针的使用及原理
3.1 RAII设计思想
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所管理的资源在其生命期内始终保持有效。
RAII设计思想的应用:智能指针,std::lock_guard,std::unique_lock等。
3.2 像指针一样使用
指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
template <class T>
class smart_ptr
{
T *_ptr;
public:
// RAII设计思想:利用对象生命周期来控制程序资源
// 构造时,将指针保存到对象内部
smart_ptr(T *ptr)
: _ptr(ptr)
{
}
// 析构时,释放指针指向的堆空间
~smart_ptr()
{
if (_ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
}
}
// 重载*和->使smart_ptr可以向原生指针一样使用
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
// 思考一下,使用默认生成的拷贝构造和赋值重载(值拷贝)可以吗?
};
使用smart_ptr修改一下第一段代码:
void Func()
{
smart_ptr<int> p1(new int(1));
smart_ptr<int> p2(new int(2));
cout << *p1 << " " << *p2 << endl;
*p1 = 10;
*p2 = 20;
cout << *p1 << " " << *p2 << endl;
cout << div() << endl;
}
运行结果:
3.3 智能指针的拷贝问题
思考一下,使用默认生成的拷贝构造和赋值重载(值拷贝)可以吗?
将上面的p2改为拷贝构p1:smart_ptr<int> p2(p1);
运行结果:
单纯的值拷贝显然是不行的,出作用域时p1, p2对象都会调用析构函数,对同一堆空间double free,程序运行崩溃。
我们来看一看C++标准中是如何解决智能指针的拷贝问题的
3.3.1 std::auto_ptr (C++98)
C++98版本的库中就提供了auto_ptr
的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移
下面简化模拟实现了一份jmx::auto_ptr
来了解它的原理:
namespace jmx
{
template <class T>
class auto_ptr
{
T *_ptr;
public:
auto_ptr(T *ptr)
: _ptr(ptr)
{
}
// auto_ptr的拷贝方法:管理权转移
auto_ptr(auto_ptr &sp)
: _ptr(sp._ptr)
{
sp._ptr = nullptr; // 将拷贝对象的指针置空
}
auto_ptr &operator=(auto_ptr &ap)
{
// 赋值重载注意检测是否是自己给自己赋值
if (_ptr != ap._ptr)
{
// 释放当前指针指向的堆空间
if (_ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
}
// 转移ap中的资源到当前对象中
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
};
}
不管是使用自己实现的jmx::auto_ptr
还是使用C++98提供的std::auto_ptr
执行上面的Func函数(用p1拷贝构造p2),都会发生段错误
这是因为auto_ptr
的拷贝方法是管理权转移,完成拷贝后原对象的指针会被置空,此时再解引用访问,就相当于访问空指针,自然会发生内存错误。并且可以看到现在的编译器会告警,不建议使用auto_ptr
。
auto_ptr
的正确使用方法:
void Func()
{
jmx::auto_ptr<int> p1(new int(1));
jmx::auto_ptr<int> p2(new int(2));
cout << "*p1 -> " << *p1 << endl;
cout << "jmx::auto_ptr<int> p3(p1);" << endl;
jmx::auto_ptr<int> p3(p1); // 拷贝构造
cout << "*p3 -> " << *p3 << endl;
cout << "*p2 -> " << *p2 << endl;
cout << "p2 = p3;" << endl;
p2 = p3; // 赋值重载
cout << "*p2 -> " << *p2 << endl;
cout << div() << endl;
}
运行结果:
结论:
auto_ptr
是一个的失败设计,因为资源的管理权转移,存在被拷贝对象指针悬空的问题。因此,很多公司明确要求不能使用auto_ptr
。
3.3.2 C++准标准库 Boost
Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。
Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的**“准”标准库**。
Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎。
Boost中很多好用的内容都被C++标准吸收了,如C++11中的右值引用、线程库、智能指针等等。
C++11和boost中智能指针的关系
- 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中的实现的。
要想使用C++标准库定义的unique_ptr
和shared_ptr
和weak_ptr
必须包含头文件<memory>
3.3.3 std::unique_ptr (C++11)
C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝。
下面简化模拟实现了一份jmx::unique_ptr
来了解它的原理:
namespace jmx
{
template <class T>
class unique_ptr
{
T *_ptr;
// C++98: 声明为私有,只声明不实现
// unique_ptr(const unique_ptr &up);
// unique_ptr &operator=(const unique_ptr &up);
public:
unique_ptr(T *ptr)
: _ptr(ptr)
{
}
~unique_ptr()
{
if (_ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
}
}
// C++11: delete关键字,语法直接支持
unique_ptr(const unique_ptr &up) = delete;
unique_ptr &operator=(const unique_ptr &up) = delete;
};
}
总结:在一些不需要拷贝指针的场景中使用unique_ptr
。
3.3.4 std::shared_ptr (C++11)
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象共同管理同一个资源。
- 在shared_ptr内部,给每个资源都维护了一个引用计数,用来记录该份资源被几个对象共同管理。
- 在对象被销毁时(也就是析构函数调用),就说明自己不管理该资源了,对象的引用计数减1。
- 如果引用计数减到0,就说明自己是最后一个管理该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在管理该份资源,不能释放该资源,否则其他对象就成野指针了。
下面简化模拟实现了一份jmx::shared_ptr
来了解它的原理:
namespace jmx
{
template <class T>
class shared_ptr
{
T *_ptr;
int *_pcount; // 引用计数的指针
public:
shared_ptr(T *ptr)
: _ptr(ptr),
_pcount(new int(1)) // 注意:在构造时申请引用计数的空间,为每一份资源绑定一个引用计数
{
}
// 1.如果引用计数减到0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
// 2.如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
void Release()
{
cout << "void Release()" << endl;
if (--(*_pcount) == 0 && _ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
delete _pcount; // 记得释放引用计数的空间
}
}
void AddCount()
{
++(*_pcount);
}
shared_ptr(const shared_ptr &sp)
: _ptr(sp._ptr),
_pcount(sp._pcount)
{
AddCount();
}
shared_ptr &operator=(const shared_ptr &sp)
{
if (_ptr != sp._ptr) // 建议比较存储指针,防止不同对象同一资源相互赋值
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
AddCount();
}
return *this;
}
~shared_ptr()
{
Release();
}
T* get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
};
}
使用jmx::shared_ptr
再次执行Func函数(用p1拷贝构造p2):
3.4 std::shared_ptr的线程安全问题
std::shared_ptr是线程安全的。
- std::shared_ptr的引用计数操作是线程安全的,这意味着多个线程可以同时访问和修改引用计数,而不会导致竞争条件。这是因为std::shared_ptr的引用计数存储在堆上的控制块中,多个std::shared_ptr对象指向同一个堆地址,因此在进行计数的增加或减少时,能够保证线程安全。此外,std::shared_ptr的引用计数更新操作被设计为原子操作,确保了在多线程环境下的正确性12。
- 然而,需要注意的是,虽然std::shared_ptr本身及其引用计数的更新是线程安全的,但它并不保证所指向对象的线程安全。如果多个线程需要同时访问或修改该对象,那么必须采取额外的同步措施,如使用互斥锁(mutex),以确保对共享数据的访问是线程安全的13。
通过下面的程序我们来测试jmx::shared_ptr的线程安全问题。需要注意的是jmx::shared_ptr的线程安全分为两方面:
3.4.1 引用计数的线程安全问题
- 智能指针对象中的引用计数是管理同一资源的多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。
- 所以智能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。
不加保护的修改引用计数:
namespace jmx
{
template <class T>
class shared_ptr
{
T *_ptr;
int *_pcount; // 引用计数
public:
void Release()
{
if (--(*_pcount) == 0 && _ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
delete _pcount; // 记得释放引用计数的空间
}
}
void AddCount()
{
++(*_pcount);
}
};
}
class Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void ThreadFunc(jmx::shared_ptr<Date>& sp, int n)
{
while(n--)
{
// 两个线程并发的进行n次sp的拷贝构造,即n次引用计数的++和--,存在线程安全问题
jmx::shared_ptr<Date> copy(sp);
}
}
int main()
{
jmx::shared_ptr<Date> sp(new Date);
cout << sp.get() << endl;
int n = 100000;
// 多线程执行
thread t1(ThreadFunc, ref(sp), n); //线程函数传参传引用需要使用ref
thread t2(ThreadFunc, ref(sp), n);
t1.join();
t2.join();
// 最后打印sp的引用计数
cout << sp.use_count() << endl;
return 0;
}
运行结果:
发现最终引用计数并不是我们预想的1,sp指针指向的堆空间也未正确释放。
互斥访问引用计数:
namespace jmx
{
// shared_ptr
template <class T>
class shared_ptr
{
T *_ptr;
int *_pcount; // 引用计数的指针
mutex *_mtx; // 互斥锁的指针
public:
shared_ptr(T *ptr)
: _ptr(ptr),
_pcount(new int(1)), // 注意:在构造时申请引用计数的空间,为每一份资源绑定一个引用计数
_mtx(new mutex) // 注意:在构造时申请互斥量的空间,为每一份资源绑定一个互斥量
{
}
void Release()
{
bool delete_flag = false;
//加锁保护引用计数
_mtx->lock();
// cout << "void Release()" << endl;
if (--(*_pcount) == 0 && _ptr != nullptr)
{
cout << "delete _ptr: " << _ptr << endl;
delete _ptr;
delete _pcount; // 记得释放引用计数的空间
delete_flag = true;
}
_mtx->unlock();
//必须先解锁再销毁互斥锁,因此:
if(delete_flag)
{
delete _mtx; // 记得释放互斥量的空间
}
}
void AddCount()
{
//加锁保护引用计数
_mtx->lock();
++(*_pcount);
_mtx->unlock();
}
shared_ptr(const shared_ptr &sp)
: _ptr(sp._ptr),
_pcount(sp._pcount),
_mtx(sp._mtx) // 记得拷贝互斥量指针
{
AddCount();
}
shared_ptr &operator=(const shared_ptr &sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_mtx = sp._mtx; // 记得拷贝互斥量指针
AddCount();
}
return *this;
}
};
}
运行结果:
shared_ptr的引用计数加锁保护,所以shared_ptr现在是线程安全的。
shared_ptr管理的对象是线程安全的吗?——不一定!
3.4.2 被管理对象的线程安全问题
智能指针管理的对象存放在堆上,两个线程同时去访问,会导致线程安全问题。
不加保护的访问管理对象:
void ThreadFunc(jmx::shared_ptr<Date>& sp, int n)
{
while(n--)
{
jmx::shared_ptr<Date> copy(sp);
//两个线程对同一日期类对象的年月日++100000次,如果是线程安全的,最后的结果应该都是200000。
++copy->_year;
++copy->_month;
++copy->_day;
}
}
int main()
{
jmx::shared_ptr<Date> sp(new Date);
cout << sp.get() << endl;
int n = 100000;
thread t1(ThreadFunc, ref(sp), n);
thread t2(ThreadFunc, ref(sp), n);
t1.join();
t2.join();
cout << sp.use_count() << endl;
//最后打印sp指向的日期类对象的年月日
cout << sp->_year << "/" <<sp->_month << "/" << sp->_day << endl;
return 0;
}
运行结果:
显然不加保护的访问管理对象不是线程安全的。
互斥的访问管理对象:
void ThreadFunc(jmx::shared_ptr<Date> &sp, int n, mutex &mtx)
{
while (n--)
{
jmx::shared_ptr<Date> copy(sp);
// 访问管理对象时需要加锁
lock_guard<mutex> lock(mtx);
++copy->_year;
++copy->_month;
++copy->_day;
}
}
int main()
{
jmx::shared_ptr<Date> sp(new Date);
cout << sp.get() << endl;
int n = 100000;
mutex mtx;
thread t1(ThreadFunc, ref(sp), n, ref(mtx));
thread t2(ThreadFunc, ref(sp), n, ref(mtx));
t1.join();
t2.join();
cout << sp.use_count() << endl;
cout << sp->_year << "/" << sp->_month << "/" << sp->_day << endl;
return 0;
}
运行结果:
提示:
std::shared_ptr::get
用于返回存储的指针;std::shared_ptr::use_count
用于返回引用计数。- C++标准库中的
std::shared_ptr
要考虑的问题会更多,比如内存碎片、与std::weak_ptr
进行配合等。因此std::shared_ptr
的具体实现会相当复杂。以上的内容(jmx::shared_ptr
)只是对其核心功能的简单模拟,二者的差别其实还是很大的。
3.5 std::shared_ptr的循环引用问题
3.5.1 循环引用的场景
请看下面的代码:
// shared_ptr的循环引用问题
template <class T>
struct ListNode
{
// 不能使用原生指针,因为原生指针和shared_ptr不能相互赋值
// ListNode *_prev;
// ListNode *_next;
jmx::shared_ptr<ListNode> _prev;
jmx::shared_ptr<ListNode> _next;
T _val;
ListNode(T val = T())
: _prev(nullptr),
_next(nullptr),
_val(val)
{
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_cycle()
{
jmx::shared_ptr<ListNode<int>> node1(new ListNode<int>(1));
jmx::shared_ptr<ListNode<int>> node2(new ListNode<int>(2));
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;
}
运行结果:
链接node1->_next和node2->_prev时,形成循环引用,最终节点并未成功释放,造成了内存泄漏问题。
循环引用分析:
- 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才会析构。这就叫循环引用,两者互相牵制谁也不放过谁。
提示:这里的node1, node2既是节点的指针,又是节点名。
3.5.2 std::weak_ptr
解决方案:在循环引用的场景下,把节点中的_prev和_next改成weak_ptr
类型就可以了。
weak_ptr
的原理:
weak_ptr
不是常规的智能指针,不支持RAII。weak_ptr
不支持指针构造,但是支持shared_ptr构造和赋值。weak_ptr
也可以向指针一样使用*和->weak_ptr
是专门用于辅助解决shared_ptr
的循环引用问题的:weak_ptr
可以指向资源,但是他不参与管理资源,不增加引用计数。
下面简化模拟实现了一份jmx::weak_ptr
来了解它的原理:
namespace jmx
{
template <class T>
class weak_ptr
{
T *_ptr;
public:
weak_ptr()
: _ptr(nullptr)
{
}
// weak_ptr只是单纯的指向资源,不参与管理资源,不增加引用计数。
weak_ptr(shared_ptr<T> sp)
: _ptr(sp.get())
{
}
// weak_ptr也可以向指针一样使用*和->
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
};
}
把节点中的_prev和_next改成weak_ptr
类型,重新编译运行:
shared_ptr的循环引用问题也得到了很好的解决。
提示:
-
以上的所有测试使用
std::shared_ptr
和std::weak_ptr
可以得到相同的结果。 -
标准库中实现的
std::weak_ptr
中也包含引用计数,仅用于检查weak_ptr是否过期,即是否还有其他std::shared_ptr
指向该资源。已经过期的weak_ptr不能再被访问。 -
std::weak_ptr::use_count
用于返回引用计数;std::weak_ptr::expired
用于检查weak_ptr是否过期,This function shall return the same as(use_count()==0)
-
以上的
jmx::weak_ptr
是std::weak_ptr
的简单模拟,实际std::weak_ptr
的实现要复杂得多。
3.6 定制删除器(jmx::shared_ptr最终版)
如果不是new出来的对象如何通过智能指针管理呢?
- 其实shared_ptr设计了一个删除器来解决这个问题。所谓删除器其实就是一个可调用对象(三者中的任意一个),shared_ptr最终会通过指定的删除器对存储的指针进行空间释放或销毁。
- 通过指定删除器,我们不仅可以调用delete或free释放单个空间,还可以调用delete[]释放连续的多个空间,甚至可以调用fclose关闭文件。
jmx::shared_ptr最终版:
namespace jmx
{
// shared_ptr最终版
template <class T>
class shared_ptr
{
T *_ptr = nullptr; // 存储指针
int *_pcount; // 引用计数指针
mutex *_mtx; // 互斥量指针
function<void(T *)> _del = [](T *ptr) // 注意:模板参数D属于构造函数,函数外不能使用,所以借助包装器实现
{ cout << "lambda: delete ptr" << endl;
delete ptr }; // 删除器,默认为lambda: delete
public:
shared_ptr(T *ptr)
: _ptr(ptr),
_pcount(new int(1)),
_mtx(new mutex)
{
}
template <class D> //删除器使用模板,支持任意类型的可调用对象
shared_ptr(T *ptr, D del)
: _ptr(ptr),
_pcount(new int(1)),
_mtx(new mutex),
_del(del)
{
}
void Release()
{
bool delete_flag = false;
_mtx->lock();
// cout << "void Release()" << endl;
if (--(*_pcount) == 0 && _ptr != nullptr)
{
cout << "_del(_ptr): " << _ptr << endl;
_del(_ptr); // 使用删除器释放_ptr
delete _pcount;
delete_flag = true;
}
_mtx->unlock();
if (delete_flag)
{
delete _mtx;
}
}
void AddCount()
{
_mtx->lock();
++(*_pcount);
_mtx->unlock();
}
shared_ptr(const shared_ptr &sp)
: _ptr(sp._ptr),
_pcount(sp._pcount),
_mtx(sp._mtx),
_del(sp._del) // 记得拷贝删除器
{
AddCount();
}
shared_ptr &operator=(const shared_ptr &sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_mtx = sp._mtx;
_del = sp._del; // 记得拷贝删除器
AddCount();
}
return *this;
}
~shared_ptr()
{
Release();
}
T *get()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
T &operator*()
{
return *_ptr;
}
T *operator->()
{
return _ptr;
}
};
}
测试程序:
// 定制删除器
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
~Date()
{
cout << "~Date()" << endl;
}
};
template <class T>
struct DeleteArray
{
void operator()(T *ptr)
{
cout << "void operator()(T* ptr): delete[] ptr" << endl;
delete[] ptr;
}
};
void test_shared_deleter()
{
// 使用默认的lambda,释放单个空间
jmx::shared_ptr<Date> sp1(new Date);
// 使用函数对象DatesDeleter,释放连续的多个空间
jmx::shared_ptr<Date> sp2(new Date[3], DeleteArray<Date>());
// 使用指定的lambda,调用fclose关闭文件
jmx::shared_ptr<FILE> sp3(fopen("./smart_ptr.cc", "r"), [](FILE *fp){
cout << "lambda: fclose(fp)" << endl;
fclose(fp); });
}
运行结果:
3.7 使用make_shared取代new开辟空间
一个坑:
int main(int argc, const char * argv[]) {
auto p1 = new Test; // 划分堆空间
std::shared_ptr<Test> sp(p1); // 创建智能指针
std::shared_ptr<Test> sp2(p1); // 创建另一个智能指针
return 0;
}
- 这段程序会抛出异常 double free detected
- new关键字返回的是对应的指针类型。
- 此处用了两个智能指针管理同一块内存,因为sp 和sp2不知道彼此的存在,所以也会重复释放。
- 同一个对象只能用同一套内存管理体系,如果它已经有智能指针了,那么再创建智能指针时,需要通过原来已有的智能指针创建,而不能重复用原始空间来创建。
STL库提供了make_shared函数,其原型为:
template <typename T, typename ...Args>
std::shared_ptr<T> std::make_shared(Args && ...args)
- 官方鼓励用make_shared函数来创建对象,而不要手动去new,这样就可以防止我们去使用原始指针创建多个引用计数体系。
例:
int main(int argc, const char * argv[]) {
auto sp = std::make_shared<int>(); // 分配堆空间,创建智能指针
auto sp2 = sp; // 创建另一个智能指针
return 0;
}
本节摘自原文:C++ make_shared使用_make shared-优快云博客
3.8 总结
为了尽可能避免内存泄漏,推荐使用C++11标准下的智能指针unique_ptr, shared_ptr和weak_ptr
- 在一些不需要拷贝指针的场景中使用
unique_ptr
- 在需要拷贝指针的场景中使用
shared_ptr
,其通过引用计数的方式来实现多个shared_ptr对象共同管理同一个资源 - 在使用
shared_ptr
的过程中如果遇到循环引用的问题:对象的类内指针相互指向。此时应该将类内指针改为weak_ptr
,不参与管理资源,不增加引用计数。
智能指针的使用经验
-
智能指针只适用于
- 用户使用malloc, new等接口自己手动申请的内存空间(堆空间),智能指针负责析构时释放空间
- 一些需要使用完进行退出关闭等特殊处理的资源,例如:文件、套接字等等。对于此类功能需要重新定制删除器。
- 除以上两点之外的任何使用场景都应该使用原生指针或引用(推荐)。
-
如果不存在指针的拷贝问题使用unique_ptr最为简单可靠。
-
如果存在指针的拷贝问题则需要shared_ptr和weak_ptr搭配使用,但要注意:
-
使用make_shared代替new开辟空间,因为make_shared本身返回的就是一个shared_ptr,可以防止我们去使用原始指针(new)创建多个引用计数体系,从而导致double free等诸多问题。
-
weak_ptr不要胡乱使用,shared_ptr能用就用(使用简单,安全可靠),weak_ptr只用于解决循环引用的问题。
-
注意使用weak_ptr之前需要:
- 通过expired判断指针是否失效
- 通过lock返回一个shared_ptr在使用期间将资源锁定(不允许释放资源)。
-