目录
1、了解内存泄露
1.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;
}
1.2 内存泄漏分类(了解)
1.3 如何检测内存泄漏(了解)
1.4如何避免内存泄漏
总结一下:
内存泄漏非常常见。
解决方案分为两种:
- 事前预防型。如智能指针等。
- 事后查错型。如泄漏检测工具
2、智能指针的引出
因为内存泄漏的危害,故下列场景针对内存的释放,异常都是格外小心的处理
问题场景一。缺陷:不确定是否要抛异常,但是你都会catch捕获
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
int* p1 = new int;
//这种写法的问题:不确定是否要抛异常,但你都catch捕获了
try
{
cout << div() << endl;
}
//catch (exception& e)
//{
// delete p1;
// throw e;
//}
catch (...)
{//下面这种写法也可以
//拦截下来先释放,再抛异常
delete p1;
throw;
}
delete p1;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
问题场景二、多个new抛异常。缺陷:不确定哪个对象会new失败
针对以上问题,引出智能指针
3、智能指针的使用及原理
3.1 RAII
//使用RAII思想设计SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
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()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
//无论是函数正常结束,还是抛异常,都会导致sp对象的生命周期到了后析构函数释放资源
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
3.2 智能指针的原理
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
SmartPtr<pair<int, int>> sp2(new pair<int, int>);
sp2->first = 20;
sp2->second = 30;
cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date);
// 需要注意的是这里应该是sparray.operator->()->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
总结一下智能指针的原理:
潜在的问题:
3.3 std::auto_ptr
C++98 版本的库中就提供了 auto_ptr 的智能指针。 auto_ptr 的实现原理:管理权转移的思想。
下面简化模拟实现了一份a uto_ptr 来了解它的原 理:
// C++98 管理权转移 auto_ptr
//模拟实现库中的auto_ptr
namespace mz
{
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;
};
}
// 结论:auto_ptr是早期的一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{
mz::auto_ptr<int> sp1(new int);
//用sp1拷贝sp2后,sp2就指向了sp1原指向的空间,而sp1会被置空(不指向任何空间)
mz::auto_ptr<int> sp2(sp1); // 管理权转移(空间的管理权转移)
// sp1悬空(因为s1已经指向空了!)
*sp2 = 10;
cout << *sp2 << endl;
cout << *sp1 << endl;
return 0;
}
auto_ptr缺陷:sp2 = sp1【赋值重载】或 sp2(sp1)【拷贝构造】场景下sp1就悬空了(nullptr),此时访问sp1就会报错,若不熟悉auto_ptr的特性就会被坑,故我们不推荐用甚至不让用auto_ptr
3.4 std::unique_ptr
C++11 中开始提供更靠谱的 unique_ptr。 unique_ptr 的实现原理:简单粗暴的防拷贝。
下面简化模拟实现了一份 UniquePtr 来了解它的原 理:
// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace mz
{
template<class T>
class unique_ptr
{
public:
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> delete;//直接不让拷贝
unique_ptr<T>& operator=(const unique_ptr<T> delete;//直接不让赋值
private:
T* _ptr;
};
}
int main()
{
/*mz::unique_ptr<int> sp1(new int);
mz::unique_ptr<int> sp2(sp1);*/
std::unique_ptr<int> sp1(new int);
//std::unique_ptr<int> sp2(sp1);
return 0;
}
3.5 std::shared_ptr
C++11 中开始提供更靠谱的并且支持拷贝的 shared_ptr。
shared_ptr 的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源 。
- 1. shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
- 2. 在对象被销毁时(即析构函数的调用),就说明自己不使用该资源了,对象的引用计数减一。
- 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 4. 如果不是0,说明除了自己还有其他对象在用该份资源,不能释放该资源,否则其他对象就成野指针了。
1、关于引用计数到底用什么类型的问题
①、int _count(不行)
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace mz
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_count(1)
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _count(sp._count)
{
//两个对象的引用计数都++
++_count;
++sp._count;
}
~shared_ptr()
{
if (--_count == 0 && _ptr)
{//只有当引用计数为0且_ptr!=nullptr时,才会释放这份资源
cout << "delete:" << _ptr << endl;//打印地址方便观察
delete _ptr;
_ptr = nullptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int _count; //引用计数
};
}
int main()
{
mz::shared_ptr<int> sp1(new int);
mz::shared_ptr<int> sp2(sp1);
return 0;
}
运行结果:什么都没有(代码中析构了会打印地址),为什么?
②、static int_count(不行)
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace mz
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
{
_count = 1;//不能放在列表初始化里,会被认为是初始化,但是函数体内可以
}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
{
++_count;//静态成员变量++
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
}
return *this;
}
~shared_ptr()
{
if (--_count == 0 && _ptr)
{//只有当引用计数为0且_ptr!=nullptr时,才会释放这份资源
cout << "delete:" << _ptr << endl;//打印地址方便观察
delete _ptr;
_ptr = nullptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
static int _count; //引用计数
};
template<class T>
int shared_ptr<T>::_count = 0;//成员变量类内定义,类外初始化
}
int main()
{
mz::shared_ptr<int> sp1(new int);
mz::shared_ptr<int> sp2(sp1);
mz::shared_ptr<int> sp3(new int);
return 0;
}
运行结果:
两块资源只析构了一次?原因如下:
③、int& _count(不行)
④、int* _pcount(可行)
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace mz
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);//对同时管理的这块资源的引用计数++
}
//sp1 = sp4
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
//if (this != &sp)这种写法也可以
if (_ptr != sp._ptr)//如果管理的不是同一块资源,才会赋值
{
//判断是否需要释放旧空间资源
//若我是最后一个管理资源的对象,则需要释放资源
if (--(*_pcount) == 0)
{ //释放旧空间资源
delete _pcount;
delete _ptr;
}
//管理同一份空间资源
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);//引用计数++
}
return *this;
}
~shared_ptr()
{
if (--(*_pcount) == 0 && _ptr)
{//只有当引用计数为0且_ptr!=nullptr时,才会释放这份资源
cout << "delete:" << _ptr << endl;//打印地址方便观察
delete _ptr;
_ptr = nullptr;
delete _pcount;
_pcount = nullptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //引用计数:有多少个对象一起共享管理资源
};
}
int main()
{
mz::shared_ptr<int> sp1(new int);
mz::shared_ptr<int> sp2(sp1);
mz::shared_ptr<int> sp3(new int);
mz::shared_ptr<int> sp4(sp3);
mz::shared_ptr<int> sp5(sp3);
return 0;
}
运行结果:
线程安全问题:
shared_ptr的线程安全分两方面:
测试进程安全问题:
修改代码:加锁 (一个线程完事了才能让另一个线程开始)
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
//shared_ptr的拷贝赋值时线程安全问题
//shared_ptr是否是线程安全的,答:注意这里的shared_ptr对象拷贝和析构++/--引用计数
//是否是安全的,库中的实现是安全的
#include<thread>
#include<mutex>
namespace mz
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
add_ref_count();//对同时管理的这块资源的引用计数++
}
//sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{5
//if (this != &sp)这种写法也可以
if (_ptr != sp._ptr)//如果管理的不是同一块资源,才会赋值
{
//判断是否需要释放旧空间资源
//若我是最后一个管理资源的对象,则需要释放资源
release();
//管理同一份空间资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
add_ref_count();
}
return *this;
}
void add_ref_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
//两个线程若同时去释放,也会出现问题
void release()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
//delete _pmtx;//这里不能直接释放锁,因为unlock还没执行呢,故用flag
flag = true;
}
_pmtx->unlock();
if (flag == true)
{//要保证资源释放完了,才能释放锁
delete _pmtx;
_pmtx = nullptr;
}
}
~shared_ptr()
{
release();
}
int use_count()
{
return *_pcount;
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; //引用计数:有多少个对象一起共享管理资源
mutex* _pmtx; //互斥锁:为了保护引用计数,管理同一份资源的对象共用一个锁
};
}
int main()
{
mz::shared_ptr<int> sp(new int);
cout << sp.use_count() << endl; //1
int n = 10000;
//单纯两个拷贝出现出现问题的概率很小
/*std::thread t1([&]() {
mz::shared_ptr<int> sp1(sp);
});
std::thread t2([&]() {
mz::shared_ptr<int> sp2(sp);
});*/
//出了作用域sp1和sp2就销毁了(一个{}算一个作用域)
//若同时进行10000次就会出现线程安全问题(100,1000次都不一定出现问题)
std::thread t1([&]() {
for (int i = 0; i < n; ++i)
{
mz::shared_ptr<int> sp1(sp);
}
});
std::thread t2([&]() {
for (int i = 0; i < n; ++i)
{
mz::shared_ptr<int> sp2(sp);
}
});
t1.join();
t2.join();
cout << sp.use_count() << endl; //1
return 0;
}
加锁后结果正确:
这里为什么要用锁而不用原子操作?
总结shared_ptr:
循环引用分析:
针对shared_ptr这个缺陷,用weak_ptr来弥补:
//严格来说,weak_ptr不是智能指针,因为他没有RAII资源管理
//专门解决shared_ptr的循环引用问题
//简化版本的weak_ptr实现
namespace mz
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T* get() const
{
return _ptr;//获取原生指针
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
struct ListNode
{
int _data;
mz::weak_ptr<ListNode> _prev;
mz::weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> sp1(new ListNode);
shared_ptr<ListNode> sp2(new ListNode);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
//循环引用
sp1->_next = sp2; //解决方式:使用wear_ptr,不增加引用计数
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
3.6 定制删除器(了解)
//定制删除器(了解)
#include<memory>
#include<cstdlib>
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
int _a2;
};
//解决:写个仿函数传给智能指针即可完成析构
template<class T>
struct DeleteArry
{
void operator()(T* pa)
{
delete[] pa;
}
};
struct Free
{
void operator()(void* p)
{
cout << "Free(p)" << endl;
free(p);
}
};
struct Fclose
{
void operator()(FILE* p)
{
cout << "Fclose(p)" << endl;
fclose(p);
}
};
int main()
{
std::shared_ptr<A> sp1(new A);//new出来的对象会正常析构
//因为智能指针的析构就是delete,没有delete[]等
//std::shared_ptr<A> sp2(new A[10]);//程序崩溃
//std::shared_ptr<A> sp3((A*)malloc(sizeof(A)));//程序崩溃
//std::shared_ptr<FILE> sp4(fopen("text.txt", "w"));//程序崩溃
//针对特殊的析构,利用仿函数,而不用本来的delete了
std::shared_ptr<A> sp2(new A[10], DeleteArry<A>());
std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
std::shared_ptr<FILE> sp4(fopen("text.txt", "w"), Fclose());
return 0;
}
运行结果:
3.7 lock_guard(补充知识)
观察下面代码:
引入锁管理守卫:lock_guard
我们模拟实现一个
#include<mutex>
//使用RAII思想设计的锁管理守卫
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lock)
:_lk(lock)
{//因为锁不支持拷贝,所以_lk加&就可以解决
_lk.lock();
}
~LockGuard()
{
cout << "解锁" << endl;
_lk.unlock();
}
//锁守卫也不允许拷贝和赋值
LockGuard(LockGuard<Lock> delete;
LockGuard<Lock>& operator()(LockGuard<Lock> delete;
private:
Lock //注意用引用是因为锁不支持拷贝,那就和外面传进来的锁用一个即可
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
mutex mtx;
LockGuard<mutex> lg(mtx);//无论是否抛异常都会正常解锁
cout << div() << endl; //div函数有可能抛异常
}
int main()
{
try
{
f();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}