下面我们先分析一下下面这段程序有没有什么内存方面的问题?
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
异常。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;
}
C/C++程序中一般我们关心两种方面的内存泄漏:
总的来说,内存泄漏非常常见,解决方案分为两种:1、事前预防型,如智能指针等。2、事后排查型,如内存泄漏检测工具。
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
RAII设计思想的应用:智能指针,std::lock_guard,std::unique_lock等。
指针可以解引用,也可以通过->去访问所指空间中的内容,因此: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;
}
运行结果:
思考一下,使用默认生成的拷贝构造和赋值重载(值拷贝)可以吗?
将上面的p2改为拷贝构p1:smart_ptr
运行结果:
单纯的值拷贝显然是不行的,出作用域时p1, p2对象都会调用析构函数,对同一堆空间double free,程序运行崩溃。
我们来看一看C++标准中是如何解决智能指针的拷贝问题的
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 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
。
Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。
Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的**“准”标准库**。
Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎。
Boost中很多好用的内容都被C++标准吸收了,如C++11中的右值引用、线程库、智能指针等等。
C++11和boost中智能指针的关系
auto_ptr
.scoped_ptr
、shared_ptr
和weak_ptr
。shared_ptr
等。不过注意的是TR1并不是标准版。unique_ptr
和shared_ptr
和weak_ptr
。需要注意的是unique_ptr
对应boost中的scoped_ptr
。并且这些智能指针的实现原理是参考boost中的实现的。要想使用C++标准库定义的unique_ptr
和shared_ptr
和weak_ptr
必须包含头文件
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
。
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象共同管理同一个资源。
下面简化模拟实现了一份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):
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
不加保护的修改引用计数:
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管理的对象是线程安全的吗?——不一定!
智能指针管理的对象存放在堆上,两个线程同时去访问,会导致线程安全问题。
不加保护的访问管理对象:
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
可以得到相同的结果。std::shared_ptr::get
用于返回存储的指针;std::shared_ptr::use_count
用于返回引用计数。std::shared_ptr
要考虑的问题会更多,比如内存碎片、与std::weak_ptr
进行配合等。因此std::shared_ptr
的具体实现会相当复杂。以上的内容(jmx::shared_ptr
)只是对其核心功能的简单模拟,二者的差别其实还是很大的。请看下面的代码:
// 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既是节点的指针,又是节点名。
解决方案:在循环引用的场景下,把节点中的_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
的实现要复杂得多。
如果不是new出来的对象如何通过智能指针管理呢?
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); });
}
运行结果: