文章目录
- 1、为什么需要智能指针?
- 2、内存泄漏
- 3、智能指针的使用及原理
- 1、RAII思想
- 2、拷贝问题
- 1、unique_ptr
- 2、shared_ptr
- 1、多线程
- 2、循环引用
- 3、定制删除器
1、为什么需要智能指针?
看一个场景
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
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;
}
new是可能开辟失败,抛异常的。上述代码中,如果p1抛异常,那么可以外面的catch可以捕获到,打印出消息;如果p1异常,p2也要抛异常,那么在这之前,应当销毁p1,再去抛;同理,到了div()那里如果也抛异常,那么得销毁p1和p2,整体就得这样写
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
try
{
int* p2 = new int;
}
catch (...)
{
delete p1;
throw;
}
try
{
cout << div() << endl;
}
catch (...)
{
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}
一下子就能看出来,这太麻烦了,如果有多个new呢?
2、内存泄漏
Windows和Linux都有检测内存泄漏的工具,不过Windows下的VLD不太靠谱,Linux中valgrind是比较出名的
Linux下几款C++程序中的内存泄露检查工具
为了预防内存泄漏,常用的办法就是用智能指针或者事后检测。
3、智能指针的使用及原理
1、RAII思想
template <class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
和封装锁的思路来类似,都是RAII。用临时变量来构造,出了作用域就自动销毁。
RAII利用对象生命周期来控制程序资源,对象构造时获取资源,析构时释放资源
上面的SmartPtr不像一个指针,它不能解引用数据,不过我们可以写对应的函数。
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
cout << *sp1 << endl;//如果模板参数是自定义类型的话就可以用->了。
2、拷贝问题
智能指针如何拷贝?
int main()
{
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(sp1);
return 0;
}
采用默认拷贝会浅拷贝,导致同一空间重复释放。这里应当如何写拷贝构造?是要用深拷贝吗?其实不是,我们要的浅拷贝,sp1和sp2指向同一个资源,以前的链表等这些迭代器结构不需要释放资源,而智能指针需要管理资源,所以不能单纯地浅拷贝,但是又不能要深拷贝。
C++98时已经有智能指针了,那个版本中有一个auto_ptr,它的方法是管理权转移,我们写到SmartPtr类中
//管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<int> sp2(sp1);
这样看就像是用ap指向sp1,然后用sp1的_ptr来初始化sp2的_ptr,然后把sp1的_ptr给置空。虽然看起来是可以的,能解决问题,但有很大隐患,这会导致sp1悬空,如果不知道管理权转移的实际写法,那么下面代码中如果有*sp1就出问题了。程序员用它的时候需要时刻提醒自己,被拷贝对象已经悬空了,不能去解引用它。
在C++11之前,有个可移植的C++库——Boost库,不是标准库,但也胜似标准库,是有C++标准委员会库工作组成员发起的,C++中有很多标准都从Boost中吸收过来,像右值引用,线程库。Boost库有scoped_ptr,weak_ptr,_shared_ptr,C++11中把scoped_ptr改名成unique_ptr。
1、unique_ptr
它的思路是防拷贝。
//防拷贝
//C++98思路:只声明不实现,但是还可以在外面强行定义,所以会把它放在私有里
//C++11思路:函数后= delete
//这里拷贝构造和赋值都写上
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
不需要拷贝的场景就用它。
2、shared_ptr
引用计数的思路。有多少个指针指向一个空间,那么这个空间的引用计数就是多少。当一个指针要释放时,如果引用计数大于0,那就不做操作,如果等于0,那就做一次释放资源,这个空间的指针也都用完了。
引用计数这个变量不能放在静态区,因为如果static修饰后,它属于类的每个对象,但我们要的是指向同一空间的所有指针。定义一个int* pcount。
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
赋值函数,比如sp1 = sp3,那么sp1的引用计数需要–,因为它要指向新空间了;假设sp1的空间还有别的指针指向,而sp3的空间只有sp3这一个指针,sp3 = sp1,那么就是sp3–。
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._str)
{
if (--(*_pcount) == 0)//处理空间上只有一个指针的情况
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
1、多线程
整体改成这样的形式来配合加锁
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
void Release()
{
if (--(*_pcount) == 0)
{
if(_ptr)//如果为空那就不需要释放
{
delete _ptr;
}
delete _pcount;
}
}
void AddCount()
{
++(*_pcount);
}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._str)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
AddCount();
}
return *this;
}
多线程比较常见的场景就是线程安全问题。同一个数会出现多次操作,导致结果不是我们想要的。多线程情况下,像传给接收引用的参数时,要写成ref(…),ref是库中的函数,否则会被认为是传值传参。
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
void Release()
{
_pmtx.lock();
if (--(*_pcount) == 0)
{
if(_ptr)
{
delete _ptr;
}
delete _pcount;
}
_pmtx.unlock();
}
void AddCount()
{
_pmtx.lock();
++(*_pcount);
_pmtx.unlock();
}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._str)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp->_pmtx;
AddCount();
}
return *this;
}
//防拷贝
//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
//C++11思路:函数后= delete
//unique_ptr(const unique_ptr<T>& up) = delete;
//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* pcount;
mutex* _pmtx;
};
在Release那里,到了引用计数减到0时,需要释放引用计数,释放锁。如果是在if里释放锁,那么外面的解锁操作就有问题了。 解决办法是可以设置一个状态位
void Release()
{
_pmtx.lock();
bool deleteFlag = false;
if (--(*_pcount) == 0)
{
if(_ptr)
{
delete _ptr;
}
delete _pcount;
deleteFlag = true;
}
_pmtx.unlock();
if (deleteFlag)
{
delete _pmtx;
}
}
shared_ptr本身是线程安全的,因为计数是加锁保护的,它实例化的对象不是线程安全的,想要线程安全,那么在对对象操作时用锁保护就行。
2、循环引用
写一个场景,还是用上面的shared_ptr
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
void Release()
{
_pmtx.lock();
bool deleteFlag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
delete _ptr;
}
delete _pcount;
deleteFlag = true;
}
_pmtx.unlock();
if (deleteFlag)
{
delete _pmtx;
}
}
void AddCount()
{
_pmtx.lock();
++(*_pcount);
_pmtx.unlock();
}
~shared_ptr()
{
Release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._str)
{
Release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp->_pmtx;
AddCount();
}
return *this;
}
//防拷贝
//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
//C++11思路:函数后= delete
//unique_ptr(const unique_ptr<T>& up) = delete;
//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* pcount;
mutex* _pmtx;
};
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
shared_ptr<ListNode> n1 = new ListNode;
shared_ptr<ListNode> n2 = new ListNode;
return 0;
}
当尝试连接两个节点时就发生了错误
n1->_next = n2;
n2->_prev = n1;
n1和n2是智能指针类型,而next和prev是ListNode类型的,无法赋值,那把ListNode里的两个指针换成shared_ptr< ListNode >类型的,但这样还不行,因为我们在定义next和prev时没有传参,是无参构造,所以在智能指针的类里应当写上缺省参数。
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
struct ListNode
{
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
现在有一个问题
n1->_next = n2;
n2->_prev = n1;
如果两句都写,程序会不释放资源,而如果只写一句或者两句都不写,那就会释放资源,就会打印"~ListNode",使用库中的智能指针也是这样,这就是智能指针引起的循环引用问题。
n1和n2都有各自的next和prev,如果不相互连接,也就是什么都不写,那么next和prev随着n1n2销毁而销毁。
写了一句,比如n1->_next = n2,那么n2这个节点除了它本身,还有n1的next指向它,n2析构时,引用计数–,但是空间不销毁,n1析构时,里面的成员变量也会随着析构,那么整体也可以完好地退出。
但是两句都写就出问题了。
n1->_next = n2;
n2->_prev = n1;
出了作用域,n2先析构,引用计数–,但是还不能销毁空间,引用计数没有为0,也还有一个指针指向它;n1析构时,n1也是一样,也不能析构,引用计数–,现在这两个空间的引用计数都为1,n1的next指向n2的空间,n2的prev指向n1的空间,那么n1这个空间什么时候析构?要看prev,prev析构,n1这个空间就析构,但是n2这个空间由next指向,next析构,n2才能析构,prev才能析构,所以next和prev已经形成了相互制约的关系,没办法全部析构了。这就是循环引用,会导致内存泄漏。
为了解决这个问题,标准库中有个weak_ptr来辅助shared_ptr,也叫做弱指针。weak_ptr不是RAII的,也就是它不是常规的智能指针,但是支持像指针一样,专门用来解决shared_ptr的辅助引用问题。用weak这样写。
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
weak_ptr不会增加引用计数。标准库中weak_ptr实现得很复杂,我们这里只模拟实现一个简单的
template <class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
struct ListNode
{
zyd::weak_ptr<ListNode> _next;
zyd::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode" << endl;
}
};
int main()
{
zyd::shared_ptr<ListNode> n1 = new ListNode;
zyd::shared_ptr<ListNode> n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
return 0;
}
3、定制删除器
在实例化的时候,传new int[10]这样的话,可能会崩溃,是因为new []会在开辟的空间前再开辟一个存放元素个数的空间,但是delete的时候会从开辟的空间开始释放,而不包含那个存储个数的空间,所以本质上是释放的位置不对。
定制删除器本质上是一个可调用对象,函数指针,仿函数,lambda都可以。
template <class T>
struct DeleteArray
{
void operator()(T* ptr)
{
cout << "仿函数" << endl;
delete[] ptr;
}
};
int main()
{
//zyd::shared_ptr<ListNode> n1 = new ListNode;
//zyd::shared_ptr<ListNode> n2 = new ListNode;
//n1->_next = n2;
//n2->_prev = n1;
std::shared_ptr(int) spa1(new int[10], DeleteArray<int>());//仿函数
std::shared_ptr(int) spa2(new int[10], [](int* ptr) {delete[] ptr; });//lambda
return 0;
}
库中的做法是把这个删除器放到构造函数里,实例化的时候传过来,保存起来,析构时用它去析构。这里的重点在于如何保存这个删除器。一个是我们可以在总的模板参数那里加一个模板参数,那么析构函数就可以直接用,也不用在构造函数那里在写上一个模板参数;或者用包装器。这里写包装器。
template <class D>
shared_ptr(const shared_ptr<T>& sp, D del)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
, _del(del)
{
AddCount();
}
void Release()
{
_pmtx.lock();
bool deleteFlag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
//delete _ptr;
_del(_ptr);
}
delete _pcount;
deleteFlag = true;
}
_pmtx.unlock();
if (deleteFlag)
{
delete _pmtx;
}
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
functional<void(T*)> _del;
这样写其实会有问题,如果用不到这个删除器就会调用默认构造,删除器没有初始化,到了析构时,删除器就是被编译器默认初始化的,用它来析构就容易出问题。我们可以用缺省
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
functional<void(T*)> _del = [](T* ptr) {
cout << "lambda delete:" << ptr << endl;
delete ptr;
};
定制删除器当作了解,重点在于shared_ptr的实现。
本篇gitee
结束。