C++内存管理之shared

 ----------------------------------------shared_ptr---------------------------------------

引子

  c++中动态内存的管理是通过new和delete来完成的,只要保证new和delete的配对使用,是没有问题的。但是有时候我们会忘记释放内存,甚至有时候我们根本就不知道什么时候释放内存。特别时在多个线程间共享数据时,更难判断内存该何使释放。这种情况下就机器容易产生引用非法内存的指针。                                                                                                    

  为了更容易(同时也更安全的管)的使用动态内存,新的标准库(C++11)提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似于常规指针。重要的区别是它负责自动释放所指向的对象。新标准提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。

 初始化 sahred_ptr

  智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:

#include <iostream>using namespace std;int main(){    /*---------空指针------------*/   shared_ptr<string> p1;
    if(!p1)                         //!默认初始化的智能指针中保存着一个空指针!并不是""空字符串        cout<<"p1==NULL"<<endl;

  /*---------初始化------------*/
  shared_ptr<string> p2(new string);   if(p2&&p2->empty()){         //!需要注意的时empty时属于string的成员函数。     *p2="helloworld";     cout<<*p2<<endl;  }
//    shared_ptr<int> pa = new int(1);//!error:不允许以暴露裸漏的指针进行赋值操作。

  //一般的初始化方式    shared_ptr<string> pint(new string("normal usage!"));    cout<<*pint<<endl;    //推荐的安全的初始化方式    shared_ptr<string> pint1 = make_shared<string>("safe uage!");    cout<<*pint1<<endl;
}

关于其它初始化智能指针的方法,如下;不推荐!在这里展示出来是希望极力避免不安全的使用范例。

/*不推荐*/    int * p = new int(32);    shared_ptr<int> pp(p);    cout<<*pp<<endl;    /*意外的情况*///    delete p;               //!不小心把delete掉了。//    cout<<*pp<<endl;·       //!pp也不再有效。

关于get()函数;

智能指针定义了一个名为get的函数,它返回一个内置指针,指向智能指针的管理的对象。此函数设置的初衷是当我们向不能使用智能指针的代码传递一个内置指针。使用get返回指针的代码不能delete此指针。

#include <iostream>#include <memory>using namespace std;void useShared_ptr(int *p){    cout<<*p<<endl;}void delePointer(int *p){    delete p;}int main(int argc, char *argv[]){    shared_ptr<int> p1 = make_shared<int>(32);//    shared_ptr<int>p2(p1.get());  //!错误的用法:但是p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了。    useShared_ptr(p1.get());//    delePointer(p1.get());        //!error:    return 0;}

再次声明:get用来将指针的访问权限传递给代码,只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值!

 关于mak_shared函数:

  最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。与只能指针一样,make_shared也定义在头文件memory中。

#include <iostream>
using namespace std;int main(){    shared_ptr<int> p3 = make_shared<int>(42);    cout<<*p3<<endl;    shared_ptr<string> pstr = make_shared<string>("99999");    cout<<*pstr<<endl;    shared_ptr<int> pint = make_shared<int>(); //!默认初始化为 0    cout<<*pint<<endl;    auto pau = make_shared<string>("auto");    //!更简单,更常用的方式。    cout<<*pau<<endl;}

  使用make_shared用其参数来构造给定类型的对象;传递的参数必须能够与该类型的某个构造函数相匹配

  通常我们用auto来定义一个对象来保存make_shared的结果,这种方式更为简单。

shared_ptr的拷贝和赋值

  当进行拷贝或者赋值操作时,每个shared_ptr都会记录有多少个其他的shared_ptr指向相同的对象:

#include <iostream>
using namespace std;int main(){    auto p = make_shared<int>(42); //!p指向的对象只有p一个引用者。    cout<<p.use_count()<<endl;    auto q(p);                     //!p和q指向相同的对象,此对象有两个引用者。    cout<<p.use_count()<<endl;    return 0;}

shared_ptr作返回值:

#include <iostream>
using namespace std;shared_ptr<string> factory(const char* p){    return make_shared<string>(p);}void use_factory(){    shared_ptr<string> p = factory("helloworld");     cout<<*p<<endl;          //!离开作用域时,p引用的对象被销毁。

} 
shared_ptr<string> return_share_ptr(){   shared_ptr<string> p = factory("helloworld");   cout<<*p<<endl;   return p;               //!返回p时,引用计数进行了递增操作。 }                      //!p离开了作用域,但他指向的内存不会被释放掉。 

int main() {   use_factory();   auto p = return_share_ptr();   cout<<p.use_count()<<endl; }

引用计数:

  可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr去初始化另一个shared_ptr;当我们给shared_ptr赋予一个新的值或者是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。

#include <iostream>using namespace std;int main(){    auto p = make_shared<int>(42); //!指向的对象只有p一个引用者。    cout<<p.use_count()<<endl;    auto q = make_shared<int>(56);//!指向的对象只有q一个引用者。    cout<<q.use_count()<<endl;    cout<<"---------afterAssin-----"<<endl;    p = q;                        //!p原来引用的对象经过赋值之后释放掉了,q引用的对象有了p和q两个引用。    cout<<*p<<"=="<<*q<<endl;    cout<<q.use_count()<<endl;}

其他shared_ptr操作

  shared_ptr还定义了一些其他的操作,参考前面的shared_ptr操作表格,例如,我们可以用reset将一个 新的指针赋予一个shared_ptr:

#include <iostream>#include <memory>using namespace std;int main(){    shared_ptr<string> p1(new string("helloworld--1"));//    p1 = new string("helloworld2--2");//error!        p1.reset(new string("helloworld2--2"));    cout<<*p1<<endl;}

  与赋值类似,reset会更新(-1)引用计数,如果需要的话,会释放p1指向的对象。reset成员经常与unique一起使用,来控制多个shared_ptr的共享对象。在改变底层对象之前,我们在检查自己是否是当前对象仅有的用户。如果不是,在改变之前要做一份新的拷贝:

#include <iostream>#include <memory>using namespace std;int main(){    shared_ptr<string> p1(new string("helloworld--1"));    shared_ptr<string> p2(p1);    if(p1.unique())        cout<<*p1 + string(" is unique!")<<endl;    else{        p1.reset(new string("new reset!"));        cout<<*p1<<endl;    }}

容器中的shared_ptr-记得用erease节省内存 

  对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,他就不会被释放掉。由于这个特性,保证shared_ptr在不用之后不再保留就非常重要了,通常这个过程能够自动执行而不需要人工干预,有一种例外就是我们将shared_ptr放在了容器中。所以永远不要忘记erease不用的shared_ptr。

#include <iostream>
using namespace std;
int main(){    list<shared_ptr<string>>pstrList;    pstrList.push_back(make_shared<string>("1111"));    pstrList.push_back(make_shared<string>("2222"));    pstrList.push_back(make_shared<string>("3333"));    pstrList.push_back(make_shared<string>("4444"));    for(auto p:pstrList)    {        if(*p == "3333");        {            /*do some thing!*/        }        cout<<*p<<endl;    }    /*包含"3333"的数据我们已经使用完了!*/    list<shared_ptr<string>>::iterator itr = pstrList.begin();    for(;itr!=pstrList.end();++itr)    {        if(**itr == "3333"){            cout<<**itr<<endl;            pstrList.erase(itr);        }    }    cout<<"-------------after remove------------"<<endl;    for(auto p:pstrList)    {        cout<<*p<<endl;    }

  while(1)  {    /*do somthing other works!*/    /*遍历 pstrList*/    //!这样不仅节约了大量内存,也为容器的使用增加了效率    }
 }

状态共享——why use shared_ptr?

  使用shared_ptr在一个常见的原因是允许多个多个对象共享相同的状态,而非多个对象独立的拷贝!

#include <iostream>using namespace std;void copyCase(){    list<string> v1({"1","b","d"});    list<string> v2 = v1;        //!v1==v2占用两段内存    v1.push_back("cc");            //!v1!=v2    for(auto &p:v1){        cout<<p<<endl;    }    cout<<"--------void copyCase()---------"<<endl;    for(auto &p:v2){        cout<<p<<endl;    }} //v1和v2分属两个不同的对象,一个改变不会影响的状态。void shareCase(){    shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb");    shared_ptr<list<string>> v2 = v1;    (*v1).push_back("c2c");    for(auto &p:*v1){        cout<<p<<endl;    }    cout<<"----------shareCase()--------"<<endl;    for(auto &p:*v2){        cout<<p<<endl;    }} //v1和v2属于一个对象的两个引用,有引用计数为证,其内容的改变是统一的。int main(){    copyCase();    cout<<"++++++++++++++++"<<endl;    shareCase();}

 

智能指针与异常

  异常发生后,常规的动态内存常常不能正确释放。但是如果使用智能指针,即程序过早结束,智能指针也能确保在内存不需要时将其释放:

void f(){     shared_ptr<int>sp(new int(42)) ; }

  函数的推出,要么有两种情况,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针。所以会被释放掉。

  与之相对的,当发生异常时,我们直接管理的内存时不会自动释放的,如果使用内置指针管理内存,且在new之后对应的delet之前发生异常,则内存不会释放。

void f(){    int *p = new int(42);    //code//!异常抛出,且没有在f()中被捕获。    delete p;      }

  如果在new和delete之间发生异常,且异常未在f()中捕获,则内存就永远不会被释放了。

shared_ptr对象的销毁

1)管理动态数组

  默认情况下,shared_ptr指向的动态的内存是使用delete来删除的。这和我们手动去调用delete然后调用对象内部的析构函数是一样的。与unique_ptr不同,shared_ptr不直接管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器来替代delete 。

#include <iostream>using namespace std;class DelTest{public:    DelTest(){        j= 0;        cout<<" DelTest()"<<":"<<i++<<endl;    }    ~DelTest(){        i = 0;        cout<<"~ DelTest()"<<":"<<i++<<endl;    }  static int i,j;};

int DelTest::i = 0;int DelTest::j = 0;void noDefine(){    cout<<"no_define start running!"<<endl;    shared_ptr<DelTest> p(new DelTest[10]);}void slefDefine(){    cout<<"slefDefine start running!"<<endl;    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});}                     //!传入lambada表达式代替delete操作。int main(){    noDefine();   //!构造10次,析构1次。内存泄漏。    cout<<"----------------------"<<endl;    slefDefine();  //!构造次数==析构次数 无内存泄漏}

  通过自定义删除器的方式shared_ptr虽然管理的是一个动态数组。但是shard_ptr并不支持下标运算符的操作。而且智能指针类型不支持指针算术运算(不能取地址)。因此为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。

2)管理非常规动态对象

  某些情况下,有些动态内存也不是我们new出来的,如果要用shared_ptr管理这种动态内存,也要自定义删除器。

#include <iostream>#include <stdio.h>#include <memory>using namespace std;void closePf(FILE * pf){    cout<<"----close pf after works!----"<<endl;    fclose(pf);}int main(){//    FILE * fp2 = fopen("bin2.txt", "w");//    if(!pf)//        return -1;//    char *buf = "abcdefg";//    fwrite(buf, 8, 1, fp2);//    fclose(fp2);    shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);    cout<<"*****start working****"<<endl;    if(!pf)        return -1;    char *buf = "abcdefg";    fwrite(buf, 8, 1, pf.get());    //!确保fwrite不会删除指针的情况下,可以将shared_ptr内置指针取出来。    cout<<"----write int file!-----"<<endl;}                  //!即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose的问题。

  在这里可以设想一下TCP/IP中链接的打开和关闭的情况,同理都可以使用智能指针来管理。

 总结:

  最后总结一下上面所陈述的内容,也是shared_ptr使用的基本规范

1)不使用相同的内置指针值初始化(或reset)多个智能指针。

2)不delete get函数返回的指针。

3)如果你使用了get返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。

4)如果你使用智能指针管理的资源不是new分配的内存,记得传递给他一个删除器。

 weak_ptr

weakptr使用的比较少,如有兴趣了解,请去参考该篇文章:https://www.cnblogs.com/DswCnblog/p/5628314.html

(0)

相关推荐

  •  C++11中shared

    在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化:delete,接受一个动态对象的指针,销毁该对象,并释放与之 ...

  • C 11中智能指针的原理、使用、实现

    目录 理解智能指针的原理 智能指针的使用 智能指针的设计和实现 1.智能指针的作用 C 程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理.程序员自己管理堆内存可以提高了程序的 ...

  • 现代C一文读懂智能指针

    https://m.toutiao.com/is/JHS4MVf/ 智能指针 C++11 引入了 3 个智能指针类型: std::unique_ptr<T> :独占资源所有权的指针. st ...

  • c 11新特性之智能指针

    很多人谈到c++,说它特别难,可能有一部分就是因为c++的内存管理吧,不像java那样有虚拟机动态的管理内存,在程序运行过程中可能就会出现内存泄漏,然而这种问题其实都可以通过c++11引入的智能指针来 ...

  • 万字整理,肝翻Linux内存管理所有知识点

    Linux的内存管理可谓是学好Linux的必经之路,也是Linux的关键知识点,有人说打通了内存管理的知识,也就打通了Linux的任督二脉,这一点不夸张.有人问网上有很多Linux内存管理的内容,为什 ...

  • 图解 Go 内存管理分配

    GCTT:dust347 Go语言中文网 今天 Illustration created for "A Journey With Go", made from the origin ...

  • Linux 内存管理之vmalloc

    走进vmalloc 根据前面的系列文章,我们知道了buddy system是基于页框分配器,kmalloc是基于slab分配器,而且这些分配的地址都是物理内存连续的.但是随着碎片化的积累,连续物理内存 ...

  • Python一切皆是对象,但这和内存管理有什么关系?

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理. PS:如有需要Python学习资料的小伙伴可以点击下方链接自行获取 Python免费学习资料 ...

  • Linux 内存管理之CMA

    什么是CMA CMA是reserved的一块内存,用于分配连续的大块内存.当设备驱动不用时,内存管理系统将该区域用于分配和管理可移动类型页面:当设备驱动使用时,此时已经分配的页面需要进行迁移,又用于连 ...

  • HBase原理|HBase内存管理之MemStore进化论

    Java工程中内存管理总是一个绕不过去的知识模块,无论HBase.Flink还是Spark等,如果使用的JVM堆比较大同时对读写延迟等性能有较高要求,一般都会选择自己管理内存,而且一般都会选择使用部分 ...

  • 看完这篇你还能不懂C语言/C 内存管理?

    C 语言内存管理指对系统内存的分配.创建.使用这一系列操作.在内存管理中,由于是操作系统内存,使用不当会造成毕竟麻烦的结果.本文将从系统内存的分配.创建出发,并且使用例子来举例说明内存管理不当会出现的 ...

  • 操作系统的内存管理算法

    本文主要介绍内存的基本概念以及操作系统的内存管理算法. 嵌入式专栏 1 内存的基本概念 内存是计算机系统中除了处理器以外最重要的资源,用于存储当前正在执行的程序和数据.内存是相对于CPU来说的,CPU ...

  • 操作系统~内存管理之覆盖与交换、连续内存分配

    文章目录 内存保护 内存覆盖 内存交换技术 内存分配 单一连续分配 固定分区分配 动态分区分配 动态分配算法 进程的运行原理 - 指令 逻辑地址VS物理地址 什么是内存?有何作用 内存管理 什么是内存 ...