C++11标准智能指针 unique_ptr<> 类简单使用实例,和基本原理分析

本文将结合C++11标准中的智能指针std::unique_ptr<>类的简单使用实例,讨论其基本原理,以期快速了解该智能指针类的使用。

std::unique_ptr<> 是什么?

std::unique_ptr<>是C++语言中提供的一种智能指针类,使用它可以方便的管理指针,尽可能的避免内存泄漏。unique_ptr 对象可以用于维护普通(常常用于索引一块内存)的指针,在其生命周期结束后,自动地删除它,并释放相关的内存。unique_ptr 重载了->*运算符,因此可以像使用普通指针那样使用 unique_ptr 智能指针。下面是一段简单的C++语言代码示例,请看:

#include <iostream>
#include <memory>

struct Task
{
    int mId;
    Task(int id ) :mId(id)
    {
        std::cout<<"Task::Constructor"<<std::endl;
    }
    ~Task()
    {
        std::cout<<"Task::Destructor"<<std::endl;
    }
};

int main()
{
    // 创建一个 unique_ptr 对象,并且绑定到 new Task(23) 上
    std::unique_ptr<Task> taskPtr(new Task(23));
    // 通过智能指针访问成员变量
    int id = taskPtr->mId;
    std::cout<<id<<std::endl;

    return 0;
}

这段C++语言代码很简单,main() 函数首先创建了一个 unique_ptr 智能指针对象,new Task(23)本来需要一个Task *指针索引,现在直接使用 unique_ptr 对象 taskPtr 管理它,所以代码中没有使用delete删除相应的指针,因为“智能”指针对象 taskPtr 会在相关指针生命周期结束后自动地删除它。编译这段C++语言代码时,记得指定-std=c++11选项,最终得到的输出如下:

# g++ t1.cpp -std=c++11
# ./a.out 
Task::Constructor
23
Task::Destructor

事实上,不管函数是正常还是不正常(程序抛出异常等)的退出,taskPtr 的析构函数总是会被调用的,因此,taskPtr 管理的 raw 指针会被自动删除,避免内存泄漏。

unique_ptr 的“专享所有权”

unique_ptr 中的“unique”一词有着“唯一的,独一无二”的意思,这主要体现在所有权上,某个 raw 指针同时只能被一个 unique_ptr 指针绑定,我们不能拷贝 unique_ptr 对象,只能转移。事实上,鉴于 unique_ptr 对象相对于其管理的 raw 指针的独一无二特性,其内部不需要像shared_ptr<>智能指针类那样需要“引用计数”机制,一旦 unique_ptr 对象的析构函数被调用,它就会删除掉绑定的 raw 指针。

使用 unique_ptr<> 智能指针类

创建一个空 unique_ptr 对象

std::unique_ptr<> 本质上是一个在标准命名空间std中的模板类,使用它需要包含头文件<memory>,例如定义一个可以绑定 int 指针的对象的C++语言代码可以如下写:

#include <memory>

std::unique_ptr<int> ptr;

还可以在定义 unique_ptr 对象的时候传入需要与之绑定的“raw指针”,这一点在前面的C++语言代码实例中已经见过:

std::unique_ptr<Task> taskPtr(new Task(23));

需要注意,不能直接把 raw 指针直接赋值给 unique_ptr 对象,也就是说下面这行C++语言代码是非法的:

taskPtr = new Task(23); // 非法

检查 unique_ptr 对象是否为空

上面定义的 unique_ptr 对象 ptr 没有与任何 raw 指针绑定,因此它是空的。检查 unique_ptr 对象是否为空,一般有两种方法,相关的C++语言代码示例如下,请看:

  • 方法 1:
if(!ptr)
    std::cout<<"ptr is empty"<<std::endl;
  • 方法 2:
if(ptr == nullptr)
    std::cout<<"ptr is empty"<<std::endl;

重置(reset)unique_ptr

调用 unique_ptr 的 reset() 方法将删除与之绑定的 raw 指针,并且将 unique_ptr 对象置空:

taskPtr.reset();
// taskPtr==nullptr 为真

上面这行C++语言代码执行后,与 taskPtr 绑定的 raw 指针将会被删除,并且 taskPtr 被置空,也即taskPtr==nullptr为真。

unique_ptr 对象不可拷贝

鉴于 unique_ptr 不可拷贝,只能移动,所以我们不能通过拷贝构造函数活着赋值操作拷贝 unique_ptr 对象,下面这两行C++语言代码都是非法的:

std::unique_ptr<Task> taskPtr2 = taskPtr; // 非法
taskPtr = taskPtr2; // 非法

转移 unique_ptr 对象的所有权

如前文所述,我们不能拷贝 unique_ptr 对象,却可以移动它,所谓“移动”,其实就是转移所有权,请看下面这个示例,首先创建一个 unique_ptr 对象:

std::unique_ptr<Task> taskPtr2(new Task(55));

此时 taskPtr2 显然不为空。现在将其对绑定 raw 指针的所有权转移到一个新的 unique_ptr 对象,相关的C++语言代码可以如下写:

std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);

if(taskPtr2 == nullptr)
    std::cout<<"taskPtr2 is  empty"<<std::endl;

// taskPtr2 对 raw 指针的所有权被转移给 taskPtr4 了
if(taskPtr4 != nullptr)
    std::cout<<"taskPtr4 is not empty"<<std::endl;

std::move() 函数将 taskPtr2 转换为右值(rvalue)引用,所以 unique_ptr 的移动构造函数可以将与 taskPtr2 绑定的 raw 指针转移给 taskPtr4,在这之后,taskPtr2 变为空。

释放对绑定 raw 指针的所有权

unique_ptr 的 release() 函数可以直接将对绑定 raw 指针的所有权释放,该函数会将绑定的 raw 指针返回,请看下面的C++语言代码示例:

std::unique_ptr<Task> taskPtr5(new Task(55));

if(taskPtr5 != nullptr)
    std::cout<<"taskPtr5 is not empty"<<std::endl;

// 释放所有权
Task * ptr = taskPtr5.release();

if(taskPtr5 == nullptr)
    std::cout<<"taskPtr5 is empty"<<std::endl;

执行完上面的代码后,taskPtr5 变为空,并且其绑定的 raw 指针被赋值给 ptr。

完整示例

下面以一段完整的C++语言代码示例结束本文:

#include <iostream>
#include <memory>

struct Task
{
    int mId;
    Task(int id ) :mId(id)
    {
        std::cout<<"Task::Constructor"<<std::endl;
    }
    ~Task()
    {
        std::cout<<"Task::Destructor"<<std::endl;
    }
};

int main()
{
    std::unique_ptr<int> ptr1;
    if(!ptr1)
        std::cout<<"ptr1 is empty"<<std::endl;
    if(ptr1 == nullptr)
        std::cout<<"ptr1 is empty"<<std::endl;

    // 不能直接将 raw 指针赋值给 unique_ptr 对象
    // std::unique_ptr<Task> taskPtr2 = new Task(); // 非法

    std::unique_ptr<Task> taskPtr(new Task(23));
    if(taskPtr != nullptr)
        std::cout<<"taskPtr is  not empty"<<std::endl;
    // 通过 unique_ptr 直接访问成员变量
    std::cout<< taskPtr->mId <<std::endl;

    taskPtr.reset();
    std::cout<<"Reset the taskPtr"<<std::endl;
    if(taskPtr == nullptr)
        std::cout<<"taskPtr is empty"<<std::endl;

    std::unique_ptr<Task> taskPtr2(new Task(55));
    // unique_ptr 不可赋值,不可拷贝
    //taskPtr = taskPtr2; // 非法
    //std::unique_ptr<Task> taskPtr3 = taskPtr2; // 非法

    {
        // 转移所有权
        std::unique_ptr<Task> taskPtr4 = std::move(taskPtr2);
        if(taskPtr2 == nullptr)
            std::cout<<"taskPtr2 is  empty"<<std::endl;
        if(taskPtr4 != nullptr)
            std::cout<<"taskPtr4 is not empty"<<std::endl;
        // taskPtr4 作用域尾部,生命周期结束,
        // 将删除 taskPtr2 转移过来的 raw 指针
    }
    std::unique_ptr<Task> taskPtr5(new Task(55));
    // 释放所有权
    Task * ptr = taskPtr5.release();
    if(taskPtr5 == nullptr)
        std::cout<<"taskPtr5 is empty"<<std::endl;
    std::cout<<ptr->mId<<std::endl;
    // 此时需要手动删除用完的指针
    delete ptr;

    return 0;
}

同样的,编译时需要指定-std=c++11,最终输出如下,请看:

# g++ t2.cpp -std=c++11
# ./a.out 
ptr1 is empty
ptr1 is empty
Task::Constructor
taskPtr is  not empty
23
Task::Destructor
Reset the taskPtr
taskPtr is empty
Task::Constructor
taskPtr2 is  empty
taskPtr4 is not empty
Task::Destructor
Task::Constructor
taskPtr5 is empty
55
Task::Destructor

小结

本文主要讨论了C++11标准中的智能指针 unique_ptr 类的基本使用和一些相关注意事项。不过应该明白,文中的示例使用的deleter是 unique_ptr 的默认 deleter,也即delete方法,这样的局限性实际上很大,原因和解决方法可以参考:shared_ptr自定义deleter

阅读更多:   C++11智能指针 , C++ , 编程语言
添加新评论

icon_redface.gificon_idea.gificon_cool.gif2016kuk.gificon_mrgreen.gif2016shuai.gif2016tp.gif2016db.gif2016ch.gificon_razz.gif2016zj.gificon_sad.gificon_cry.gif2016zhh.gificon_question.gif2016jk.gif2016bs.gificon_lol.gif2016qiao.gificon_surprised.gif2016fendou.gif2016ll.gif