【C++】智能指针

【C++】智能指针
前言

        上文我们学到了C++11的异常,了解到了C++与C语言处理错误的区别,异常的特点在于抛出与接收。【C++11】异常-ZEEKLOG博客

        本文我们来学习C++中的下一个功能:智能指针

1.智能指针的使用场景

        在上文我们知道了抛异常的知识,抛异常的“抛”这个动作一般来说是当程序出现了错误,抛出错误信息为了让我们解决。这个原本是解决错误的动作,在某些时候却称为了“铸就”错误的是罪魁祸首。

        比如:我们知道执行throw,这意味着在这个局部域中throw后面的语句将不再执行,跳过一段又一段程序直到找到匹配的catch时,才会从catch这个语句进行向下执行。那么一个局部域中如果在抛出异常时申请了空间,明明可以正常销毁的,但是却因为抛异常跳过了销毁空间的语句。这就导致一个及其严重的事故:内存泄漏!

        在此之前,为了防止出现内存泄漏。我们通常是将抛出的异常再次捕获,执行销毁语句后,将异常重新抛出。但是这种方法并不太好用,所以为了更好的解决这个问题:智能指针诞生了。

2.RAII和智能指针的设计思路 

        RAII(Resource Acquisition Is Initialization)是资源获取立即初始化的缩写。RAII是一种资源管理的类的设计思想,其本质就利用类的声明周期来管理资源,类的生命周期没结束时一直保持资源有效,类的生命周期结束时通过析构函数来释放资源。这样就可以保证出现上述情况时,资源可以正常的销毁,避免内存泄漏。(这里的资源可以是内存、文件指针、网络连接、互斥锁等等)

        智能指针除了会满足RAII的设计思路,还有考虑到访问资源的便捷性,所以智能指针还会重载operator * / operator -> / operator [ ]等运算符,便于访问。

#include<iostream> using namespace std; template<class T> class Smartptr { public: Smartptr(T* ptr) :_ptr(ptr) { } ~Smartptr() { cout << "~Smartptr" << endl; delete[] _ptr; _ptr = nullptr; } T* operator-> () { return _ptr; } T& operator* () { return *_ptr; } T& operator[] (size_t i) { return _ptr[i]; } private: T* _ptr; }; double Divide(int a, int b) { //当除数为0时抛异常 if (b == 0) { string s = "除数为0"; throw s; } return (double)a / b; } void Func() { //使用智能指针,抛异常后也可以正常释放 Smartptr<int> p1 = new int[10]; for (int i = 0; i < 10; i++) p1[i] = i; int a, b; cin >> a >> b; cout << Divide(a, b)<<endl; } int main() { try { Func(); } catch (const string& errid) { cout << errid << endl; } }

        当Func函数生命周期结束时,Smartptr会自动调用析构函数实现资源是释放。 

3.标准库中智能指针的使用

        标准库中的智能指针包含在头文件 <memory>

        智能指针是用于管理资源的,因此对于智能指针的拷贝来说,我们期望是拷贝出现的智能指针和和原指针共同管理这块资源的,而不是让拷贝出来的资源自己又去管理一个新资源。所以智能指针的拷贝只能是浅拷贝。浅拷贝就会面对一个问题:多次析构,而下面之所以有这么多不同的智能指针正式因为解决多次析构的方法不同导致的。

        auto_ptr,这个是C++98中提供的智能指针。它的特点是拷贝时将被拷贝对象的管理权转移给拷贝对象,这会导致拷贝对象悬空,当我们访问拷贝对象时就会报错。这是一个非常不好的设计,许多公司都是明令禁止使用这个智能指针的。

        unique_ptr,是C++11中提供的智能指针。其特点是不支持拷贝,只支持移动。如果不需要拷贝的情景下,非常推荐使用这个

        share_ptr,是C++11中提供的智能指针。其特点是支持拷贝,也支持移动。如果需要拷贝的场景推荐使用这个。其底层是使用引用计数实现的。

        weak_ptr,是C++11中提供的智能指针。虽然叫做智能指针但不太算得上,因为weak_ptr并不支持RAII,也就意味着weak_ptr并不能管理资源。weak_ptr的主要作用在于解决share_ptr的缺陷:循环引用。而循环引用带来的是内存泄漏。

#include<iostream> #include<memory> using namespace std; struct Date { Date(int year = 0,int month = 0,int day = 0) :_day(day) ,_month(month) ,_year(year) { } ~Date() { cout << "~Date" << endl; } int _day; int _month; int _year; }; int main() { auto_ptr<Date> ap1(new Date); //拷贝时,拷贝对象安排ap1会被悬空 auto_ptr<Date> ap2(ap1); //此时访问ap1就会报错 //ap1->_day; unique_ptr<Date> up1(new Date); //不支持拷贝 //unique_ptr<Date> up2(up1); // //支持移动,但是移动后up1也被悬空 unique_ptr<Date> up2(move(up1)); shared_ptr<Date> sp1(new Date); //支持拷贝 shared_ptr<Date> sp2(sp1); shared_ptr<Date> sp3(sp2); cout << sp1.use_count() << endl; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl; // ⽀持移动,但是移动后sp1也悬空,所以使用移动要谨慎 shared_ptr<Date> sp4(move(sp1)); }

         智能指针析构时默认是使用delete释放资源,这就意味这当资源不是通过new申请时,将资源交给智能指针,析构时就会出问题。

        为了解决这一问题,智能指针支持在构造函数里面给一个删除器。删除器其实就是一个可调用对象,我们按照自己想释放资源的方式实现删除器。智能指针接收后就会按照删除里实现的逻辑进行释放资源。

        因为使用delete[]释放资源的情况十分常用。使用库里面给我们专门特化了这个情况(share_ptr、unique_ptr均有特化版本)。只需要单独在尖括号里面加一对中括号即可。

        值得一提的是,删除器只能在构造时给出,并且后续不能修改。当shared_ptr利用已经存在的对象拷贝构造/赋值给新对象,这个新对象会继承其删除器,即使这并没有显示的写出。而unique_ptr则是将删除器移动给新对象。

 shared_ptr<int[]> sp(new int[10]); shared_ptr<Date[]> sp(new Date[10]);
#include<iostream> #include<memory> using namespace std; //总结:使用shared_ptr,建议传Lambda和函数 // 而使用unique_ptr,建议传Lambda 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; } }; template<class T> struct Delete { void operator()(T* ptr) { delete[] ptr; } }; int main() { //直接传仿函数 shared_ptr<Date> sp1(new Date[10], Delete<Date>()); //给出Lambda shared_ptr<Date> sp2(new Date[10], [](Date* ptr) {delete[] ptr; }); //值得一提的是unique_ptr与shared_ptr给出删除器的方法是不同的 //由上面我们可以知道shared_ptr是在构造函数里面给出的 //而unique_ptr的删除器是要在模板参数中给出,这就有点搞笑了 unique_ptr<Date, Delete<Date>> up1(new Date[10]); //传函数到还可以,但是这个相传Lambda就麻烦了,因为Lambda并非类型而是对象 //unique_ptr < Date, [](Date* ptr) {delete[] ptr; } > up2(new Date[10]); //具体要这样写才行,大家了解即可 auto la = [](Date* ptr) {delete[] ptr; }; unique_ptr< Date, decltype(la)> up2(new Date[10],la); }

         shared_ptr除了支持用指针构造,还支持使用make_shared直接构造。其好处是避免了空间的碎片化,因为shared_ptr内部成员除了指针还有一个引用计数。使用指针构造只是初始化了指针,还有一个引用计数没有初始化,为此编译器还会再去开辟空间用于初始化引用计数,而要是这种情况过多就会导致空间碎片化,不利用空间利用。而使用make_shared时就会将指针连同引用计数一起开辟,让这两个指针指向的空间连续。

        shared_ptr与unique_ptr都支持operator bool的类型转化。当智能指针的对象是一个空对象,没有管理资源,就会返回false,反之返回true。所以我们可以把智能指针的对象给if让其判断是否为空对象。

        shared_ptr和unique_ptr的构造函数都是用explicit修饰的,其目的是为了防止普通的指针隐式类型转换为智能指针类型。

int main() { shared_ptr<Date> sp1 = make_shared<Date>(1, 2, 3); auto sp2 = make_shared<Date>(1, 2, 3); //自动推导 shared_ptr<Date> sp3; if (sp2) cout << "不是空对象" << endl; if (!sp3) cout << "是空对象" << endl; //报错,不允许隐式类型转化 shared_ptr<Date> sp4 = new Date(1, 2, 3); shared_ptr<Date> sp5 = new Date(1, 2, 3); //仅支持显示类型转化 shared_ptr<Date> sp6 = shared_ptr<Date>(new Date(1, 2, 3)); }

4.智能指针原理

        下面将模拟实现三个智能指针的实现思路

        auto_ptr和unique_ptr比较简单。auto_ptr会将管理权转移,不建议使用。unique_ptr不支持拷贝,仅支持移动,我们将拷贝禁用掉即可。

        重点关注:share_ptr。share_ptr是靠引用计数实现的,share_ptr内部会有一个计数器记录有多少个指针共同管理当前资源。初始化为1,当拷贝时就将计数器++。析构时,当计数器不为1时仅将当前指针赋值为nullptr,当计数器为1时将计数器和资源释放。

//auto_ptr模拟实现 //其特点是管理权转移,不建议使用! template<class T> class auto_ptr { auto_ptr(T* ptr) :_ptr(ptr) { } //管理权限转移 auto_ptr(const auto_ptr<T>& ap) :_ptr(ap._ptr) { ap._ptr = nullptr; } auto_ptr<T>& operator=(const auto_ptr<T>& ap) { //检查是否赋值给自己 if (_ptr != ap._ptr) { //释放当前的资源 if (_ptr) delete _ptr; _ptr = ap._ptr; ap._ptr = nullptr; } } ~auto_ptr() { cout << "~auto_ptr" << endl; delete _ptr; _ptr = nullptr; } //像指针一样使用 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr = nullptr; };
//unique_ptr模拟实现 //unique_ptr不支持拷贝,仅支持移动 template<class T> class unique_ptr { unique_ptr(T* ptr) :_ptr(ptr) {} //不支持拷贝(禁用) unique_ptr(const unique_ptr<T>& up) = delete; unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete; //支持移动 unique_ptr(unique_ptr<T>&& up) :_ptr(up._ptr) { up._ptr = nullptr; } unique_ptr<T>& operator=(unique_ptr<T>&& up) { if (_ptr != up._ptr) { if (_ptr) delete _ptr; _ptr = up._ptr; up._ptr = nullptr; } } ~unique_ptr() { cout << "~unique_ptr()" << endl; delete _ptr; _ptr = nullptr; } //像指针一样使用 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } private: T* _ptr = nullptr; };
//模拟实现shared_ptr //其特点是支持拷贝、也支持移动,其底层是通过引用计数实现的 //这里添加了上述我们所将的删除器 template<class T> class shared_ptr { shared_ptr(T* ptr) :_ptr(ptr) , _num(new int(1)) { } //添加删除器 template<class D> shared_ptr(T* ptr,D del) :_ptr(ptr) ,_num(new int(1)) ,_del(del) { } shared_ptr(const shared_ptr<T>& sp) :_ptr(sp._ptr) ,_num(sp._num) ,_del(sp._del) { *(_num)++; } shared_ptr<T>& operator=(shared_ptr<T>& sp) { if (_ptr != sp._ptr) { //如果当前引用计数只有1时,直接释放资源 if (_num == 1) { delete _ptr; delete _num; _ptr = _num = nullptr; } _ptr = sp._ptr; *(_num)--; _num = sp._num; *(_num)++; _del = sp._del; } //返回*this主要是为了实现链式操作:a=b=c return *this; } ~shared_ptr() { if (*(_num) != 1) { *(_num)--; _ptr = nullptr; } else { _del(_ptr); delete _num; _num = _ptr = nullptr; } } int use_count() { return *_num; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; int* _num; //引用计数 //_del的类型是不确定的,利用包装器实现 function<void(T*)> _del = [](T* ptr) {delete ptr; }; };

5.weak_ptr与shared_ptr

5.1share_ptr的循环引用

        目前有两个share_ptr对象:sp1、sp2。这两个的引用计数分别都为1。当sp1指向sp2时,sp2的引用计数为2,当sp2也指向sp1时,sp1的引用计数为2。这时程序结束,sp1进行销毁,引用计数不为1,仅进行减1操作。sp2进行销毁,引用计数不为1,仅进行减1操作。此时销毁操作结束,资源没有被释放,造成了内存泄漏。这就是循环引用带来的问题。

using namespace std; struct ListNode { int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev; // 这里改成weak_ptr,当n1->_next = n2时便于赋值 ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> sp1(new ListNode); shared_ptr<ListNode> sp2(new ListNode); cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; //循环引用 sp1->_next = sp2; sp2->_prev = sp1; //引用计数增加至2 cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; }

         我们可以看到结果并没有调用析构函数。则表明循环引用的出现导致了内存泄漏。而C++11为了解决这一问题,设计出了weak_ptr。

5.2weak_ptr

        weak_ptr严格意义上并不算智能指针,因为weak_ptr并不支持RAII,也不支持访问资源。weak_ptr在初始化的时候仅支持使用share_ptr初始化。而weak_ptr并不会增加shared_ptr的引用计数,这就完美解决了上面导致循环引用的原因。

struct ListNode { int _data; //shared_ptr<ListNode> _next; //shared_ptr<ListNode> _prev; // 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时 // 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了 weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev; ~ListNode() { cout << "~ListNode()" << endl; } }; int main() { shared_ptr<ListNode> sp1(new ListNode); shared_ptr<ListNode> sp2(new ListNode); cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; //未使用weak_ptr将导致循环引用 sp1->_next = sp2; sp2->_prev = sp1; //未使用weak_ptr引用计数将增加至2 cout << sp1.use_count() << endl; cout << sp2.use_count() << endl; }

 

         weak_ptr是没有重载operator*/operator->之类的运算符的,weak_ptr是不支持访问资源的,因为如果当weak_ptr绑定shared_ptr的资源已经释放了,这个时候weak_ptr再去访问就很危险了。 

        weak_ptr支持expired检查指向的资源是否过期,weak_ptr也可以使用use_count得到shared_ptr的引用计数个数

        如果weak_ptr想要访问资源可以使用lock,lock会返回一个管理资源的shared_ptr。如果资源已经释放,则返回一个空对象。如果没有释放则返回一个shared_ptr的对象。通过lock返回的shared_ptr对象访问资源是安全的。

#include<iostream> #include<memory> using namespace std; int main() { shared_ptr<int> sp1(new int(1)); shared_ptr<int> sp2(sp1); weak_ptr<int> wp1(sp2); cout << wp1.use_count() << endl; //weak_ptr不增加引用计数 cout << wp1.expired() << endl; //资源没有被释放,有效 //通过lock访问资源 auto sp = wp1.lock(); *sp += 9; cout << *sp << endl; cout << *sp1 << endl; //资源被释放后,无效 //引用计数不断的减少,为0时资源释放 sp1 = make_shared<int>(1); cout << wp1.use_count() << endl; cout << wp1.expired() << endl; sp2 = make_shared<int>(1); cout << wp1.use_count() << endl; cout << wp1.expired() << endl; }

Read more

C++socket网络编程——udp服务器

C++socket网络编程——udp服务器

目录 一.端口号 VS  PID 二.套接字编程的类型 三.socket编程接口 四.基于udp的服务端和客户端全部代码 客户端 服务端 五.解释与运行 一些细节: 六.总结 一.端口号 VS  PID pid已经能够标识一台主机上的一个唯一一个进程了,为什么还需要端口号? 1. 不是所有的进程都需要网络通信,但是所有的进程都需要都pid; 2. 系统和网络功能解耦。         另外,一个进程可以绑定多个端口,但一个端口只能被一个进程绑定。         系统内定的端口号【0,1023】一般都要有固定的应用层协议使用,如http:80,https:443。 二.套接字编程的类型 1. 域间套接字编程——同一个机器内 2. 原始套接字编程——网络工具 3. 网络套接字编程—

By Ne0inhk
【C++哲学】面向对象的三大特性之 继承

【C++哲学】面向对象的三大特性之 继承

🔥拾Ծ光:个人主页 👏👏👏欢迎来到我的专栏:《C++》,《数据结构》,《C语言》 目录 一、继承的概念和定义 1、什么是继承? 2、继承的定义 2.1、定义格式 2.2、继承基类成员访问方式的变化 3、继承类模板 二、基类和派生类 1、基类与派生类间的类型转换⭐️ 2、继承中的作用域 2.1、隐藏规则 2.2、面试题实践⭐️ 三、派生类的默认成员函数 1、派生类4个常见的默认成员函数⭐️⭐️ 2、实现一个不能被继承的类 四、继承与友元 五、继承与静态成员 六、多继承及其菱形继承问题 1、继承模型 2、虚继承 3、

By Ne0inhk

Qt C++ 插件开发指南:插件架构设计与动态加载实战

一、Qt插件开发概述 1.1 插件技术核心价值 插件架构是一种软件设计模式,通过将应用程序的核心功能与扩展功能分离,允许第三方或开发者在不修改主程序源代码的情况下对软件进行功能扩展、特性升级或定制化改造。这种架构模式在大型软件系统中应用广泛,典型场景包括:IDE工具的插件扩展(如Qt Creator的插件体系)、图形软件的滤镜插件、办公软件的功能模块扩展等。 Qt作为成熟的C++跨平台框架,提供了一套完整的插件机制,其核心优势体现在: * 跨平台兼容性:Qt插件可在Windows、Linux、macOS等系统上统一构建和加载,无需针对不同平台编写适配代码; * 二进制级扩展:插件以动态链接库(.dll/.so/.dylib)形式存在,主程序与插件通过统一接口交互,无需重新编译主程序即可更新插件; * 低耦合设计:主程序仅依赖插件接口抽象,不关心具体实现,插件可独立开发、测试和部署; * 框架原生支持:Qt提供QPluginLoader、QObject、Q_INTERFACES等核心类和宏,简化插件注册、发现和通信流程。 1.2 Qt插件的两种类型 Qt插件体系主要分

By Ne0inhk