C第二十一讲智能指针
C++第二十一讲:智能指针
1.智能指针使用场景分析
智能指针的使用大多发生在容易出现内存泄漏的地方,因为对于某些场景,稍微的操作不当就容易造成内存泄漏,而智能指针的实现,让我们不再需要担心内存泄漏的问题,它能够帮助我们进行内存的释放:
double divided(int a, int b)
{
if (b == 0) throw("Divided by zero condition!");
else return (double)a / (double)b;
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << divided(len, time) << endl;
}
catch (...)
{
//1.如果在这里捕获到异常,需要进行delete释放资源
delete[] array1;
delete[] array2;
throw; //异常重新抛出,捕获到什么抛出什么
}
//2.如果没有抛出异常,那么这里也要进行空间的释放
delete[] array1;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
上面1、2位置的delete是必不可少的,如果少一个就会导致内存泄漏的问题,而且如果需要释放多个资源,那么还需要手动写出多个delete,这样是很麻烦的,下面我们使用智能指针的知识来解决上面的问题
2.RAII和智能指针的设计思路
什么是RAII:RAII是和资源管理有关的一种编程技术,本质是通过对象的生命周期自动化资源管理,比如智能指针的自动释放资源、文件操作的自动关闭文件、锁管理的自动解锁,防止死锁问题
智能指针除了满足RAII的设计思路外,还要方便资源的访问,所以指针指针还像迭代器一样,重载了operator*/operator->/operator[]等运算符,方便访问资源
下面我们看一下RAII思想的智能指针的简单实现:
namespace YWL
{
template<class T>
class Smart_ptr
{
public:
Smart_ptr(T* ptr)
:_ptr(ptr)
{}
~Smart_ptr()
{
delete[] _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
}
此时就不需要再进行析构了,因为当smart_ptr初始化的对象生命周期一到,就会自动执行析构:
double divided(int a, int b)
{
if (b == 0) throw("Divided by zero condition!");
else return (double)a / (double)b;
}
void Func()
{
YWL::Smart_ptr<int> array1(new int[10]);
YWL::Smart_ptr<int> array2(new int[10]);
try
{
int len, time;
cin >> len >> time;
cout << divided(len, time) << endl;
}
catch (...)
{
throw; //异常重新抛出,捕获到什么抛出什么
}
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
3.C++标准库智能指针的使用
C++标准库中实现的智能指针都被保存在这个头文件中,智能指针有好几种,它们的主要区别在于底层实现的不同
3.1auto_ptr
auto_ptr是C++98实现的智能指针,它的特点是拷贝时将被拷贝对象的资源的管理权交给拷贝对象,这个设计非常糟糕,因为它会导致被拷贝对象悬空,访问报错的问题,所以强烈不建议使用auto_ptr:
#include<memory>
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
auto_ptr<Date> ptr1(new Date());
auto_ptr<Date> ptr2(ptr1);//auto_ptr拷贝时会将ptr1置空,从而造成问题
//ptr1->_day++;//进程直接终止
return 0;
}
我们可以查看auto_ptr的模拟实现来观察它的设计:
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr) delete _ptr;
}
auto_ptr(auto_ptr<T>& autoptr)
:_ptr(autoptr._ptr)
{
autoptr._ptr = nullptr;//转移管理权
}
auto_ptr& operator=(auto_ptr<T>& ap)
{
//检测是否为⾃⼰给⾃⼰赋值
if (this != &ap)
{
//释放当前对象中资源
if (_ptr)
delete _ptr;
//转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.2unique_ptr
unique_ptr是C++11设计出来的智能指针,它的特点是不支持拷贝,只支持移动
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
unique_ptr<Date> ptr1(new Date());
//不支持拷贝
//unique_ptr<Date> ptr2(ptr1);
//只支持移动,但是移动后ptr1也会悬空
unique_ptr<Date> ptr2(move(ptr1));
//ptr1->_day++;
return 0;
}
它的实现也是相对来说比较简单的:
template<class T>
class unique_ptr
{
public:
//explicit的作用为防止不必要的类型转换unique_ptr<Date> ptr3 = new Date();
//可以避免上面代码情况的发生,也就是不支持隐式类型转换,new Date并不是unique_ptr<Date>类型的
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr) delete _ptr;
}
//不支持拷贝,只支持移动
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr(unique_ptr<T>&& up)
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
//不支持拷贝,只支持移动
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
3.3shared_ptr
shared_ptr是C++11设计设计出来的智能指针,它的特点是既支持拷贝,又支持移动,使用智能指针时,建议使用shared_ptr,而它的拷贝不是像auto_ptr那样的资源管理权的转移,而是采用了引用计数的方法:
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
shared_ptr<Date> ptr1(new Date());
shared_ptr<Date> ptr2(ptr1);
cout << ptr1->_day << endl;//1
ptr1->_day++;
cout << ptr2->_day << endl;//2
return 0;
}
3.3.1删除器
在实现shared_ptr之前,我们需要先强调一下删除器的概念:
智能指针析构默认是delete释放资源,如果不是new出来的资源,交给智能指针管理,析构时就会崩溃,而且对于数组类型的对象,delete释放资源也会造成报错:
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
int _year;
int _month;
int _day;
};
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
int main()
{
//1.对于数组类型的对象,这样写会导致程序崩溃
//shared_ptr<Date> ptr1(new Date[10]);
//解决方案一:因为new[]经常使用,所以C++11对于unique_ptr和shared_ptr
// 都特化了一份[]的版本
shared_ptr<Date[]> ptr2(new Date[10]);
unique_ptr<Date[]> ptr3(new Date[10]);
//解决方案二:使用删除器
//unique_ptr和shared_ptr使用删除器的方法不同,这点设计的很麻烦,总而言之:
//unique_ptr需要指定删除器的类型,还需要指定删除器的指针
//shared_ptr只需要指定删除其的指针即可
//1.使用函数指针作删除器
unique_ptr<Date, void(*)(Date*)> ptr4(new Date[10], DeleteArrayFunc<Date>);
shared_ptr<Date> ptr5(new Date[10], DeleteArrayFunc<Date>);
//2.仿函数对象作删除器
unique_ptr<Date, DeleteArray<Date>> ptr6 (new Date[10]);
shared_ptr<Date> ptr7(new Date[10], DeleteArray<Date>());
//3.lambda表达式作删除器
auto DeleteArray1 = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(DeleteArray1)> ptr8(new Date[10], DeleteArray1);
shared_ptr<Date> ptr9(new Date[10], [](Date* ptr) { delete[] ptr; });
//4.实现其他资源管理的删除器
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
下面我们再来实现一下shared_ptr:
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
//当需要析构时,再进行析构
if(--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
if (_ptr && _pcount == 0)
{
delete _ptr;
delete _pcount;
cout << "~shared_ptr" << endl;
}
}
private:
T* _ptr;
int* _pcount;
};
带删除器的shared_ptr的实现:
对于删除器需要修改的点在于:
1.默认构造需要通过传入的删除器进行初始化
2.如果没有传入删除器,那么应该使用什么进行删除
3.析构函数里需要使用到删除器
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
//删除器需要一个类型,那么我们就使用函数模板
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(new int(1))
,_del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
,_del(sp._del)
{
++(*_pcount);
}
void release()
{
if (--*(_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
3.4标准库中的智能指针问题
3.4.1operator bool(shared_ptr && unique_ptr)
C++标准库中支持直接使用if(ptr1)的方式来判断ptr是否为空,也就是是否指向了一块有效的空间,其实应该的使用方法为:ptr1.operator bool(),但是简化为可以直接使用了
3.4.2make_shared(shared_ptr)
我们可以使用shared_ptr sp2 = make_shared(2024, 9, 11);来直接构造一个智能指针,效果和shared_ptr sp1(new Date(2024, 9, 11));是相同的
3.4.3use_cout
支持ptr1.use_cout()来获取ptr中有多少个指针指向这块资源
4.weak_ptr
4.1shared_ptr的循环引用问题
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> ptr1(new ListNode());
shared_ptr<ListNode> ptr2(new ListNode());
cout << ptr1.use_count() << endl;//1
cout << ptr2.use_count() << endl;//1
ptr1->_next = ptr2;
ptr2->_prev = ptr1;
cout << ptr1.use_count() << endl;//2
cout << ptr2.use_count() << endl;//2
return 0;
}
4.2weak_ptr
使用weak_ptr可以解决循环引用的问题:
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> ptr1(new ListNode());
shared_ptr<ListNode> ptr2(new ListNode());
cout << ptr1.use_count() << endl;//1
cout << ptr2.use_count() << endl;//1
ptr1->_next = ptr2;
ptr2->_prev = ptr1;
cout << ptr1.use_count() << endl;//1
cout << ptr2.use_count() << endl;//1
return 0;
}
weak_ptr的解决原理为:weak_ptr初始化的指针能够指向shared_ptr指向的那一块空间,而且不增加shared_ptr的引用计数,也就是说,此时shared_ptr的next和prev指针的使用不会增加引用计数了,所以就可以顺利进行析构了
4.2.1weak_ptr的使用
weak_ptr并没有重载operator* 和 operator->,所以它并不能够进行资源的访问,但是它支持调用lock返回一个管理资源的shared_ptr,如果资源已经释放,那么shared_ptr返回空对象,否则就可以正常进行资源的访问:
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
//1.expired可以检查资源是否过期,过期返回1,否则返回0
cout << wp.expired() << endl;//0
cout << wp.use_count() << endl;//2,wp的指向不增加引用计数
//2.使用lock函数可以进行资源的访问
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << *sp3 << endl;//111111
cout << wp.use_count() << endl;//3,新增了一个指向
*sp3 += "###";
return 0;
}
weak_ptr的实现:
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())//get函数可以获取sp指向的那一块空间的地址
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
5.shared_ptr的线程安全问题
这个在后面会讲到,后面再说