本文转载自:内存管理之Share_Ptr

引子

C++中动态内存的管理是通过new和delete来完成的,只要保证new和delete的配对使用,是没有问题的。但是有时候我们会忘记释放内存,甚至有时候我们根本就不知道什么时候释放内存。特别时在多个线程间共享数据时,更难判断内存该何使释放。这种情况下就机器容易产生引用非法内存的。
  为了更容易(同时也更安全的管)的使用动态内存,新的标准库(C++11)提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似于常规指针。重要的区别是它负责自动释放所指向的对象。新标准提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则独占所指向的对象。标准库还定义了一个weak_ptr的伴随类,他是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
shared_ptr和unique_ptr都支持的操作
定义和改变shared_ptr的其他方法

初始化 sahred_ptr

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

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

  /*---------初始化------------*/
  shared_ptr<string> p1(new string); 
  if(p1&&p1->empty()){         //!需要注意的时empty时属于string的成员函数。 
    *p1="helloworld"; 
    cout<<*p1<<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初始化另一个智能指针或者为另一个智能指针赋值!

关于make_shared函数:

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回此对象的shared_ptr。与智能指针一样,make_ptr也定义在头文件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;
}
output:
1
2

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*/    //!这样不仅节约了大量内存,也为容器的使用增加了效率  
  }
 }

# output
1111
2222
3333
4444
3333
-------------after remove------------
1111
2222
4444

状态共享——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();
}
#output
1
b
d
cc
--------void copyCase()---------
1
b
d
++++++++++++++++
bb
bb
c2c
----------shareCase()--------
bb
bb
c2c
Program ended with exit code: 0

智能指针与异常

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

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

函数的退出,要么有两种情况,正常处理结束或者发生了异常,无论哪种情况,局部对象都会被销毁。在上面的程序中,sp是一个shared_ptr,因此sp销毁时会检查引用计数。在此例中,sp是指向这块内存的唯一指针。所以会被释放掉。
  与之相对的,当发生异常时,我们直接管理的内存时不会自动释放的,如果使用内置指针管理内存,且在new之后对应的delete之前发生异常,则内存不会释放。

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(){
        static int i = 0;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        static int i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
};

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();  //!构造次数==析构次数 无内存泄漏
}
#output
no_define start running!
 DelTest():0
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
~ DelTest():0
ZXPro(68858,0x1000c35c0) malloc: *** error for object 0x100509788: pointer being freed was not allocated
ZXPro(68858,0x1000c35c0) malloc: *** set a breakpoint in malloc_error_break to debug

通过自定义删除器的方式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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐