16.5 C++智能指针-unique_ptr

16.1 C++智能指针-new/delete探秘
16.2 C++智能指针-shared_ptr
16.3 C++智能指针-weak_ptr
16.4 C++智能指针-shared_ptr使用场景、陷阱、性能分析与使用建议
16.5 C++智能指针-unique_ptr

5.unique_ptr简介与常用操作

  5.1 unique_ptr简介

    讲解了不少shared_ptr的知识,但是谈到使用智能指针,一般来说,最先想到和优先考虑选择使用的还是unique_ptr智能指针。
    unique_ptr智能指针是一种独占式智能指针,或者理解成专属所有权这种概念也可以,也就是说,同一时刻,只能有一个unique_ptr指针指向这个对象(这块内存)。当这个unique_ptr被销毁的时候,它所指向的对象也会被销毁。

(1)常规初始化(unique_ptr和new配合)

{
    unique_ptr<int> pi; //可以指向int对象的一个空指针
    if (pi == nullptr)//条件成立
    {
        cout << "pi目前还是空指针" << endl; 
    }
    unique_ptr<int> pi2(new int(105)); //定义该智能指针时,直接把它绑定到一个new返回的指针上,此时pi2就指向一个值为105的int对象了	
}

(2)make_unique函数

    C++11中没有make_unique函数,但是C++14里提供了这个函数。
    与常规初始化比,也是要优先选择使用make_unique函数,这代表着更高的性能。当然,后续会讲解“删除器”概念,如果想使用删除器,那么就不能使用make_unique函数,因为make_unique不支持指定删除器的语法

{
    unique_ptr<int> p1 = std::make_unique<int>(100);		
    auto p2 = std::make_unique<int>(200); //可以用auto简写
    shared_ptr<int> p3(new int(100)); //int重复两次。而且不能使用auto来简写,不然p3就变成普通指针(裸指针)了而不是智能指针
}

  5.2 unique_ptr常用操作

(1)unique_ptr不支持的操作

{
    unique_ptr<string> ps1(new string("I Love China!")); 
    //unique_ptr<string> ps2(ps1); //不可以,该智能指针不支持拷贝动作
    //unique_ptr<string> ps3 = ps1; //不可以,该智能指针不支持拷贝动作
    //unique_ptr<string> ps4;
    //ps4 = ps1; //不可以,该智能指针不支持赋值动作
}

    总结:unique_ptr不允许复制、赋值等动作,是一个只能移动不能复制的类型。

(2)移动语义

    虽然刚刚讲述了unique_ptr不支持的操作,如它不支持复制动作,但是它支持移动。看一看这种移动语义的写法。
可以通过std::move来将一个unique_ptr转移到其他的unique_ptr:

{
    unique_ptr<string> ps1(new string("I Love China!"));
    unique_ptr<string> ps3 = std::move(ps1);  //转移后ps1为空了,ps3指向原来ps1所指
}

(3)release成员函数

    放弃对指针的控制权(切断了智能指针和其所指向的对象之间的联系),返回指针(裸指针),将智能指针置空。返回的这个裸指针可以手工delete释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值。

{
    //将所有权从ps1转移(移动)给ps2:
    unique_ptr<string> ps1(new string("I Love China!"));
    unique_ptr<string> ps2(ps1.release());
    if (ps1 == nullptr)//条件成立
    {
        cout << "ps1被置空" << endl; 
    }
    //ps2.release();   //这会导致内存泄漏
    string* tempp = ps2.release(); //或者写成auto tempp = ps.release();
    delete tempp;
}

(4)reset成员函数

    当reset不带参数时,释放智能指针指向的对象,并将智能指针置空。当reset带参数时,释放智能指针原来所指向的内存,让该智能指针指向新内存。

{
    unique_ptr<string> prs(new string("I Love China!"));
    prs.reset(); //当reset()不带参数时,释放prs指向的对象,并将prs置空
    if (prs == nullptr)//条件成立
    {
        cout << "prs被置空" << endl; 
    }
}
{
    unique_ptr<string> prsdc(new string("I Love China 1!"));
    unique_ptr<string> prsdc2(new string("I Love China 2!"));
    //当prsdc2.reset(......)中带参数时,释放prsdc2原来所指向的内存,让prsdc2指向新内存
    prsdc2.reset(prsdc.release());  //reset释放原来prsdc2指向的对象内存,让prsdc2指向prsdc所指向的内存,同时prsdc被置空
    
    prsdc2.reset(new string("I Love China!")); //reset参数可以是个裸指针,reset释放原来prsdc2指向的对象内存,让prsdc2指向新new出来的string		
}

(5)=nullptr;

    释放智能指针所指向的对象,并将智能指针置空。

{
    unique_ptr<string> ps1(new string("I Love China!"));
    ps1 = nullptr; //释放ps1指向的对象,并将ps1置空
}

(6)指向一个数组

{
    std::unique_ptr<int[]> ptrarray(new int[10]); //前面带上空括号[]表示是数组,下面行才可以用[下标]来引用数组元素
    ptrarray[0] = 12; //数组提供索引运算符[]
    ptrarray[1] = 24;
    ptrarray[9] = 124;  //能访问的下标是0-9,不要超过这个范围,否则可能导致程序异常		
}
class A
{
public:
    A()
    {
    }
    ~A() //有自己的析构函数
    {
    }
};
{
    //std::unique_ptr<A> ptrarray(new A[10]); //一个类A的数组,而且类A有析构函数,但前面的<>中没有使用A[],就报异常。原因在16.2.1的5中已经解释了
    std::unique_ptr<A[]> ptrarray(new A[10]); //这个写法没有问题,也不会泄露内存,注意前面的<>中正常的书写为A[]
}

(7)get成员函数

    返回智能指针中保存的对象(裸指针)。小心使用,若智能指针释放了所指向的对象,则返回的对象也就变得无效了

{
    unique_ptr<string> ps1(new string("I Love China!"));
    string* ps = ps1.get();
    const char* p1 = ps->c_str();
    *ps = "This is a test very good!"; 
    const char* p2 = ps->c_str();  //调试观察不难发现p1和p2是不同的内存地址,这是string内部工作机制决定的
}

    为什么要有这样一个函数呢?主要是考虑到有些函数的参数需要的是一个内置指针(裸指针),所以需要通过get取得这个内置指针并传递给这样的函数。但要注意,不要delete这个get到的指针,否则会产生不可预料的后果。

(8)*解引用

    *p:解引用的感觉,获得p指向的对象。

{
    unique_ptr<string> ps1(new string("I Love China!"));
    const char* p1 = ps1->c_str();
    *ps1 = "This is a test very good!";
    const char* p2 = ps1->c_str();//调试观察不难发现p1和p2是不同的内存地址,这是string内部工作机制决定的
    std::unique_ptr<int[]> ptrarray(new int[10]); //对于定义的内容是数组,是没有*解引用运算符的
    //*ptrarray; //错误
}

(9)swap成员函数

    用于交换两个智能指针所指向的对象

{
    unique_ptr<string> ps1(new string("I Love China1!"));
    unique_ptr<string> ps2(new string("I Love China2!"));
    std::swap(ps1, ps2); //用全局函数也可以
    ps1.swap(ps2); //也可以
}

(10)智能指针名字作为判断条件

{
    unique_ptr<string> ps1(new string("I Love China1!"));
    //若ps1指向一个对象,则为true
    if (ps1) //条件成立
    {
    //执行
        cout << "ps1指向了一个对象" << endl;
    }
}

(11)转换成shared_ptr类型

    如果unique_ptr为右值,就可以将其赋给shared_ptr。模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr,shared_ptr将接管原来归unique_ptr所拥有的对象。

auto myfunc()
{
    return unique_ptr<string>(new string("I Love China!")); //这就是一个右值(短暂的临时对象,都是右值,14.12.4中详细说过)						
}
{
    shared_ptr<string> pss1 = myfunc(); //可以成功,引用计数为1
}

    另外前面讲过,一个shared_ptr创建的时候,它的内部指针会指向一个控制块,当时讲解过这个控制块创建时机,那么,把unique_ptr转换成shared_ptr的时候,系统也会为这个shared_ptr创建控制块。因为unique_ptr并不使用控制块,只有shared_ptr才使用控制块。

{
    unique_ptr<std::string> ps(new std::string("I Love China!"));
    shared_ptr<string> ps2 = std::move(ps); //执行后ps为空,ps2是shared_ptr且引用计数为1
}

  5.3 返回unique_ptr、删除器与尺寸问题

(1)返回unique_ptr

    虽然上面说过,unique_ptr智能指针不能复制。但有一个例外,如果这个unique_ptr将要被销毁,则还是可以复制的,最常见的是从函数返回一个unique_ptr。

unique_ptr<string> tuniqp()
{
    unique_ptr<string> pr(new string("I Love China!"));
    return pr;	//从函数返回一个局部unique_ptr对象是可以的 返回局部对象pr会导致系统生成临时unique_ptr对象,并调用unique_ptr的移动构造函数
}
unique_ptr<string> tuniqp()
{
    return unique_ptr<string>(new string("I Love China!"));
}
{
    unique_ptr<string> ps;
    ps = tuniqp(); //可以用ps接收tuniqp返回结果,则临时对象直接构造在ps里,如果不接收,则临时对象会释放,同时释放掉所指向的对象的内存
}

(2)删除器

    默认情况下,当析构一个unique_ptr时,如果这个智能指针非空(指向一个对象),则在unique_ptr内部,会用delete来删除unique_ptr所指向的对象(裸指针)。所以这里的delete可以看成是unique_ptr智能指针的默认删除器。程序员可以重载这个默认的删除器,换句话说,就是提供一个自己的删除器,提供的位置就在unique_ptr的尖括号“<>”里面,并在所指向的对象类型之后。
    那么,这个删除器究竟是什么呢?其实就是一个可调用对象,可调用对象在15.3.3节中有所提及,在后面的章节也会更详细地讲解。简单来说,例如:函数是可调用对象的一种,另外,如果一个类中重载了“()”运算符,就可以像调用函数一样来调用这个类的对象,这也叫可调用对象。
    前面已经学习过shared_ptr的删除器,shared_ptr的删除器指定比较简单,在参数中书写一个具体删除器名(如函数名、lambda表达式等)就可以了。
    而unique_ptr的删除器相对复杂一点,多了一步——先要在类型模板参数中传递进去类型名,然后在参数中再给具体的删除器名。看一看删除器的写法和使用方法。

    1>范例。

void mydeleter(string* pdel) 
{
    delete pdel;
    pdel = nullptr;
}
{
    typedef void(*fp)(string*); //定义一个函数指针类型,类型名为fp
    unique_ptr<string, fp> ps1(new string("I Love China!"), mydeleter);
}

    2>做个修改。

{
    using fp2 = void(*)(string*); //用using定义一个函数指针类型,类型名为fp2
    unique_ptr<string, fp2> ps2(new string("I Love China!"), mydeleter);
}

    3>继续在main主函数中做修改。

{
    typedef decltype(mydeleter)* fp3; //注意这里多了个*,因为decltype是返回函数类型,加*表示函数指针类型,现在fp3应该是void *(string *),decltype后面会讲
    unique_ptr<string, fp3> ps3(new string("I Love China!"), mydeleter);
}

    4>继续在main主函数中做修改。

{
    std::unique_ptr<string, decltype(mydeleter)*> ps4(new string("I Love China!"), mydeleter);
}

    5>改用lambda表达式的写法再看看

{
    auto mydella = [](string* pdel) {
        delete pdel;
        pdel = nullptr;
    };
    std::unique_ptr<string, decltype(mydella)> ps5(new string("I Love China!"), mydella);
    int ilen = sizeof(ps5);
}

  指定删除器额外说明
    还记得学习shared_ptr的时候曾说过:就算是两个shared_ptr指定的删除器不相同,只要它们所指向的对象相同,那么这两个shared_ptr也属于同一个类型。
    但是unique_ptr不同,指定unique_ptr中的删除器会影响unique_ptr的类型。因为在unique_ptr中,删除器类型是智能指针类型的一部分(在“<>”里),所以从这一点来讲,shared_ptr的设计更灵活。
    在讲解shared_ptr的时候,删除器不同,但指向类型(所指向对象的类型)相同的shared_ptr,可以放到同一个容器中。但到了unique_ptr这里,如果删除器不同,则就等于整个unique_ptr类型不同,那么,这种类型不同的unique_ptr智能指针没有办法放到同一个容器中去的。

(3)尺寸问题

    通常情况下,unique_ptr的尺寸与裸指针一样

{
    string* p;
    int ilenp = sizeof(p); //4(字节)
    unique_ptr<string> ps1(new string("I Love China!"));
    int ilen = sizeof(ps1);  //4(字节)
}

    可以看到,unique_ptr也基本和裸指针一样:足够小,操作速度也够快。但是,如果增加了删除器,那unique_ptr的尺寸可能不变化,也可能有所变化。
    1.如果删除器是lambda表达式这种匿名对象,unique_ptr的尺寸就没变化。
    2.如果删除器是一个函数,unique_ptr的尺寸就会发生变化。
    unique_ptr尺寸变大肯定对效率有一定影响,所以把一个函数当作删除器,还是要慎用。这一点与shared_ptr不同,shared_ptr是不管指定什么删除器,其大小都是裸指针的2倍。

智能指针总结

  智能指针背后的设计思想

    智能指针主要的目的就是帮助程序员释放内存,以防止忘记释放内存时造成内存泄漏。实际上,作为一个严谨的程序员,写出内存泄漏的代码当然是不应该的,但作为新手程序员,内存泄漏这样的事情确实是时有发生。

void myfunc()
{
    string* ps = new std::string("I Love China!");
    //....
    //....
    if (true) //当某个条件为真,就return,忘记释放内存导致泄漏
    {
        return;
    }
    delete ps; //释放内存
    return;
}

    这种本不应该出现的内存泄漏在已然发生的情况下,智能指针的好处就体现出来了。通过修改一下代码解决内存泄漏问题。这里笔者用C++98中的auto_ptr演示,读者可以先理解成auto_ptr和已经讲解的unique_ptr一样。

{
    std::auto_ptr<std::string> ps(new std::string("I Love China!"));
    if (true) //当某个条件为真,就return,忘记释放内存导致泄漏
    {
        return;
    }
    //delete ps; //释放内存
    return;
}

    代码经过上面的修改,就不需要担心忘记delete造成内存泄漏的问题了。所以,这里面的自动释放内存就是智能指针背后设计的思想。

  auto_ptr为什么被废弃

    auto_ptr是C++98时代的智能指针,具有unique_ptr的部分特性。实际上,在C++11新标准之前,也只有auto_ptr这么一个智能指针。而unique_ptr、shared_ptr、weak_ptr都是C++11新标准出现后才出现的。
    auto_ptr有些使用上的限制(缺陷),如不能在容器中保存auto_ptr,也不能从函数中返回auto_ptr。所以,在C++11新标准中,auto_ptr已经被unique_ptr取代(在支持C++11新标准的编译器上,读者也不要再使用auto_ptr了)。

{
    //std::auto_ptr<std::string> ps(new std::string("I Love China!"));
    //std::auto_ptr<std::string> ps2 = ps; //ps2指向字符串,ps变为空,这可以防止ps和ps2析构一个string两次,所以这个代码没问题
    
    //std::shared_ptr<std::string> ps(new std::string("I Love China!"));
    //std::shared_ptr<std::string> ps2 = ps; //ps2和ps都有效,引用计数为2
    
    //std::unique_ptr<std::string> ps(new std::string("I Love China!"));
    //std::unique_ptr<std::string> ps2 = ps;
    
    std::unique_ptr<std::string> ps(new std::string("I Love China!"));
    std::unique_ptr<std::string> ps2 = std::move(ps); //要用移动语义了
}

    虽然auto_ptr和unique_ptr都是独占式智能指针,但unique_ptr这种编译的时候就会报错,而不会默默地就把ps的所有权转移到ps2上去的方式,避免了后续误用ps导致程序崩溃的问题。
    不难看出,auto_ptr被废弃的主要原因就是设计的不太好,容易被误用引起潜在的程序崩溃等问题,所以C++11中使用unique_ptr来取代auto_ptr,因为unique_ptr比auto_ptr使用起来更安全。

  智能指针的选择

    如果程序中要使用多个指向同一个对象的指针,应选择shared_ptr。
    如果程序中不需要多个指向同一个对象的指针,则可使用unique_ptr。
    总之,在选择的时候,优先考虑使用unique_ptr,如果unique_ptr不能满足需求,再考虑使用shared_ptr。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值