7

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

 3 years ago
source link: https://blog.popkx.com/C-11-smart-pointer-unique_ptr-examples-and-basic-principle-analysis/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

本文将结合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++语言代码示例如下,请看:

if(!ptr)
    std::cout<<"ptr is empty"<<std::endl;
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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK