在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
一、前序 什么是智能指针? ——是一个类,用来存储指针(指向动态分配对象也就是堆中对象的的指针)。 c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。 二、unique_ptr unique_ptr 是auto_ptr的升级版,并且auto_ptr在c++11中已经弃用。 unique_ptr 是一个独享所有权的智能指针: 1、拥有它指向的对象 2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作 3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象 unique_ptr 可以实现如下功能: 1、为动态申请的内存提供异常安全 2、讲动态申请的内存所有权传递给某函数 3、从某个函数返回动态申请内存的所有权 4、在容器中保存指针 5、auto_ptr 应该具有的功能 我们可以通过“.”操作访问指针,通过“->”来访问它指向的对象,shared_ptr 是一样的。 unique_ptr<PTest> p1(new PTest("ceshi1")); unique_ptr<PTest> p2(new PTest("p2 test")); p1->print(); p1->set("ceshi2"); p1->print(); //p1.release(); p1.reset(); p1 = std::move(p2); if (p2 == nullptr) { std::cout << "p2 is nullptr" << endl; } p1->print(); PTest *p = p1.release(); p->print(); p2.reset(p); p2->print(); p1 = function(); p1->print(); 注意release函数是让指针和指针指向的对象脱离关系,并没销毁,要想销毁,调用reset一个空对象 三、shared_ptr 从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源(指向的对象)会被释放。此处调动reset不能销毁对象,只有当计数为0时候才可以。 多线程调用它指向的对象的时候,一定要加锁。 对于动态数组的操作unique_ptr 可以自动上释放,shared_ptr不可以,需要添加一个删除器,即一个lambd表达式 shared_ptr<PTest>p3(new PTest("p3:from shared_ptr")); p3->print(); shared_ptr<PTest>p4(new PTest("p4:from shared_ptr")); p4->print(); shared_ptr<PTest>p5(p4); p5->print(); std::cout<< p5.use_count()<<endl; p4.reset(); std::cout << p5.use_count()<<endl; p5.reset(); std::cout << p5.use_count() << endl; std::getchar(); return 0;
四、week_ptr——为了解决循环引用的问题 weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
weak_ptr没有重载 * 和 -> ,所以并不能直接使用资源。但可以使用lock()获得一个可用的shared_ptr对象,
void runGame(){ std::shared_ptr<Monster> monster1(new Monster()); std::weak_ptr<Monster> r_monster1 = monster1; r_monster1->doSomething();//Error! 编译器出错!weak_ptr没有重载* 和 -> ,无法直接当指针用 std::shared_ptr<Monster> s_monster1 = r_monster1.lock();//OK!可以通过weak_ptr的lock方法获得shared_ptr。
auto_ptr尽量不要用!
1.unique_ptr
下图演示了两个 unique_ptr 实例之间的所有权转换。
unique_ptr 与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器运行效率很高,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。unique_ptr 指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过 reset 方法重新指定、通过 release 方法释放所有权、通过移动语义转移所有权,unique_ptr 还可能没有对象,这种情况被称为 empty。[6] ^{[6]}
unique_ptr的基本操作有:
//智能指针的创建
//所有权的变化
auto_ptr 从 C++98 使用至今,为何从 C++11 开始,引入unique_ptr 来替代 auto_ptr 呢?原因主要有如下几点:
(1)基于安全考虑。
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
当然,同样的策略也适用于复制构造函数,即auto_ptr<string> vocation(ps)时也需要上面的策略。每种方法都有其用途,但为何要摒弃 auto_ptr 呢?
下面举个例子来说明。
#include <iostream> #include <string> #include <memory> using namespace std; int main() { auto_ptr<string> films[5] ={ auto_ptr<string> (new string("Fowl Balls")), auto_ptr<string> (new string("Duck Walks")), auto_ptr<string> (new string("Chicken Runs")), auto_ptr<string> (new string("Turkey Errors")), auto_ptr<string> (new string("Goose Eggs")) }; auto_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针 cout << "The nominees for best avian baseballl film are\n"; for(int i = 0; i < 5; ++i) { cout << *films[i] << endl; } cout << "The winner is " << *pwin << endl; return 0; }
运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2] 已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把 auto_ptr 换成 shared_ptr 或 unique_ptr 后,程序就不会崩溃,原因如下: 使用 shared_ptr 时运行正常,因为 shared_ptr 采用引用计数,pwin 和 films[2] 都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。 使用 unique_ptr 时编译出错,与 auto_ptr 一样,unique_ptr 也采用所有权模型,但在使用 unique_ptr 时,程序不会等到运行阶段崩溃,而在编译期因下述代码行出现错误: unique_ptr<string> pwin; pwin = films[2]; //films[2] loses ownership
从上面可见,unique_ptr 比 auto_ptr 更加安全,因为 auto_ptr 有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移,如下代码所示: unique_ptr<string> upt(new string("lvlv")); unique_ptr<string> upt1(upt); //编译出错,已禁止拷贝 unique_ptr<string> upt1=upt; //编译出错,已禁止拷贝 unique_ptr<string> upt1=std::move(upt); //控制权限转移 auto_ptr<string> apt(new string("lvlv")); auto_ptr<string> apt1(apt); //编译通过 auto_ptr<string> apt1=apt; //编译通过
unique_ptr<string> upt1=std::move(upt); //控制权限转移 if(upt.get()!=nullptr) //判空操作更安全 { //do something }
unique_ptr<string> demo(const char* s) { unique_ptr<string> temp (new string(s)); return temp; }
//假设编写了如下代码: unique_ptr<string> ps; ps = demo('Uniquely special");
(3)扩展 auto_ptr 不能完成的功能。 //方式一: vector<unique_ptr<string>> vs { new string{“Doug”}, new string{“Adams”} }; //方式二: vector<unique_ptr<string>>v; unique_ptr<string> p1(new string("abc"));
unique_ptr<int[]> p (new int[3]{1,2,3}); p[0] = 0;// 重载了operator[]
void end_connection(connection *p) { disconnect(*p); } //资源清理函数
//资源清理器的“类型” unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);// 传入函数名,会自动转换为函数指针
3.shared_ptr
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:
3.2 通过辅助类模拟实现 shared_ptr class Point { private: int x, y; public: Point(int xVal = 0, int yVal = 0) :x(xVal), y(yVal) {} int getX() const { return x; } int getY() const { return y; } void setX(int xVal) { x = xVal; } void setY(int yVal) { y = yVal; } };
(2)辅助类 class RefPtr { private: friend class SmartPtr; RefPtr(Point *ptr):p(ptr),count(1){ } ~RefPtr(){delete p;} int count; Point *p; };
做好前面的准备后,我们可以为基础对象类 Point 书写一个智能指针类了。根据引用计数实现关键点,我们可以写出如下智能指针类: class SmartPtr { public: //构造函数 SmartPtr() { rp = nullptr; } SmartPtr(Point *ptr):rp(new RefPtr(ptr)) {} SmartPtr(const SmartPtr &sp):rp(sp.rp) { ++rp->count; cout << "in copy constructor" <<endl; } //重载赋值运算符 SmartPtr& operator=(const SmartPtr& rhs) { ++rhs.rp->count; if (rp != nullptr && --rp->count == 0) { delete rp; } rp = rhs.rp; cout << "in assignment operator" << endl; return *this; } //重载->操作符 Point* operator->() { return rp->p; } //重载*操作符 Point& operator*() { return *(rp->p); } ~SmartPtr() { if (--rp->count == 0) delete rp; else cout << "还有" << rp->count << "个指针指向基础对象" << endl; } private: RefPtr* rp; };
(4)智能指针类的使用与测试 int main() { //定义一个基础对象类指针 Point *pa = new Point(10, 20); //定义三个智能指针类对象,对象都指向基础类对象 pa //使用花括号控制三个智能指针的生命周期,观察计数的变化 { SmartPtr sptr1(pa);//此时计数 count=1 cout <<"sptr1:"<<sptr1->getX()<<","<<sptr1->getY()<<endl; { SmartPtr sptr2(sptr1); //调用拷贝构造函数,此时计数为 count=2 cout<<"sptr2:" <<sptr2->getX()<<","<<sptr2->getY()<<endl; { SmartPtr sptr3; SmartPtr sptr3=sptr1; //调用赋值操作符,此时计数为 conut=3 cout<<"sptr3:"<<(*sptr3).getX()<<","<<(*sptr3).getY()<<endl; } //此时count=2 } //此时count=1; } //此时count=0;对象 pa 被 delete 掉 cout << pa->getX() << endl; return 0; }
sptr1:10,20
(5)对智能指针的改进
//模板类作为友元时要先有声明 template <typename T> class SmartPtr; //辅助类 template <typename T> class RefPtr { private: //该类成员访问权限全部为private,因为不想让用户直接使用该类 friend class SmartPtr<T>; //定义智能指针类为友元,因为智能指针类需要直接操纵辅助类 //构造函数的参数为基础对象的指针 RefPtr(T *ptr):p(ptr), count(1) {} //析构函数 ~RefPtr() { delete p; } //引用计数 int count; //基础对象指针 T *p; }; //智能指针类 template <typename T> class SmartPtr { public: //构造函数 SmartPtr(T *ptr) :rp(new RefPtr<T>(ptr)) {} //拷贝构造函数 SmartPtr(const SmartPtr<T> &sp):rp(sp.rp) { ++rp->count; } //重载赋值操作符 SmartPtr& operator=(const SmartPtr<T>& rhs) { ++rhs.rp->count; //首先将右操作数引用计数加1, if (--rp->count == 0) //然后将引用计数减1,可以应对自赋值 delete rp; rp = rhs.rp; return *this; } //重载*操作符 T & operator *() { return *(rp->p); } //重载->操作符 T* operator ->() { return rp->p; } //析构函数 ~SmartPtr() { if (--rp->count == 0) //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象 { delete rp; } else { cout << "还有" << rp->count << "个指针指向基础对象" << endl; } } private: RefPtr<T> *rp; //辅助类对象指针 };
现在使用智能指针类模板来共享其它类型的基础对象,以int为例:
int main() { //定义一个基础对象类指针 int* ia = new int(10); { SmartPtr<int> sptr1(ia); cout <<"sptr1:"<<*sptr1<<endl; { SmartPtr<int> sptr2(sptr1); cout <<"sptr2:"<<*sptr2<<endl; *sptr2=5; { SmartPtr<int> sptr3=sptr1; cout <<"sptr3:"<<*sptr3<<endl; } } } //此时count=0;pa对象被delete掉 cout << *ia << endl; return 0; }
测试结果如下:
sptr1:10
4.2 weak_ptr 用法
weak_ptr<T> w; //创建空 weak_ptr,可以指向类型为 T 的对象
#include < assert.h> #include <iostream> #include <memory> #include <string> using namespace std; int main() { shared_ptr<int> sp(new int(10)); assert(sp.use_count() == 1); weak_ptr<int> wp(sp); //从 shared_ptr 创建 weak_ptr assert(wp.use_count() == 1); if (!wp.expired()) //判断 weak_ptr 观察的对象是否失效 { shared_ptr<int> sp2 = wp.lock();//获得一个shared_ptr *sp2 = 100; assert(wp.use_count() == 2); } assert(wp.use_count() == 1); cout << "int:" << *sp << endl; return 0;
程序输出:
int:100
4.3 weak_ptr 的作用
#include <iostream> #include <memory> class Woman; class Man { private: //std::weak_ptr<Woman> _wife; std::shared_ptr<Woman> _wife; public: void setWife(std::shared_ptr<Woman> woman) { _wife = woman; } void doSomthing() { if(_wife.lock()) {} } ~Man() { std::cout << "kill man\n"; } }; class Woman { private: //std::weak_ptr<Man> _husband; std::shared_ptr<Man> _husband; public: void setHusband(std::shared_ptr<Man> man) { _husband = man; } ~Woman() { std::cout <<"kill woman\n"; } }; int main(int argc, char** argv) { std::shared_ptr<Man> m(new Man()); std::shared_ptr<Woman> w(new Woman()); if(m && w) { m->setWife(w); w->setHusband(m); } return 0; }
在 Man 类内部会引用一个 Woman,Woman 类内部也引用一个 Man。当一个 man 和一个 woman 是夫妻的时候,他们直接就存在了相互引用问题。man 内部有个用于管理wife生命期的 shared_ptr 变量,也就是说 wife 必定是在 husband 去世之后才能去世。同样的,woman 内部也有一个管理 husband 生命期的 shared_ptr 变量,也就是说 husband 必须在 wife 去世之后才能去世。这就是循环引用存在的问题:husband 的生命期由 wife 的生命期决定,wife 的生命期由 husband 的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。
一般来讲,解除这种循环引用有下面三种可行的方法:
weak_ptr 对象引用资源时不会增加引用计数,但是它能够通过 lock() 方法来判断它所管理的资源是否被释放。做法就是上面的代码注释的地方取消注释,取消 Woman 类或者 Man 类的任意一个即可,也可同时取消注释,全部换成弱引用 weak_ptr。
另外很自然地一个问题是:既然 weak_ptr 不增加资源的引用计数,那么在使用 weak_ptr 对象的时候,资源被突然释放了怎么办呢?不用担心,因为不能直接通过 weak_ptr 来访问资源。那么如何通过 weak_ptr 来间接访问资源呢?答案是在需要访问资源的时候 weak_ptr 为你生成一个shared_ptr,shared_ptr 能够保证在 shared_ptr 没有被释放之前,其所管理的资源是不会被释放的。创建 shared_ptr 的方法就是 lock() 成员函数。
注意: shared_ptr 实现了 operator bool() const 方法来判断被管理的资源是否已被释放。
5.如何选择智能指针 在了解 STL 的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。
(2)如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋值给另一个的算法(如 sort())。例如,可在程序中使用类似于下面的代码段。 unique_ptr<int> make_int(int n) { return unique_ptr<int>(new int(n)); } void show(unique_ptr<int>& p1) { cout << *p1 << ' '; } int main() { //... vector<unique_ptr<int>> vp(size); for(int i = 0; i < vp.size(); i++) { vp[i] = make_int(rand() % 1000); //copy temporary unique_ptr } vp.push_back(make_int(rand() % 1000)); //ok because arg is temporary for_each(vp.begin(), vp.end(), show); //use for_each()
其中 push_back 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给 show() 传递对象,for_each() 将非法,因为这将导致使用一个来自 vp 的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。 在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个 unique_ptr 需要满足的条件相同,即 unique_ptr 必须是一个临时对象。与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>: unique_ptr<int> pup(make_int(rand() % 1000)); // ok 在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是更好的选择。如果你的编译器没有unique_ptr,可考虑使用 Boost 库提供的 scoped_ptr,它与 unique_ptr 类似。 (3)虽然说在满足 unique_ptr 要求的条件时,使用 auto_ptr 也可以完成对内存资源的管理,但是因为 auto_ ptr 不够安全,不提倡使用,即任何情况下都不应该使用 auto_ptr。 (4)为了解决 shared_ptr 的循环引用问题,我们可以祭出 weak_ptr。 (5)在局部作用域(例如函数内部或类内部),且不需要将指针作为参数或返回值进行传递的情况下,如果对性能要求严格,使用 scoped_ptr 的开销较 shared_ptr 会小一些。
< |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论