目录
前言
上文提及如下程序是一个不安全的程序,因为当第二次以及第二次以后开辟空间失败,程序抛出异常时,原先开辟的资源没有被释放,从而导致内存泄漏,上文给出C++98的解决方案,在此不再过多赘述;
void Func()
{
int* p1 = new int[10];
int* p2 = new int[20];
int* p3 = new int[30];
delete[] p1;
delete[] p2;
delete[] p3;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "unknow exception" << endl;
}
return 0;
}
C++11解决上述问题的思想是无论函数是正常结束或者抛异常结束,每层函数栈帧空间都是正常结束,可以利用对象的生命周期来控制程序资源,即创建对象时,自动调用构造函数,因此借助构造函数保存资源,当对象的生命周期结束时,自动调用析构函数,因此可以借助析构函数释放资源;
template<class T>
class SmartPtr
{
public:
//借助构造函数保存资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
//借助析构函数释放资源
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete[] _ptr;
}
private:
T* _ptr;
};
void Func()
{
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[20];
SmartPtr<int> sp3 = new int[30];
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "unknow exception" << endl;
}
return 0;
}
运行结果:
- 当第一次开辟空间失败时,对象sp1未构造(传递给构造函数的参数ptr没有传递);
- 当第二次开辟空间失败时,对象sp2未构造,但是对象sp1会在Func函数的函数栈帧结束时调用析构函数,释放空间;
- 当第三次开辟空间失败时,对象sp3未构造,但是对象sp1、sp2会在Func函数的函数栈帧结束时调用析构函数,释放空间;
智能指针的使用与原理
RAII
RAII是一种利用对象生命周期来控制程序资源的简单技术;
首先在对象构造时获取资源,其次在对象的生命周期内始终可以访问资源,最后在对象析构的时候释放资源;因此实际上把管理一份资源的责任托管给一个对象; 这种处理方式存在两大好处:
不需要显式地释放资源;
采用此方式,对象所需的资源在其生命期内始终保持有效;
template<class T>
class SmartPtr
{
public:
//借助构造函数保存资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
//借助析构函数释放资源
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为;指针可以解引用,也可以通过->去访问所指空间中的内容,因此:SmartPtr模板类中还需要将 * 、->重载,才可让其像指针一样去使用;
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<int> sp1(new int(0));
SmartPtr<int> sp2(new int(0));
*sp1 = 10;//测试operator*()
SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";// <====> sp3.operator->()->first = "apple";
sp3->second = 1;//<====> sp3.operator->()->second = 1;
return 0;
}
运行结果:
智能指针原理总结:
- 具有RAII特性;
- 重载operator * 与operator ->,具有指针一样的行为;
智能指针的拷贝
template<class T>
class SmartPtr
{
public:
//借助构造函数保存资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
//借助析构函数释放资源
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<int> sp1(new int(10));
SmartPtr<int> sp2(sp1);
return 0;
}
运行结果:
对象sp1拷贝构造sp2,调用拷贝构造函数,对于内置类型成员进行浅拷贝即值拷贝,对象sp1与对象sp2成员变量_ptr指向同一块空间,对象sp1与对象sp2销毁时,调用析构函数,导致同一块空间二次释放,造成系统崩溃;
若想解决浅拷贝造成的二次析构问题,就要实现深拷贝的拷贝构造函数与拷贝赋值函数吗?
vector<int> v1 = { 1, 2, 3, 4, 5 };
vector<int> v2(v1);
SmartPtr<int> sp1(new int(10));
SmartPtr<int> sp2(sp1);
std::auto_ptr
#include <memory>
int main()
{
std::auto_ptr<int> ap1(new int(10));
std::auto_ptr<int> ap2(ap1);
return 0;
}
监视窗口:
C++标准库中的auto_ptr的本质为将管理权转移,即被拷贝对象ap1将资源管理权转移给拷贝对象ap2,但是导致了被拷贝对象ap1悬空,当使用一个对象拷贝构造另一个对象时,拷贝结束时,被拷贝对象不可被访问,若使用被拷贝对象,易造成系统崩溃;
#include <memory>
int main()
{
std::auto_ptr<int> ap1(new int(10));
std::auto_ptr<int> ap2(ap1);
*ap1 = 20;//对象ap1管理的空间数值修改为20
return 0;
}
运行结果:
auto_ptr的模拟实现
namespace SmartPtr
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
//ap2=ap1
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
//检测是否为自己给自己赋值
if (this != &ap)
{
//释放当前对象中的资源
if (_ptr)
{
delete _ptr;
}
//转移ap中的资源到当前对象中
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
结论:auto_ptr是一个失败的设计,也是C++标准库中的耻辱,巨坑无比,因此许多公司明确禁止使用auto_ptr;
std::unique_ptr
#include <memory>
int main()
{
std::unique_ptr<int> ap1(new int(10));
std::unique_ptr<int> ap2(ap1);// err: unique_ptr类的拷贝构造函数已删除
return 0;
}
C++标准库中的unqiue_ptr的本质为禁止拷贝,实现方式有两种,C++98中,拷贝构造函数只声明不实现并且将其置于私有;C++11中,拷贝构造函数声明=delete;
unique_ptr的模拟实现
namespace SmartPtr
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//禁用拷贝构造函数
unique_ptr(const unique_ptr<T>& ap) = delete;
//禁用拷贝赋值函数
unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
需要注意的是必须禁用赋值运算符重载函数,若不禁用,由于赋值运算符为默认成员函数,编译器自动生成,对于内置类型成员为值拷贝,浅拷贝会导致同一块空间重复释放;
std::shared_ptr
shared_ptr的原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源
#include <memory>
int main()
{
std::shared_ptr<int> sp1(new int(1));//std::shared_ptr::use_count
cout << sp1.use_count() << endl;//use_count()查看多少对象管理此资源
std::shared_ptr<int> sp2(sp1);
cout << sp1.use_count() << endl;
*sp1 += 10;
*sp2 += 10;
return 0;
}
运行结果:
shared_ptr的模拟实现
//设计方案一:每个对象各自有各自的计数
template<class T>
class shared_ptr
{
public:
//...
private:
T* _ptr;
size_t _count;//_count记录多少对象管理_ptr指向的空间
};
上述实现方案是否可行?(绝对不可行)
shared_ptr<int> sp1;
shared_ptr<int> sp2(sp1);
对象析构时,先析构sp2再析构sp1,销毁对象sp2时,引用计数减一,释放空间,但是对象sp1的引用计数仍为1,当销毁对象sp1时,仍然释放已经释放的空间;
//设计方案二:采用整个类专属的静态成员变量完成计数
template<class T>
class shared_ptr
{
public:
//...
private:
T* _ptr;
static size_t _count;//静态成员变量_count记录多少对象管理此资源
};
//静态成员变量初始化
template<class T>//类外需要模版参数,类外声明模版参数
size_t shared_ptr<T>::_count = 0;
上述实现方案是否可行?
shared_ptr<int> sp1;
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3(new int(5));
引用计数是每个资源配备一份引用计数,当开辟新空间时,应该给新开辟的空间配备一份新的引用计数,但是静态成员变量属于整个类,属于这个类的所有对象,即新开辟空间的引用计数只有原先的静态计数_count而且将_count改为1,所以上述方案也不可行;
namespace SmartPtr
{
template<class T>
class shared_ptr
{
public:
// RAII
// 创建对象时才需要开辟计数空间并且将引用计数的值置为1
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
// 析构时,--计数,计数减到0,
// 说明最后一个管理对象销毁则可以释放资源
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
// 拷贝构造 sp2(sp1)
shared_ptr(const shared_ptr<T>& sp)
{
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
// 行为类似指针
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//获取原生指针
T* get() const
{
return _ptr;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};
}
赋值运算符重载
//sp1=sp2
//this-->赋值对象 sp-->被赋值对象
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止case3的出现,无论自身给自身赋值或者不同的对象管理同一份资源均不发生任何变化
if (_ptr != sp._ptr)
{
//首先赋值对象的引用计数自减1,若减到0即case2,若没有减到0即case1;
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
// 拷贝时++计数
++(*_pcount);
}
return *this;
}
shared_ptr的循环引用
shared_ptr的致命缺陷 —— 循环引用
- 循环引用: 两个或多个对象中shared_ptr成员变量指向对方(即对象之间相互持有彼此的引用计数)造成循环引用,导致对象的引用计数无法归0,从而无法释放对象所占用的内存;
struct ListNode
{
int _val;
SmartPtr::shared_ptr<ListNode> _next;
SmartPtr::shared_ptr<ListNode> _prev;
ListNode(int val=0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
SmartPtr::shared_ptr<ListNode> n1(new ListNode(10));
SmartPtr::shared_ptr<ListNode> n2(new ListNode(20));
n1->_next = n2;
n2->_prev = n1;
return 0;
}
解决方案:C++标准库引入了weak_ptr配合shared_ptr解决循环引用,weak_ptr(弱引用智能指针)可以指向shared_ptr持有的对象并且不会增加对象的引用计数;通过weak_ptr可以打破循环引用使得对象的引用计数正确下降为0,从而触发析构函数的调用;
std::weak_ptr
注意:weak_ptr<T>模版类没有重载operator * 与operator ->,因此weak_ptr类型指针只能访问某一shared_ptr指针指向的堆内存空间,无法对其进行修改;
//简化版本weak_ptr的模拟实现
namespace SmartPtr
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
// 拷贝构造
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp.get();
}
weak_ptr(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
}
//operator=
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}
private:
T* _ptr;
};
}
使用weak_ptr解决shared_ptr的循环引用问题,解决方案如下:
struct ListNode
{
int _val;
SmartPtr::weak_ptr<ListNode> _next;
SmartPtr::weak_ptr<ListNode> _prev;
ListNode(int val=0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
SmartPtr::shared_ptr<ListNode> n1(new ListNode(10));
SmartPtr::shared_ptr<ListNode> n2(new ListNode(20));
n1->_next = n2;
n2->_prev = n1;
return 0;
}
运行结果:
欢迎大家批评指正,博主会持续输出优质内容,谢谢各位观众老爷观看,码字画图不易,希望大家给个一键三连支持~ 你的支持是我创作的不竭动力~