C++11智能指针
1.写在前面
学完了继承多态,二叉搜索树,map和set,AVL树,红黑树,红黑树的封装,哈希表的底层,C++11新特性,异常,现在我们也迎来了C++的学习的一个尾声智能指针。
目录
1.写在前面
2.智能指针
3.C++标准库智能指针的应用
4.智能指针的原理
unique_ptr实现代码:
weak_ptr实现代码:
2.智能指针
书接上文,我们在知道了new之后有可能抛异常,new一次可能抛一次,如果要去try,catch代码会太过繁琐,所以我们一般不这么搞,所以我们引入了智能指针。
下面的代码我们连续创建两次数组,第一次创建可能抛异常,它就没创建没什么事,但是第二次创建抛异常就可能会造成内存泄漏了。我们解决是去套try,catch但这样太过繁琐了。
解决办法:引入智能指针。
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针,否则代码太戳了
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
// ...
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
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;
}
有的同学要问了,下面不是有catch(...)吗?怎么不能捕获吗?其实,抛异常的代码只有在try,catch中才能被捕获的。
那么什么是智能指针呢?就是自动管理资源,委托给智能指针取管理资源的释放,而不是自己去手动释放。本质就是把资源委托给对象,在cpp中,对象的学习与应用贯穿始终,我们的对象中有构造函数,拷贝构造函数,移动构造等等,当它的生命周期结束时,自动调析构来释放资源,这样我们就实现了交给对象去管理资源的释放了。
看起来很简单,说白了就是把资源的管理权交给对象,让对象利用析构来自动释放资源,防止了内存泄漏。但是其中还有很多细节需要注意。众所周知,简单的事做好很不简单,我们再来细细说道。
这时候问题会出现,当我用另一个对象去拷贝这个对象,会析构两次导致程序崩掉,有的同学就要说了,那我们不用编译器自动生成的浅拷贝,我们自己写一个深拷贝呢?nonono!!!,我们搞出智能指针的目的就是共同管理资源,浅拷贝是符合我们的意愿的,只不过它会析构两次,因为它们的资源都指向了同一块,那么我们就来解决这个问题。
3.C++标准库智能指针的应用
1.C++标准库智能指针放在<memory>下,我们在头文件包含<memory>就可以使用它。
2.C++98中,就曾有过智能指针叫auto_ptr,但是它设计的非常糟糕,因为它的拷贝是管理权的转移,意思是我用一个对象去拷贝它,我自己的资源就没了,这是非常糟糕的,如果有人不知道,这种行为会非常危险。
sp3(sp1)
这里拷贝之后,sp1的资源管理权被转移给sp3,这样sp1就不管理资源了,如果有人不知道,会导致悬空。故说它设计的糟糕。
3.C++11痛改前非,设计出了三种智能指针,先来第一个unique_ptr,它的意思是独特的指针,唯一指针,不支持拷贝,只支持移动,意思是它也是管理权的转移,但是我们这个和auto_ptr的区别是我们是知道我们()里的必须是右值的。
它的实现原理也非常简单啊,就是禁掉编译器自动生成拷贝构造就可以而来。
其中有几个接口我们了解一下,get()是获取底层的指针,release是释放资源手动置空,reset是释放之前的管理新来的。
其中我们来着重了解一下operator bool()这个接口,我们知道内置类型和自定义类型可以转换成自定义类型,但是其他类型能转换成内置类型吗?答案是能,C++11设计了operator boll这个接口判断智能指针是否管理了资源,我们一般这样写
if(up1)
{
}
其实也可以这样写up1.operator bool()也是一样的,有资源返回true,没有返回false。
总结unique_ptr就是不拷贝用它。
那问题来了我们也不能一直不拷贝把,我就想拷贝共同管理一份资源怎么办呢?C++11也设计了一个提供拷贝的shared_ptr,它的底层原理是引用计数。下面我们来了解它。
4.shared_ptr提供拷贝,让多个对象来管理同一份资源,底层是引用计数,当计数归0才析构它。在极端情况下shared_ptr会陷入循环引用的情况无法析构,我们又为了解决这个问题设计出了weak_ptr。
总结一下,其实我们实际应用上只有unique_ptr和shared_pty,前者是只支持移动,不支持拷贝,后者是支持了拷贝。weak_ptr是为了专门解决shared_ptr循环引用来设计出来的,不拷贝坚决用unique_ptr,拷贝了用shared_ptr。

struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year),_month(month)
,_day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
// 拷⻉时,管理权限转移,被拷⻉对象ap1悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1对象已经悬空
//ap1->_year++;
unique_ptr<Date> up1(new Date);
// 不⽀持拷⻉
//unique_ptr<Date> up2(up1);
// ⽀持移动,但是移动后up1也悬空,所以使⽤移动要谨慎
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
// ⽀持拷⻉
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;}
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
// 这样实现程序会崩溃
// unique_ptr<Date> up1(new Date[10]);
// shared_ptr<Date> sp1(new Date[10]);
// 解决⽅案1
// 因为new[]经常使⽤,所以unique_ptr和shared_ptr
// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
// 解决⽅案2
// 仿函数对象做删除器
//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>
());
// unique_ptr和shared_ptr⽀持删除器的⽅式有所不同
// unique_ptr是在类模板参数⽀持的,shared_ptr是构造函数参数⽀持的
// 这⾥没有使⽤相同的⽅式还是挺坑的
// 使⽤仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调⽤
// 但是下⾯的函数指针和lambda的类型不可以
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) {delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
// 报错
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
4.智能指针的原理
下面我们先来说一下简单的unique_ptr,它的试卷方式就是把编译器默认生成的拷贝构造等禁掉,只允许一个移动构造就可以了,下面我们着重来用代码实现一下shared_ptr,我们上文中提了一下,shared_ptr它的底层是运用引用计数来实现的,其中我们实现中最要注意的是赋值运算符重载,下面我们来实现:
shared_ptr实现代码
template<class T>
class shared_ptr
{
public:explicit shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}void release(){if (--(*_pcount) == 0){// 最后⼀个管理的对象,释放资源delete _ptr;delete _pcount;_ptr = nullptr;_pcount = nullptr;}}T* get(){return _ptr;}int use_count(){return *(_pcount);}shared_ptr<T>& operator=(const shared_ptr<T>&sp){if (sp._ptr != _ptr){_pcount--;if (_pcount == 0){delete _ptr;_ptr = nullptr;}else{_pcount = sp._pcount;_ptr = sp._ptr;*(_pcount)++;}}return *this;}~shared_ptr(){release();}T& operator*(){return *_ptr;}T* operator->()//省略了一个->{return _ptr;}
private:T* _ptr;int* _pcount;
};
unique_ptr实现代码:
template<class T>
class unique_ptr
{
public:
explicit 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>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
weak_ptr实现代码:
// 需要注意的是我们这⾥实现的shared_ptr和weak_ptr都是以最简洁的⽅式实现的,
// 只能满⾜基本的功能,这⾥的weak_ptr lock等功能是⽆法实现的,想要实现就要
// 把shared_ptr和weak_ptr⼀起改了,把引⽤计数拿出来放到⼀个单独类型,shared_ptr
// 和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}private:
T* _ptr = nullptr;
};
}
今天我们的学习就到这里了,内容太多,我们下节博客再来详细解释一下这些智能指针的其他细节。再见。