wordpress代码逻辑,杭州seo整站优化,成都网站定制建设,平面设计工作室赚钱吗深入篇【C】总结智能指针的使用与应用意义(auto_ptr/unique_ptr/shared_ptr/weak_ptr#xff09;底层原理剖析模拟实现 智能指针的出现智能指针的使用应用意义/存在问题智能指针原理剖析模拟实现auto_ptrunique_ptrshared_ptrweak_ptr 智能指针的出现
首先我们要理… 深入篇【C】总结智能指针的使用与应用意义(auto_ptr/unique_ptr/shared_ptr/weak_ptr底层原理剖析模拟实现 智能指针的出现智能指针的使用应用意义/存在问题智能指针原理剖析模拟实现auto_ptrunique_ptrshared_ptrweak_ptr 智能指针的出现
首先我们要理解智能指针是什么。为什么要有智能指针。什么场景会用到智能指针。 首先我们知道C的异常有很大的缺陷那就是执行流会乱跳这样就可能会造成内存泄露问题比如在new和delete之间出现异常那么就会出现资源没有释放内存泄露。 int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
void func()
{div();
}
int main()
{try{func();}catch(const exception e)//抛基类异常用父类来接受{cout e.what() endl;}
}首先这是正常的使用异常处理当div函数抛异常时就会直接跳到catch捕获的地方进行处理。
//异常的缺点内存泄露,在new和delete之间抛异常
#include vector
int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
void func()
{vectorint* p1 new vectorint;div();delete p1;cout delete: p1 endl;
}
//正常情况下如果不抛异常就不会内存泄露但抛异常后就会泄露解决方法
//是再套一层异常判断捕获的异常不处理继续抛出(在抛出之前将资源释放)
int main()
{try{func();}catch (const exception e)//抛基类异常用父类来接受{cout e.what() endl;}
}那如果是这样的场景呢在new和delete之间如果div()抛异常了那么开辟的空间p1就无法释放。最终会造成内存泄露的。 而想要处理这样的问题就需要对div函数套一层异常判断如果出现异常先释放资源了再将异常抛出给外面的捕获处理。
//异常的缺点内存泄露,在new和delete之间抛异常
#include vector
int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
void func()
{vectorint* p1 new vectorint;//如果p1抛异常就 没有问题没有资源的申请所以也不需要释放vectorint* p2 new vectorint;//如果p2抛异常p1就内存泄露vectorint* p3 new vectorint;//如果p3抛异常p1和p2就内存泄露//…………//所以异常出现后很容易造成内存泄露有什么办法可以解决呢try{div();}catch (...){delete p1;cout delete p1 endl;throw;//再抛出}delete p1;cout delete: p1 endl;
}
//正常情况下如果不抛异常就不会内存泄露但抛异常后就会泄露解决方法
//是再套一层异常判断捕获的异常不处理继续抛出(在抛出之前将资源释放)
int main()
{try{func();}catch (const exception e)//抛基类异常用父类来接受{cout e.what() endl;}
} 只不过这样做还是有缺陷如果有很多个资源申请呢?或者有连续的资源申请呢比如资源1申请如果出现异常那么就直接跳出去如果资源2异常那么就需要对这个操作套一层异常处理需要先将资源1的资源释放了然后再重新抛异常给外面。如果资源3申请时出现异常呢…………这样是不是每次申请资源时都需要套上异常处理呢这样也太麻烦了吧但你又不得不这样做因为你要保证内存安全啊
//异常的缺点内存泄露,在new和delete之间抛异常
#include vector
int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
void func()
{vectorint* p1 new vectorint;//如果p1抛异常就 没有问题没有资源的申请所以也不需要释放vectorint* p2 new vectorint;//如果p2抛异常p1就内存泄露需要给该步骤套上异常处理释放p1然后再将异常抛出vectorint* p3 new vectorint;//如果p3抛异常p1和p2就内存泄露需要给该步骤套上异常处理释放p1p2然后再将异常抛出//…………//所以异常出现后很容易造成内存泄露有什么办法可以解决呢try{div();}//如果div()抛异常p1p2p3都得释放catch (...){delete p1;delete p2;delete p3;cout delete p1 endl;throw;//再抛出}delete p1;cout delete: p1 endl;
}
//正常情况下如果不抛异常就不会内存泄露但抛异常后就会泄露解决方法
//是再套一层异常判断捕获的异常不处理继续抛出(在抛出之前将资源释放)
int main()
{try{func();}catch (const exception e)//抛基类异常用父类来接受{cout e.what() endl;}
}
这时智能指针就出现了 智能指针有三大特性 1.RAII 2.可以像指针一样 3.存在拷贝问题。 一.RAII是一种利用对象生命周期来控制程序资源的技术。 我们在对象构造的时候将开辟的资源交给对象那么这样在对象生命周期内该资源一直存在然后在该对象的析构函数里进行释放资源。这样对象析构资源也就释放了。 也就是我们将资源交给一个对象进行管理当对象的生命周期还在时资源就存在当对象销毁时资源就被释放这样我们就将管理资源的责任托管给了对象。 好处 1.释放了双手不需要我们显示的释放资源。就不用怕资源最后没有释放。 2.采用这种方式对象所需的资源在其生命周期内始终保持有效。 二.智能指针从指针二字我们就应该能意识到它是具备指针的特性的而指针有哪些特性呢 1.可以解引用。通过解引用访问资源。 2.可以使用→运算符来访问资源里的内容。 三.所以说智能指针本质上也是一个指针那指针之间也是可以赋值拷贝的。并且是值拷贝。
智能指针的使用
智能指针的实现其实很简单智能指针底层就是封装着该类型的指针。原理(RAII)就是将申请的资源托管给一个对象管理所以在对象构造时将资源给对象即可。当对象析构时就将资源释放。 然后还要实现*运算符重载和→运算符重载。
namespace tao
{templateclass Tclass smater_ptr
{
public:smater_ptr( T* ptr)//将资源交给对象管理:_ptr(ptr){}~smater_ptr()//对象销毁时就将资源释放{cout delete: _ptr endl;delete _ptr;}//像指针一样使用T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;
};
那么我们就可以处理上面遗留的问题了 #include vector
int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}
void func()
{//vectorint* p1 new vectorint;如果p1抛异常就 没有问题没有资源的申请所以也不需要释放//vectorint* p2 new vectorint;如果p2抛异常p1就内存泄露//vectorint* p3 new vectorint;如果p3抛异常p1和p2就内存泄露//…………//所以异常出现后很容易造成内存泄露有什么办法可以解决呢smater_ptrvectorint p1(new vectorint);//将资源给对象p1管理smater_ptrvectorint p2(new vectorint);smater_ptrvectorint p3(new vectorint);smater_ptrstring ps(new string(小陶来咯));div();cout *ps endl;cout ps-size() endl;
}int main()
{try{func();}catch (const exception e)//抛基类异常用父类来接受{cout e.what() endl;}
}这里再怎么抛异常都不会影响资源的释放因为资源是被智能指针对象管理着当对象销毁时管理的资源肯定会释放的。 我们也不用担心连续的申请资源会出现异常的情况了。
应用意义/存在问题
一般正常使用智能指针就可以避免大多数的内存泄露问题但不排除乱用的。智能指针的应用能帮助我们很好的处理因为异常出现而导致的内存泄露问题。不过初期的智能指针还存在着问题比如拷贝问题。C98时期就已经存在智能指针auto_ptr.不过吐槽点很多现在公司基本禁止使用auto_ptr. 随着C的发展又出现其他的智能指针比如uniqe_ptr ,shared_ptr,weak_ptr(本质不是)。
那智能指针存在什么问题呢我们来分析分析 int main()
{smater_ptrstring sp1(new string(xioatao));smater_ptrstring sp2(new string(xioyao));//存在这样的场景smater_ptrstring sp2(sp1);//指针之间的拷贝就应该是浅拷贝我们不用写编译器会自动生成。//但浅拷贝会出现什么问题呢//1.同一块资源被释放两次 2.内存泄露return 0;
}呐这就是智能指针的拷贝问题指针之间的拷贝肯定是值拷贝因为是内置类型不存在深拷贝。那浅拷贝我们不写编译器生成的拷贝构造就是浅拷贝所以我们就不用写了吗 首先我们要分析确实是浅拷贝但是如果指针浅拷贝了就会出现这样的问题①指向同一块的空间被释放两次②有一块空间没有释放内存泄露。
那我们来看看C库里是如何处理这些问题的。
智能指针原理剖析模拟实现
auto_ptr
C98库里提供的auto_ptr智能指针处理这种问题的原理是管理权转移。 什么叫管理权转移呢就比如sp3(sp1)将sp1拷贝给sp3。也就是用sp1构造sp3。首先我们要明白sp1是管理着一块资源的sp3还没有实例化没有管理资源。这里直接将sp1管理资源的权力转移给sp3。然后sp1就没有权力管理资源了也就是不需要管理资源了。不管理资源是如何做到的呢直接将智能指针对象里面的指针置空即可。
namespace tao
{template class Tclass auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}//sp3(sp1)auto_ptr(auto_ptrT p):_ptr(p._ptr){//将资源转移后被拷贝对象再置空p._ptr nullptr;//如果不置空就会释放两次}private:T* _ptr;};
这就是C98提供的auto_ptr智能指针。这个版本被吐槽的很多因为你将sp1对象管理资源的权转移后sp1就悬空了。如果有人要再次访问sp1呢就是那个不管理资源的智能指针。(里面的指针必须置空不然就会释放两次。) //我们写一个类方便观察资源创建和释放
#include smater_ptr.h
class B
{
public:B(int b 0):_b(b){cout b0 endl;}~B(){cout this;cout ~B() endl;}private:int _b;
};
//自定义类型构造会调用它的构造函数和析构函数。
int main()
{tao::auto_ptrB b1(new B(1));tao::auto_ptrB b2(new B(2));//这个是auto_ptr C98时期就出现但不好,很多公司严禁不给使这个tao::auto_ptrB b3(b1);//因为存在严重的不合理地方当出现智能指针拷贝赋值的地方//auto_ptr处理的方式是直接转移资源的管理权。//拷贝时会把被拷贝对象的资源管理转移给拷贝对象。而被被拷贝对象就没有资源管理直接置空//存在问题管理权转移后再次访问被拷贝对象//b1这个智能指针已经没有资源可以管理了里面的指针直接置空了不能再访问了//b1-_b;b3-_b;
}unique_ptr
由于auto_ptr的设计太不合理C11中开始提供更靠谱的unique_ptr智能指针。 unique_ptr的原理其实很简单四个字简单粗暴。 直接不给拷贝简单粗暴的防止拷贝。利用delete关键字将函数定义为删除函数不能使用。指针赋值的操作也被禁止。 template class Tclass unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}unique_ptr(unique_ptrT p) delete;//直接删除掉不能拷贝private:T* _ptr;};shared_ptr
但是如果就是存在智能指针间的拷贝那该怎么办呢unique_ptr肯定不能使用了。 所以C11又提供了一个可以允许拷贝的智能指针那就是shared_ptr. shared_ptr智能指针实现的原理是引用计数 原理 ①当有一个对象管理资源时计数器就显示1.当有两个对象指向资源时计数器就显示2.当有n个对象指向资源时计数器就显示n。 ②当指向资源的对象生命周期结束时首先将该资源的上的计数器减减。然后判断计数器是否为0.如果不为0,说明还存在对象管理着资源。如果为0就说明没有对象管理资源了该资源就可以释放了。 那这个计数器应该如何设计呢是设计成普通计数器就可以吗还是设计成静态的呢 如果设计成普通的计数器就是在智能指针内部存一个计数器。这就表明每个对象都有一个计数器。这合理吗 这肯定不合理啊指向相同资源时如何进行计数呢这样不能根据计数器来判断一个资源上有多少对象管理了。一个资源就一个计数器就可以了。那设计成静态计数器呢 设计成静态计数器如果只有一块资源的话也不是不可以但我们不知道会有多少资源啊当有新的资源开辟后该资源上应该只有一个对象管理但使用静态计数器后所有对象共享该计数器就造成了该资源上的计数不对。
我们想要的计数器应该是要伴随着资源的申请而生成。当有一个资源生成被智能指针管理后就会生成一个计数器来计算该资源受管理的个数。当有两个资源时就会有两个计数器各计算各的互不影响。所以这里的计数器应该是动态计数器。 当有资源申请时并给对象管理时(也就是调用对象的构造函数)时就会动态生成一个计数器。 计数器实现完后我们就可以利用引用计数来实现拷贝。比如用p2(p1),用p1拷贝构造p2.那么p2就要指向p1指向的资源并且资源上的计数器要加加。这两个智能指针都管理着资源。不过当p2销毁时资源并不会释放计数器首先会减减然后判断计数器是否为0只有计数为0了才可以将资源释放。
template class T
class shared_ptr
{
public:shared_ptr(T* ptrnullptr):_ptr(ptr),_pcount(new int(1)){}//当有资源申请并交给对象管理时计数器才会生成。//只有对象管理一个资源时才会生成计数器。不然不会生成。//当拷贝时(没有资源的生成管理)就让其他对象的指针向生成的计数器。~shared_ptr(){//当有对象要销毁时首先先减减计数器//只有最后一个对象管理时才可以释放资源if (--(*_pcount) 0){cout delete _ptr endl;delete _ptr;delete _pcount;}}T operator*(){return *_ptr;}T* operator-(){return _ptr;}shared_ptr(shared_ptrT ps):_ptr(ps._ptr), _pcount(ps._pcount)//这个单纯指向没有再管理一个新的资源所以不会生成计数器只需要指向原来生成的计数器。{(*_pcount);//然后原来的计数器加加即可表面该资源上多了一个对象管理}T* get()const//获取_ptr {return _ptr;}
private:T* _ptr;int* _pcount;//动态计数器
};
shared_ptr除了支持智能指针间的拷贝还支持指针间的赋值。那么赋值重载是如何实现的呢 首先赋值是两个都已经存在的对象进行赋值。而拷贝构造是已存在的拷贝给不存在的对象。 假设两个存在的对象各自都管理着一块资源当两个对象进行赋值会发生什么呢 不过这里要注意两个细节细节一就是赋值对象的计数器减减后需要进行判断是否为0.如果计数器为0了就说明该资源上没有对象管理那么该资源就可以释放了。如果不是0那就没事。 细节二就是自己给自己赋值的场景会有bug存在。
//shared_ptr的赋值运算符重载
shared_ptrT operator(const shared_ptrT ps)
{//自己给自己赋值的场景要避免if (_ptr ps._ptr)return *this;//首先要对赋值对象管理的资源的计数器减减要注意被赋值对象管理的资源是否只有一个智能指针控制if (--(*_pcount) 0){delete _ptr;delete _pcount;}_ptr ps._ptr;_pcount ps._pcount;//正常转移指向即可//被赋值对象的计数器即可(*ps._pcount);return *this;
}验证一下
int main()
{tao::shared_ptrB b1(new B(1));tao::shared_ptrB b2(b1);tao::shared_ptrB b3(b1);tao::shared_ptrB b4(new B(2));tao::shared_ptrB b5(b4);b1 b5;} weak_ptr
shared_ptr几乎已经完美了既具有智能指针的特性又允许拷贝和赋值。但还是具有缺点的具有什么缺点呢 该问题就是循环计数 当出现循环计数时shared_ptr是没有办法解决。什么叫循环计数问题呢
当存在这种需求时动态开辟的节点需要链接起来时。我们使用智能指针来管理资源会发生什么呢 struct Node
{B _val;Node* _next;Node* _prev;
};int main()
{tao::shared_ptrNode sp1(new Node);tao::shared_ptrNode sp2(new Node);//sp1-_next sp2;//类型不匹配sp1-next的类型是Node而sp2的类型是shared_ptr类型所以这样无法链接起来。
// //所以Node节点里存的应该是shared_ptr类型的指针这样才可以链接起来
}我们发现无法链接起来因为节点里next是Node*类型的而sp2是shared_ptr类型的。所以为了能够链接起来节点里存的应该是shared_ptr类型的next。
struct Node
{B _val;shared_ptrNode _next;shared_ptrNode _prev;
};int main()
{tao::shared_ptrNode sp1(new Node);tao::shared_ptrNode sp2(new Node);
// //所以Node节点里存的应该是shared_ptr类型的指针这样才可以链接起来sp1-_next sp2;sp2-_prev sp1;}这样两个节点就链接起来了。可是链接起来后就出现问题了 我们发现两个智能指针管理的资源都没有释放这是为什么呢 C中是如何解决循环计数的呢C11提供了weak_ptr专门用来处理shared_ptr出现的循环计数问题。 那么weak_ptr解决循环计数的原理是什么呢 首先我们需要明白引起循环计数的原因是什么要理解什么场景下会发生循环计数。 1.主要原因就是因为智能指针定义在节点的内部。 2.然后就是因为计数器要等于1时才可以释放资源。
就是因为内部的智能指针参与了资源的管理导致计数器增加外面管理的资源的对象销毁后资源也无法销毁需要里面的智能指针对象销毁才可以销毁而里面的对象销毁又需要资源先销毁才可以销毁。所以最主要原因就是内部的智能指针管理资源。 所以weak_ptr实现的原理就是让节点里面的智能指针不管理资源就单纯的链接节点不参与资源的管理但可以访问资源。 这样计数器就不会增加当外面的对象销毁资源就会正常销毁。 所以正常操作应该是这样
struct Node
{B _val;weak_ptrNode _next;weak_ptrNode _prev;//weak_ptr不管理资源不会增加计数器
};
int main()
{shared_ptrNode sp1(new Node);shared_ptrNode sp2(new Node);sp1-_next sp2;sp2-_prev sp1;
} 所以weak_ptr严格上来说不是智能指针它不具备RAII特性。不管理资源。它主要是提供支持由shared_ptr类型转换成weak_ptr类型的拷贝构造和赋值。而不是管理资源。 template class Tclass weak_ptr{public:weak_ptr():_ptr(nullptr){}T operator*(){return *_ptr;}T* operator-(){return _ptr;}weak_ptr(const shared_ptrT sp)//在类外无法访问到sp的保护成员_ptr,所以徐娅get函数来获取。:_ptr(sp.get()){}weak_ptrT operator(const shared_ptrT sp){_ptr sp.get();return *this;}private:T* _ptr;};
};