天天看点

C++(STL):03---智能指针之shared_ptr

一、shared_ptr类

  • 头文件:#include<memory>
  • 智能指针,是一个模板。创建智能指针时,必须提供指针所指的类型
  • 如果当做前提条件判断,则是检测其是否为空
  1. shared_ptr<string> p1; //指向string
  2. shared_ptr<list<int>> p2;//指向int的list
  3. if(p1 && p1->empty())
  4. *p1="h1";

二、make_shared函数

  • 最安全的分配和使用动态内存的方法就是调用该函数
  • 此函数在内存中动态分配对象并初始化,返回此对象的shared_ptr
//指向一个值为42的int的shared_ptr
shared_ptr<int> p = make_shared<int>(42);
//p2指向一个值为10个'9'的string
shared_ptr<string> p2=make_shared<string>(10, '9');
//p3指向一个值初始化为0的int数
shared_ptr<int> p3 = make_shared<int>();      
  • 配合auto使用:make_shared函数可以赋值给auto,这样比较简单

auto p=make_shared<vector<string>>();

三、shared_ptr的拷贝、赋值与引用计数

  • 引用计数:shared_ptr类所指向的对象都有一个引用计数
  • 但对shared_ptr类进行拷贝时,计数器就会增加。例如:当用一个shared_ptr初始化另一个shared_ptr、或者它作为参数传递给一个函数以及作为函数的返回值,它所关联的计数器就会增加
  • 当我们给让shared_ptr指向另一个对象或者shared_ptr销毁时,原对象的计数器就会递减
  • 一旦一个shared_ptr的计数器为0,就会自动释放该对象的内存
auto p=make_shared<int>(42); //p指向一个引用者
auto q(p); //用p初始化q,那么p所指的对象计数器加1      
  1. auto r=make_shared<int>(42);
  2. r=q;
  • 将q赋值给r,那么:
  •  r原来所指的对象引用计数变为0,然后自动释放内存
  • q所指的对象的引用计数+1

四、shared_ptr的自动销毁对象内存机制

  • 由上面可知,当指向一个对象的最后一个shared_ptr对象被销毁时,shared_ptr类会自动销毁此对象。shared_ptr类是通过析构函数来完成销毁工作的
  • 内存浪费:因为只有在销毁掉最后一个shared_ptr时,该指针所指向的内存才会释放,因此如果你忘记了销毁程序不再需要的shared_ptr,程序仍然正在执行,那么就造成内存浪费

五、shared_ptr与作用域的关系

  • shared_ptr类所指向的内存何时被释放,与shared_ptr类的生存周期有关

演示案例:

  • 首先我们定义下面的函数返回一个指向于一个值的share_ptr指针
  1. shared_ptr<Foo> factory(T arg)
  2. {
  3. return make_share<Foo>(arg);//返回一个share_ptr类型的智能指针
  4. }
  • 情景一:例如下面函数调用factory函数来生成一个shared_ptr指针,但是p一旦离开了作用域(use_factory函数),那么p指针就失效了,因此p所指向的内存地址也就自动释放了
  1. //函数结束之后,p就自动释放它所指向的对象的内存
  2. void use_factory(T arg)
  3. {
  4. shared_ptr<Foo> p=factory(arg);
  5. }
  • 情景二:下面的函数也是 factory函数来生成一个shared_ptr指针,但是p指针通过返回值返回了,所以,如果有另一个shared_ptr指针调用了该函数,那么该p所指向的内存地址不会随着use_factory函数的调用而释放
  1. auto use_factory(T arg)
  2. {
  3. shared_ptr<Foo> p=factory(arg);
  4. return p;
  5. }

六、shared_ptr与new的使用

使用规则:

  • ①我们可以使用将shared_ptr类对象指向一个new所申请的动态内存
  • ②new申请的动态内存的使用、释放等规则仍然符合shared_ptr类的使用规则

使用语法:

  • 因为智能指针的构造函数是explicit的。因此:我们不能将一个内置指针隐式地转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针
  1. shared_ptr<int> p=new int(1024); //错误
  2. shared_ptr<int> p2(new int(1024)); //正确:使用直接初始化
  • 动态内存作为返回值时的使用手法:限于上面的使用语法,一个返回shared_ptr的函数不能在其返回语句中隐式转换为一个普通指针 
  1. shared_ptr<int> clone(int p)
  2. {
  3. return new int(p); //错误
  4. }
  5. shared_ptr<int> clone(int p)
  6. {
  7. return shared_ptr<int>(new int(p)); //正确
  8. }

七、shared_ptr类的函数传参使用

  • 当一个函数的参数是shared_ptr类时,有以下规则:
  • 函数的调用是传值调用
  • 调用函数时,该shared_ptr类所指向的对象引用计数加1。但是函数调用完成之后,shared_ptr类自动释放,对象的引用计数又减1
  1. void process(shared_ptr<int> ptr){ ... }
  2. shared_ptr<int> p(new int(42)); //初始化一个智能指针对象p
  3. process(p); //p所指的对象引用计数加1
  4. //process函数调用之后,p所指的引用计数减1
  5. int i=*p; //正确

函数参数使用时与new的关系:

  • 因为shared_ptr类会在生存周期结束之后,将引用计数减1,当引用计数为0时,会释放内存空间
  •  下面是一个特殊的应用场景,需要注意
  1. void process(shared_ptr<int> ptr){ ... }
  2. int *x(new int(1024));
  3. process(x); //错误,不能将int*转换为一个shared_ptr<int>
  4. process(shared_ptr<int>(x)); //合法的,但是process函数返回之后内存会被释放
  5. int j=*x; //错误,x所指的内存已经被释放了

八、get函数的使用

  • shared_prt类的get函数返回一个内置指针,指向智能指针所管理的对象
  • 此函数的设计情况:我们需要向不能使用智能指针的代码传递一个内置指针
  • get函数将内存的访问权限传递给一个指针,但是之后代码不会delete该内存的情况下,对get函数的使用才是最安全的
  • 永远不要用get初始化另一个智能指针或者为另一个智能指针赋值
  1. shared_ptr<int> p(new int(42)); //引用计数变为1
  2. int *q=p.get(); //正确:使用q需要注意,不要让它管理的指针被释放
  3. {//新语句块
  4. shared_ptr<int>(q); //用q初始化一个智能指针对象
  5. } //语句块结束之后,智能指针对象释放它所指的内存空间
  6. int foo=*p;//错误的,p所指的内存已经被释放了

九、reset、unique函数的使用

  • reset函数会将shared_prt类原先所指的内存对象引用计数减1,并且指向于一块新的内存
  1. shared_ptr<int> p;
  2. p=new int(1024); //错误:不能将一个指针赋予shared_ptr
  3. p=reset(new int(1034)); //正确,p指向一个新对象
  • reset函数与unqie函数配合使用:在改变对象之前,检查自己是否为当前对象的唯一用户
  1. shared_ptr<string> p=make_shared<string>("Hello");
  2. if(!p.unique()) //p所指向的对象还有别的智能指针所指
  3. p.reset(new string(*p)); //现在可以放心的改变p了
  4. *p+=newVal; //p所指向的对象只有自己一个智能指针,现在可以放心的改变对象的值了

十、异常处理

  • 当程序发生异常时,我们可以捕获异常来将资源被正确的释放。但是如果没有对异常进行处理,则有以下规则:
  • shared_ptr的异常处理:如果程序发生异常,并且过早的结束了,那么智能指针也能确保在内存不再需要时将其释放
  • new的异常处理:如果释放内存在异常终止之后,那么就造成内存浪费
  1. voif func()
  2. {
  3. shared_ptr<int> sp(new int(42));
  4. ...//此时抛出异常,未捕获,函数终止
  5. }//shared_ptr仍然会自动释放内存
  6. voif func()
  7. {
  8. int *ip=new int(42);
  9. ...//此时抛出异常,未捕获
  10. delete ip; //在退出之前释放内存,此语句没有执行到,导致内存浪费
  11. }

十一、重置shared_prt类删除器

  • 概念:前面介绍过,当shared_ptr生命周期结束时,会调用默认的析构函数来释放(delete)自己所指向的内存空间。但是我们可以使用shared_prt的语法来指定删除器函数,那么在shared_ptr生命周期结束时就会自动调用这个函数 

演示案例:

  • 下面演示一个shared_ptr指定删除器函数以及避免内存泄露的案例
  • 错误情景:我们调用f函数来打开一个网络连接,但是在f函数调用之后没有关闭这个连接。因此就会造成内存的泄露 
  1. struct destination; //连接的对象
  2. struct connection; //连接需要的信息
  3. connection connect(destbination*); //打开连接
  4. void disconnect(connection); //关闭连接
  5. void f(destination &d)
  6. {
  7. connection c=connect(&d);//打开一个连接
  8. ....//使用这个连接
  9. //如果在f函数退出之前忘记调用disconnect函数,那么该连接就没有关闭
  10. }
  • 正确情景:现在我们定义一个新的函数“end_connection”,并且配合shared_ptr类的使用。shared_ptr指定了一个删除器函数“end_connection”。因此下面的代码能够保证在f函数的各种调用结束时,保证连接正确的关闭
  1. void end_connection(connection *p)
  2. {
  3. disconnection (*p);
  4. }
  5. void f(destination &d)
  6. {
  7. connection c=connect(&d);
  8. shared_ptr<connection> p(&c,end_connection);
  9. ....//使用这个连接
  10. //当f函数退出或者异常退出,p都会调用end_connection函数
  11. }

十二、shared_prt与动态数组的使用 

  • 与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理动态数组,必须提供自己定义的删除器
  • 如果未提供删除器,shared_ptr默认使用delete删除动态数组,此时delete少一个“[]”,因为会产生错误
//本例中,传递给shared_ptr一个lambda作为删除器




shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; } );
shared_ptr<int> sp2(new int[3]{1,2,3}, [](int *p) { delete[] p; });
sp2.reset();  //使用自己书写的lambda释放数组      
  • 动态数组的访问:shared_ptr不支持点和箭头成员运算符访问数组,并且不提供下标运算符访问数组,只能通过get()函数来获取一个内置指针,然后再访问数组元素
shared_ptr<int> sp(new int[3]{1,2,3}, [](int *p) { delete[] p; });
for (size_t i = 0; i != 3; ++i)
*(sp.get() + i) = i;