当前位置: 首页 > news >正文

C++11智能指针

1.写在前面

学完了继承多态,二叉搜索树,map和set,AVL树,红黑树,红黑树的封装,哈希表的底层,C++11新特性,异常,现在我们也迎来了C++的学习的一个尾声智能指针。

目录

1.写在前面

2.智能指针

3.C++标准库智能指针的应用

4.智能指针的原理

shared_ptr实现代码

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。

5.智能指针析构时默认是进⾏delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。智能指针⽀持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调⽤ 对象,这个可调⽤对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制器,
在智能指针析构时就会调⽤删除器去释放资源。因为new[]经常使⽤,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,使⽤时 unique_ptr<Date[]> up1(new
Date[5]);shared_ptr<Date[]> sp1(new Date[5]); 就可以管理new []的资源。
下面我们再详细解释一下这句话,意思就是我们的智能指针底层都是delete 的,但是如果资源是多个的我们就无法析构,我们之前学过一个对象是delete,但是一个数组里的对象是配套delete[]使用的,这时候不搞一下就会报错,所以我们用lambda去搞一个定制删除器,不管它是malloc出来的资源还是new出来一个对象还是多个对象都可以正确去释放它,下方的代码有具体案例。
   
6.
shared_ptr 除了⽀持⽤指向资源的指针构造,还⽀持 make_shared ⽤初始化资源对象的值
直接构造。
7.
shared_ptr unique_ptr 都⽀持了operator bool的类型转换,如果智能指针对象是⼀个
空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断
是否为空。
8.
shared_ptr unique_ptr 都得构造函数都使⽤explicit 修饰,防⽌普通指针隐式类型转换
成智能指针对象。
结合下图理解,我们等号右边是普通指针,等号左边是智能指针,如果不禁掉隐式转换,它可能就把普通指针转换成智能指针取搞了。我们以后等号右边是智能指针其实它是普通指针。
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;
};
}

今天我们的学习就到这里了,内容太多,我们下节博客再来详细解释一下这些智能指针的其他细节。再见。

http://www.xdnf.cn/news/944029.html

相关文章:

  • Linux入门(十五)安装java安装tomcat安装dotnet安装mysql
  • 虚拟机网络不通的问题(这里以win10的问题为主,模式NAT)
  • Python爬虫(52)Scrapy-Redis分布式爬虫架构实战:IP代理池深度集成与跨地域数据采集
  • 使用 C# 将 Word、Excel、PDF 和 PPT文档转换为 Markdown 格式
  • 《C++初阶之入门基础》【普通引用 + 常量引用 + 内联函数 + nullptr】
  • 【BUG】记STM32F030多通道ADC DMA读取乱序问题
  • 2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
  • 曲面的存在性定理
  • 【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
  • 【医疗电子技术】新型医疗电子和医学人工智能发展现状和趋势
  • 【异常】极端事件的概率衰减方式(指数幂律衰减)
  • 漏洞检测方案如何选工具?开源与商业工具适用环境大不同
  • 【时序预测】-Transformer系列
  • Hibernate Validator 数据验证
  • pymongo配置事务环境并封装事务功能
  • JDBC基础关键_001_认识
  • Spring类型转换器相关接口和实现原理
  • 【JavaScript】利用`localStorage`实现多窗口数据交互同步【附完整源码】
  • OD 算法题 B卷【删除字符串中出现次数最少的字符】
  • 如何禁用windows server系统自动更新并防止自动重启
  • 推理式奖励模型:使用自然语言反馈改进强化学习效果
  • 卫星接收天线G/T值怎么计算?附G/T计算excel表格链接
  • 打卡day48
  • 12.7Swing控件5 JProgressBar
  • Spring AI中使用ChatMemory实现会话记忆功能
  • 算法打卡第18天
  • 【CUDA 】第5章 共享内存和常量内存——5.3减少全局内存访问(2)
  • Linux 环境配置
  • 【立体匹配】:双目立体匹配SGBM:(1)运行
  • 深入解析JavaScript构造函数与原型链