【智能指针】—— 我与C++的不解之缘(三十三)

【智能指针】—— 我与C++的不解之缘(三十三)

一、智能指针的使用

还记得,在异常学习的时候,我们分析出了一个问题

doubleDivide(int x,int y){if(y ==0){throwstring("the y is zero");}return(double)x /double(y);}voidtest(int x,int y){int* arr =newint[10];Divide(x, y);delete[] arr; cout <<"delete[] arr"<< endl;}intmain(){while(1){int x, y; cin >> x >> y;try{test(x, y);}catch(const string& str){ cout << str << endl;}catch(...){ cout <<"unknown exception"<< endl;}}return0;}
在上述代码中,Divide函数,如果y==0就会抛出异常,并且在该函数内没有捕获该异常,就继续将异常抛给外层调用的函数test,在testnew了一个int数组,但是没有捕获Divide函数抛出的异常,程序直接接跳到main函数当中去,就导致申请的空间资源没有被释放。
在这里插入图片描述

在异常学习中,我们的解决方法就是在test函数中捕获Divide函数抛出的异常,进行资源的释放再将异常重新抛出。

voidtest(int x,int y){int* arr1 =newint[10];try{Divide(x, y);}catch(...){delete[] arr; cout <<"delete[] arr"<< endl;throw;}delete[] arr; cout <<"delete[] arr"<< endl;}
在这里插入图片描述

这样看起来我们似乎解决了这一个问题,但是如果我们开辟了两个数组呢?

voidtest(int x,int y){int* arr1 =newint[10];int* arr2 =newint[10];try{Divide(x, y);}catch(...){delete[] arr1; cout <<"delete[] arr1"<< endl;delete[] arr2; cout <<"delete[] arr2"<< endl;throw;}delete[] arr1; cout <<"delete[] arr1"<< endl;delete[] arr2; cout <<"delete[] arr2"<< endl;}

在上述代码中,如果我们在new第二个数组时,new抛异常了呢?(这里如果第一个new抛异常,那没啥问题)

我们总不能再给第二个new套一层try吧,那不现实;如果申请了多个资源,每一个都要套上try那太不现实了。

在当时我们也没有解决这一个问题,但是有了智能指针那就好很多了。

先来看一下什么是智能指针

二、RAII和智能指针

RAII思想

RAIIResource Acquisition Is Initialization的缩写,它是一个管理资源的类的设计思想;本质上就是利用对象生命周期来管理获取到的动态资源,避免发生内存泄露(这里资源指内存、文件指针、网络连接、互斥锁等等)RAII思想:在获取到资源时把资源委托给一个对象,接着控制对资源的访问,这样资源在该对象的生命周期内始终是有效的,最后该对象生命周期结束,在析构的时候释放了资源;这样我们就保证了资源的正常释放,就可以结局上述资源泄露的问题。

那什么意思呢?

简单来说就是,我们不要去主动管理这些动态资源了,把这些动态资源交个一个对象去管理,这样出了这个对象的作用域,该对象的析构函数就会把这些动态资源自动释放,就不需要我们自己去释放了。
template<classT>classsmartptr{public:smartptr(T* ptr):_ptr(ptr){}~smartptr(){delete[] ptr; cout <<"delete []"<< endl;}private: T* _ptr;};

有了上面代码,我们就可以创建smartptr来帮助我们管理动态资源

voidtest(int x,int y){ smartptr<int>arr1(newint[10]); smartptr<int>arr2(newint[10]);Divide(x, y);}
在这里插入图片描述

可以看到,无论Divide是否抛异常,我们申请的资源都能成功释放(因为arr1arr2出了作用域就调用析构函数,就会对资源进行释放

智能指针思想

通过观察上述代码,我们可以发现一个问题:如何访问动态资源呢?

我们将动态资源交给一个对象去管理,是可以解决资源泄露的问题;但是我们如何去访问这个动态资源呢?

所以为了方便我们访问动态资源,智能指针就还要实现重载operator*operator->opeartor[]这些运算符

这里这种思想就类似于迭代器,我们可以像指针一样去访问迭代器,这里智能指针也一样,我们也要可以像指针一样去访问智能指针。
template<classT>classsmartptr{public:smartptr(T* ptr):_ptr(ptr){} T&operator*(){return*_ptr;} T&operator[](size_t i){return _ptr[i];} T operator->(){return _ptr;}~smartptr(){delete[] _ptr; cout <<"delete []"<< endl;}private: T* _ptr;};

这里其实还存在一个致命的问题,那就是拷贝的问题:

对于我们自己申请的资源,我们就行拷贝(赋值)时,就是简单的值拷贝,并且释放的时候我们就只需要释放一个即可

但是如果使用智能指针,拷贝肯定不能使用深拷贝(我们想要的就是值拷贝),那我们该如何去释放这个资源呢?

**同一个资源是不能释放两次的;**现在来看C++库里面的智能指针是如何解决这一问题的。

三、C++标准库中智能指针的使用

C++标准库中智能指针都在这个头文件下;

智能指针有很多种,除了weak_ptr以外都符合RAII和可以像指针一样访问的行为,在原理上来说就是解决拷贝的问题不同。

auto_ptr

auto_ptr:这是C++98中设计出来的智能指针,它解决拷贝问题的方法就是在拷贝时将被拷贝对象资源的管理权转移给拷贝对象(简单来说就是,我把资源转给你,我自己置为nullptr),这个可以说很糟糕,它会把被拷贝对象悬空,我们再访问就会报错;这里不推荐使用auto_ptr

这里为了观察就简单实现一个Date

structDate{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;};intmain(){ auto_ptr<Date>ap1(new Date); auto_ptr<Date>ap2(ap1);//拷贝之后ap1被置为空return0;}
在这里插入图片描述
~Date() 

最后资源也是释放一次。

unique_ptr

unique_ptrC++11设计出来的,它解决拷贝问题的方法就简单粗暴了,直接不支持拷贝,只支持移动;

在不需要拷贝的场景下就推荐使用unique_ptr

通过看文档可以发现,它的拷贝构造和拷贝赋值是delete掉的。
在这里插入图片描述
在这里插入图片描述
intmain(){ unique_ptr<Date>up1(new Date);//unique_ptr<Date> up2(up1);//不支持拷贝//支持移动,但移动之后up1也置为空,使用要小心 unique_ptr<Date>up2(move(up1));return0;}

shared_ptr

前面两个智能指针,一个拷贝是管理权转移,应该干脆就不支持,那还是没有到达我们想要的结果,现在来看shared_ptr

shared_ptrC++11设计的智能指针,它支持拷贝,也支持移动;

需要拷贝的场景就需要它了。(其底层使用引用计数来实现的
intmain(){ shared_ptr<Date> sp1; shared_ptr<Date>sp2(sp1); shared_ptr<Date>sp3(sp1);//查看当前有多少对象管理这一资源 cout << sp1.use_count()<< endl; sp1->_year =2025; cout << sp1->_year << endl; cout << sp2->_year << endl; cout << sp3->_year << endl;return0;}
3 2025 2025 2025 ~Date() 
weak_ptr也是C++11设计的智能指针,它和上述智能指针不同,不支持RAII它不能直接管理资源;

weak_ptr实现本质上是为了解决shared_ptr循环引用导致内存泄露的问题。

删除器

智能指针的析构默认是进行delete来释放资源,那也就是说,如果我们将不是new的资源交给智能指针,在析构的时候就会崩溃;

所以智能指针在构造时就支持给一个删除器。

本质上所谓的删除器就是一个可调用对象,这个可调用对象中实现我们需要是释放资源的方式;

当我们在构造智能指针时,给了删除器,在智能指针析构时就会调用删除器去释放资源。

而我们又经常使用new[],所以unique_ptrshared_ptr都特化了一个[]的版本,我们在使用时只需类型给成T[]即可;(例如:unique_ptr<Date[]> up1(new Date[10])shared_ptr<Date[]> sp1(new Date[10])
intmain(){ unique_ptr<Date[]>up1(new Date[5]); shared_ptr<Date[]>sp1(new Date[5]);//删除器//删除器是一个可调用对象,那我们就可以使用仿函数、函数指针、lambda来做删除器//仿函数 unique_ptr<Date, DeleteArr<Date>>up2(new Date[3]);//类模版这里要传的是类型 shared_ptr<Date>sp2(new Date[3], DeleteArr<Date>());//构造函数这里我们要传对象//函数指针//这里类模版要传的是类型,而根据函数类型又没有办法得到函数,构造也要显示传递 unique_ptr<Date,void(*)(Date*)>up3(new Date[3],DeleteArrFunc<Date>); shared_ptr<Date>sp3(new Date[3], DeleteArrFunc<Date>);//lambdaauto del =[](Date* ptr){delete[] ptr;};//这里我们没办法指定lambda的类型,所以要先创建一个lambda对象然后使用decltype来推它的类型 unique_ptr<Date,decltype(del)>up4(new Date[3], del);//shared_ptr就很好用了,只用在构造函数时传递即可 shared_ptr<Date>sp4(new Date[3],[](Date* ptr){delete[] ptr;});return0;}
这里shared_ptr1除了可以使用指向资源的指针构造,还可以使用make_shared有初始化资源对象的值直接进行构造。

shared_ptrunique_ptr都支持operator bool的类型转换,如果智能指针对象是一个空的对象就返回false;赋值返回true。(这样我们就可以直接对智能指针对象进行判断是否为空)。

四、智能指针的实现原理

首先对于auto_ptr,它的拷贝是管理权转移,感觉很不符合逻辑;而unique_ptr是直接不支持拷贝,这两种智能指针实现起来还是非常简单,这里就不详细叙述了;

template<classT>classauto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& p):_ptr(p._ptr){ p._ptr =nullptr;} auto_ptr<T>&operator=(auto_ptr<T>& ap){if(*this!= ap){if(_ptr)delete _ptr; _ptr = ap._ptr; ap._ptr =nullptr;}return*this;} T&operator*(){return*_ptr;} T&operator[](size_t i){return _ptr[i];} T*operator->(){return _ptr;}~auto_ptr(){if(_ptr)delete _ptr;}private: T* _ptr;};template<classT>classunique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(unique_ptr<T>& p)=delete; unique_ptr<T>&operator=(unique_ptr<T>& p)=delete;unique_ptr(unique_ptr<T>&& p):_ptr(p._ptr){ p._ptr =nullptr;} T&operator*(){return*_ptr;} T&operator[](size_t i){return _ptr[i];} T*operator->(){return _ptr;}private: T* _ptr;};

share_ptr实现原理

shared_ptr使用了引用计数,简单来说呢,我们不光要在智能指针中记录要管理的资源,还要记录一下当前管理资源的智能指针的个数,所以这里我们需要一个引用计数;

那如何去实现这个引用计数呢?

这里首先肯定不能在类中存放一个值;

那静态成员变量是否可以呢?

显然是不可以的,因为静态成员变量是属于类的,我们想要的引用计数是和管理资源有关的,所以这里我们就只能存放int*,采用堆上动态开辟的方式,在构造的时候开辟一块空间,在拷贝的时候,将指针值拷贝给另一个对象,并且对值进+1

那这样多个shared_ptr管理一块资源时,当析构的时候就--引用计数,当引用计数减到0时,表示当前析构的就是管理这一块资源的最后一个智能指针对象,就要析构资源。
在这里插入图片描述
在这里插入图片描述
OK呢,那现在就来简简单单手搓一个简易版shared_ptr出来

首先对于shared_ptr的成员:T*的指针、int*pcount引用计数;

简单的operator*operator->operator[]这些就不解释了,直接看代码:

template<classT>classshared_ptr{ T&operator*(){return*_ptr;} T&operator[](size_t i){return _ptr[i];} T*operator->(){return _ptr;}private: T* _ptr;int* _pcount;};

构造函数

对于构造函数,首先就是默认构造,我们直接将_ptr_pcount赋值为nullptr即可。

然后就是:我们在创建一个shared_ptr的智能指针对象时,要做的就是开辟一块引用计数的空间,并赋值成1(表示当前有一个对象管理这一块资源);然后把传过来的一块资源赋值给_ptr(让ptr指向要管理的资源即可)。

shared_ptr():_ptr(nullptr),_pcount(nullptr){}shared_ptr(T* ptr):_ptr(ptr){ _pcount =newint(1);}

拷贝构造

对于拷贝构造,当我们调用拷贝构造时,就表明我们要将一个智能指针对象管理的资源共享给另一个智能指针对象,此时我们的引用计数要进行+1

这里因为我们调用拷贝构造时,我们当前对象是没有管理任何资源的(_ptrpcount都为nullptr),我们才能直接将被拷贝对象sp_ptr_pcount直接赋值给我们*this_ptr_pcount
shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}

拷贝赋值

对于拷贝赋值,它拷贝构造那样,可以直接进行赋值操作;

当我们调用拷贝赋值时,我们当前对象可能是管理着其他资源的;所以我们要先将当前管理的资源进行处理(如果有其他对象管理着这一块资源,那--引用计数即可;如果没有其他对象管理这一块资源,我们还要对其进行释放)。

这一块对资源进行处理的操作,我们可以发现和析构函数的逻辑一样,所以我们可以将其单独写成一个函数release
voidrelease(){(*_pcount)--;if(*_pcount ==0){delete _ptr;delete _pcount;} _ptr =nullptr; _pcount =nullptr;} shared_ptr<T>&operator=(const shared_ptr<T>& sp){release(); _ptr = sp._ptr; _pcount = sp._pcount;(*_pcount)++;return*this;}

析构函数

对于析构函数,它的逻辑就是--引用计数,如果引用计数减到0,那就释放资源;(逻辑和上面拷贝赋值处理资源的逻辑一样

这里就可以直接复用release

~shared_ptr(){release();}

这里对于拷贝赋值和析构函数这里,博主还有一种想法:

我们在拷贝赋值的参数那里让他传值传参,这样就会调用一次拷贝构造,构造了一个临时对象sp

我们再让this指向的_ptr_pcountsp进行一下交换,这样出了拷贝赋值函数,sp会自动调用析构函数;

这样我们只需要在析构函数内部实现处理资源的操作就OK了。
voidswap(shared_ptr<T>& sp){ std::swap(_ptr, sp._ptr); std::swap(_pcount, sp._pcount);} shared_ptr<T>&operator=(shared_ptr<T> sp){swap(sp);return*this;}~shared_ptr(){(*_pcount)--;if(*_pcount ==0){delete _ptr;delete _pcount;} _ptr =nullptr; _pcount =nullptr;}
上面这种方法,博主在vector模拟实现时有所耳闻,也是非常好理解的

我们拷贝赋值的参数写的是shared_ptr<T>,这样在传参时是传值传参,就会去调用拷贝构造,构造一个新的对象sp指向被调用对象管理的资源;

sp的作用域就是operator=函数内,所以我们把this的指向的对象和生成的形参对象sp进行交换(值交换);

这样出了作用域sp要调用析构函数,就会把*this对象原来管理的资源进行处理;

到这里,简易版的shared_ptr就实现完成了

现在我们来加上删除器

删除器

删除器,我们要想在类中可以调用这个删除器,那我们要将删除器存下来;

shared_ptr我们只需要在构造函数中传递就可以了,那我们构造函数如下面所示
template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del){}
但是但是,对于这个类型D我们只在构造函数中指定它是什么啊,那在类中如何去存储这个删除器呢?

真的要在模版参数那里多一个吗?库里面也没有多这一个模版参数啊.

这里就使用function包装器就可以解决问题了

因为我们的删除器肯定都是没有返回值(void),且参数肯定是T*,所以使用function<void(T*)>包装即可。

这里在展示代码之前,罗列几个要注意的点:在拷贝赋值时,我们要将删除器一同传递过去;(不同的类型,删除器不一样)在析构逻辑中,我们直接调用删除器去资源,但是对于引用计数的修改还是需要我们就行操作的。删除器我们要给缺省值,当我们在构造函数不穿第二个参数时,我们默认的删除器是delete的。
template<classT>classshared_ptr{public:explicitshared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}template<classD>shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(newint(1)),_del(del){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){(*_pcount)++;}voidswap(shared_ptr<T>& sp){ std::swap(_ptr, sp._ptr); std::swap(_pcount, sp._pcount); std::swap(_del, sp._del);} shared_ptr<T>&operator=(shared_ptr<T> sp){swap(sp);return*this;}~shared_ptr(){(*_pcount)--;if(*_pcount ==0){//delete _ptr;_del(_ptr);delete _pcount;} _ptr =nullptr; _pcount =nullptr;} T&operator*(){return*_ptr;} T&operator[](size_t i){return _ptr[i];} T*operator->(){return _ptr;}private: T* _ptr;int* _pcount; function<void(T*)> _del =[](T* ptr){delete ptr;};};

五、shared_ptr循环引用问题

对于shared_ptr大多数情况下已经可以去管理资源了,支持RAII也支持拷贝;

但是有一种特殊情况,循环引用的场景下,还是会遇到问题的;(会导致资源没得到释放)
structListNode{int _date; std::shared_ptr<ListNode> _next; std::shared_ptr<ListNode> _prve;~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){ std::shared_ptr<ListNode>n1(new ListNode); std::shared_ptr<ListNode>n2(new ListNode); cout << n1.use_count()<< endl; cout << n2.use_count()<< endl; n1->_next = n2; n2->_prve = n1; cout << n1.use_count()<< endl; cout << n2.use_count()<< endl;return0;}
在这里插入图片描述

可以看到,在上述代码中,我们让n1->_next指向n2n2->_prve_prve指向n1

这样指向之后我们发现n1n2的引用计数都变成了2,并且知道程序结束,也没有释放资源。

这种情况,就是我们所说的内存泄露。

我们现在来分析一下,为什么会造成内存泄露呢?

如上图所示,n1->_next = n2n2->_next = n1之后,n1n2对应的引用计数都+1,变成了2

那我们现在n1n2调用析构:

在这里插入图片描述

n1n2调用析构之后,其对应的引用计数-1减到了1,没有减到0,这两块空间还没有释放;

那我们右边节点什么时候释放呢,左边节点中的_next管理着,左边节点中_next析构后,右边节点就释放了;

那左边节点的_next什么时候释放呢?,那要等到左边节点析构,右边节点的_prve管理着,等右边节点_prve析构,左边节点就释放了。

左边节点右边的_prve管理着,右边节点左边的_next管理着,那这样都在等对方析构,而谁都不会释放,就形成了循环引用,从而导致内存泄露。

weak_ptr解决循环引用问题

那这个问题如何解决呢?

要像解决这个问题,我们来看一下这个问题的本质是什么?

**那就是我们将n1->_next绑定n2节点时,n2节点的引用计数会+1;将n2->_prve绑定n1节点时,n1几点的引用计数会+1。**这样就导致我们在析构n1n2时,引用计数-1之后不等于0,就无法释放资源。

简单来说,就是n1->_nextn2->_prve参与了资源的管理。
c++11还有一种智能指针weak_ptr,它就是专门来解决这个问题的。

我们先来看一下weak_ptr

在这里插入图片描述

其实通过观察weak_ptr的构造函数和赋值重载就可以发现,它支持使用shared_ptr去构造和赋值;

但是它有一个特点,我们将shared_ptr的智能指针对象赋值给weak_ptr,我们shared_ptr对象的计数引用不会变化(weak_ptr不会参与shared_ptr的管理资源)。

那这样,我们再看上述问题,我们将ListNode结构体中_next_prve的类型改成weak_ptr

那这样,将n1->_next绑定n2节点时,n2节点的引用计数不会+1

n2->_prve绑定n1节点时,n1节点的引用计数不会+1

这样我们在析构n1n2时,引用计数-1就等于0,就会释放资源。
structListNode{int _date;//std::shared_ptr<ListNode> _next;//std::shared_ptr<ListNode> _prve; std::weak_ptr<ListNode> _next; std::weak_ptr<ListNode> _prve;~ListNode(){ cout <<"~ListNode()"<< endl;}};intmain(){ std::shared_ptr<ListNode>n1(new ListNode); std::shared_ptr<ListNode>n2(new ListNode); cout << n1.use_count()<< endl; cout << n2.use_count()<< endl; n1->_next = n2; n2->_prve = n1; cout << n1.use_count()<< endl; cout << n2.use_count()<< endl;return0;}
在这里插入图片描述
可以看到引用计数并没有+1,也成功析构,没有造成资源泄露。

weak_ptr

这里简单了解有效weak_ptr
  • weak_ptr不支持RAII,也不支持访问资源,我们在文档中也会发现weak_ptr构造没有支持绑定资源,而是支持绑定到shared_ptr;在绑定到shared_ptr时,不会增加shared_ptr的引用计数。
  • weak_ptr没有重载operator*operator->,它不参与资源管理(如果weak_ptr绑定的shared_ptr已经析构了,那如果再去访问就和危险);
  • weak_ptr支持了expired检查指向的资源是否过期,use_count也支持获取shared_ptr的引用计数。
  • weak_ptr还支持了lock,它可以返回shared_ptr;如果资源已经释放,那返回的就是空对象;如果没有释放,那返回的shared_ptr可以进行访问资源。

六、内存泄露

什么是内存泄漏?

这个问题之前就有所耳闻;内存泄漏指因为疏忽或者错误造成程序没有释放已经不再使用的内存,(右边就是忘记释放或者异常导致未能释放);

内存泄漏指的实际上是程序分配某部分内存以后,因为某种失误,导致失去了对这些内存的控制,造成了内存的浪费

内存泄漏的危害

在我们平常写代码过程中,我们感受不到内存泄漏带来的影响;因为我们写的普通程序,它运行一会就结束了;

进程正常结束,页表映射关系解除,物理内存也就释放了;

但是对于一些长期运行的内存,如果出现内存泄漏,那影响很大了,就好比操作系统后台服务器、长时间运行的客户端;如果出现内存泄漏,就导致可用的内存不断减少,运行速度越来越慢,最后卡死。

现在已经支持一些对于内存泄露的检查

但是我们能够避免内存泄露还是尽量去避免的。

养成良好的习惯,申请的内存空间记得去释放;

如果遇到异常问题,可能我们无法彻底去解决,那就使用智能指针去解决

当然使用智能指针要注意避免循环引用的情况。

对于定期使用内存泄露工具检测,尤其是在一个项目上线之前。

简单总结

内存泄漏非常常见,我们尽可能的去避免;在写代码是去预防:使用智能指针去管理资源;事后查错:使用泄漏检测工具去定期检查。

七、shared_ptr线程安全问题

这部分呢,在博主学习了linux线程之后,再来叙述。

八、Boost库

最后,我们来了解一下Boost

Boost库是c++语言标准库提供的扩展的一些C++程序库,Boost社区建立的初衷之一就是为了c++标准化工作提供参考。

Boost社区的发起人Dawes本人就是C++委员会的成员之一。

Boost库的开发中Boost社区在这个方向上取得了丰硕的成果。C++98有了第一个智能指针auto_ptrC++boost库给出了更多实用的智能指针scoped_ptr/scoped_array和shared_ptr/shared_array/weak_ptrC++ TR1,引⼊了shared_ptr等,不过注意的是TR1并不是标准版。C++ 11,引⼊了unique_ptrshared_ptrweak_ptr。需要注意的是unique_ptr对应boost的

漏,那影响很大了,就好比操作系统后台服务器、长时间运行的客户端;如果出现内存泄漏,就导致可用的内存不断减少,运行速度越来越慢,最后卡死。

现在已经支持一些对于内存泄露的检查

但是我们能够避免内存泄露还是尽量去避免的。

养成良好的习惯,申请的内存空间记得去释放;

如果遇到异常问题,可能我们无法彻底去解决,那就使用智能指针去解决

当然使用智能指针要注意避免循环引用的情况。

对于定期使用内存泄露工具检测,尤其是在一个项目上线之前。

简单总结

内存泄漏非常常见,我们尽可能的去避免;在写代码是去预防:使用智能指针去管理资源;事后查错:使用泄漏检测工具去定期检查。

到这里本篇关于智能指针的讲解就结束了,感谢各位的支持!!!
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws

Read more

【C++】深入浅出“图”——最短路径算法

【C++】深入浅出“图”——最短路径算法

文章目录 * 一、Dijkstra算法 * 二、Bellman_Ford算法 * 三、Floyd_Warshall算法 一、Dijkstra算法 最短路径问题是指,从在带权的有向图中从某一顶点出发,找到通往另一顶点的最短路径,“最短”指的是沿路径各边的权值总和最小。 Dijkstra算法是单源最短路径的经典贪心算法,只能用于没有负权的图。它从起点出发,每次选当前距离最小且未确定最短路径的节点,用它去松弛(更新)所有邻接点的最短路径估计值,标记该节点为 “已确定”,重复此过程直到所有节点处理完毕,最终得到起点到图中所有节点的最短路径。 // src是选定的起点,dist记录起点到各点的最短路径,pPath记录到每个点的最短路径的前驱顶点下标voidDijkstra(const V& src, vector<W>& dist, vector<int>& pPath){ size_t srci =GetVertexIndex(

By Ne0inhk
【Linux】线程池(一)C++ 手写线程池:基于策略模式实现高性能日志模块

【Linux】线程池(一)C++ 手写线程池:基于策略模式实现高性能日志模块

文章目录 * 池化技术 * 线程池的日志模块 * 日志与策略模式 * 日志模块 * 两个核心问题 * 设计文件等级 * 刷新策略 * 获取日志时间 * logger类实现 * 内部类LogMessage实现 * 日志刷新流程图及源码 池化技术 池化技术可以减少很多的底层重复工作,例如创建进程、线程、申请内存空间时的系统调用和初始化工作,例如线程池,先预先创建好一些线程,当任务到来时直接将预先创建好的线程唤醒去处理任务,效率会远远高于任务到来时临时创建线程。例如内存池,但我们要用1mb空间时内存池会一次性申请20mb空间,效率会远远高于用多少空间申请多少空间(申请空间会调用系统调用)。 线程池是执行流级别的池化技术,STL中的空间配置器和内存池是内存块管理级别的池化技术。 线程池的日志模块 下⾯开始,我们结合我们之前所做的所有封装,进⾏⼀个线程池的设计。在写之前,我们要做如下准备。 * 准备线程的封装 * 准备锁和条件变量的封装 * 引⼊日志,对线程进⾏封装 日志与策略

By Ne0inhk
【探寻C++之旅】C++ 智能指针完全指南:从原理到实战,彻底告别内存泄漏

【探寻C++之旅】C++ 智能指针完全指南:从原理到实战,彻底告别内存泄漏

前言 作为 C++ 开发者,你是否曾因以下场景头疼不已?函数中new了数组,却因异常抛出导致后续delete没执行,排查半天定位到内存泄漏;多模块共享一块内存,不知道该由谁负责释放,最后要么重复释放崩溃,要么漏释放泄漏;用了auto_ptr后,拷贝对象导致原对象 “悬空”,访问时直接崩溃却找不到原因。 如果你有过这些经历,那智能指针一定是你必须掌握的现代 C++ 工具。它基于 RAII 思想,自动管理动态资源,让你无需手动delete,从根源上减少内存泄漏风险。今天,我们就从 “为什么需要智能指针” 到 “不同智能指针的实战场景”,带你系统掌握这一核心特性。 请君浏览 * 前言 * 一、智能指针的诞生:解决手动管理内存的 “千古难题” * 1.1 一个典型的内存泄露场景 * 1.2 智能指针的核心:RAII 思想 * 二、C++ 标准库智能指针:

By Ne0inhk
【C++模版】泛型编程:代码复用的终极利器

【C++模版】泛型编程:代码复用的终极利器

目录 一、泛型编程 1.1 为什么需要泛型编程? 1.2 模板:泛型编程的基础 二、函数模板 2.1 函数模板的定义格式 2.2 函数模板的原理 2.3 函数模板的实例化 2.3.1 隐式实例化 2.3.2 显式实例化 2.4 模板参数的匹配原则 ☃. 小彩蛋: 模板中::的二义性问题 三、类模板 3.1 类模板的定义格式 3.2 类模板的实例化 四、非类型模板参数  4.1 核心概念与语法 经典案例:实现编译期定长数组

By Ne0inhk