cpp - unique_ptr详解

转自守望的个人博客

unique_ptr

  • 一个unique_ptr独享它指向的对象。
  • 也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
  • 使用它需要包含下面的头文件:
    1
    #include<memory>

基本使用

  • 常见方式有:
    1
    2
    3
    //可以指向int的unique_ptr,不过是空的
    std::unique_ptr<int> up;
    up = std::unique_ptr<int>(new int(22));
1
2
3
// 也可以指向一个new出来的对象。
std::unique_ptr<string> up1(new string("xd"));
std::unique_ptr<int[]> up2(new int[10]);//数组需要特别注意
  • 你也可以结合上面两种方式,如:

    1
    2
    3
    std::unique_ptr<int> up;//声明空的unique_ptr
    int *p= new int(1111);
    up.reset(p);//令up指向新的对象,p为内置指针
  • 通常来说,在销毁对象的时候,都是使用delete来销毁,但是也可以使用指定的方式进行销毁。举个简单的例子,假如你打开了一个连接,获取到了一个文件描述符,现在你想通过unique_ptr来管理,希望在不需要的时候,能够借助unique_ptr帮忙关闭它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include<iostream>
    #include<memory>

    void closeSocket(int* fd)
    {
    std::cout << "mock close socket!" << std::endl;
    delete fd;
    }

    int* newSocket()
    {
    return new(std::nothrow) int(21);
    }

    int main()
    {
    int* socket = newSocket();//just for example
    std::unique_ptr<int, decltype(closeSocket)*> up(socket, closeSocket);
    /*下面是另外两种写法,后面一种是使用lambda表达式*/
    //std::unique_ptr<int,void(*)(int*)> up(&socketFd,myClose);
    //std::unique_ptr<int,void(*)(int*)> ip(&socketFd,[](int *fd){close(*fd);});
    return 0;
    }
  • 它的用法如下:

    1
    2
    std::unique_ptr<T,D> up(t,d);
    std::unique_ptr<T,D> up(d);//空的unique_ptr
  • 含义分别如下:

    • T unique_ptr管理的对象类型
    • D 删除器类型
    • t unique_ptr管理的对象
    • d 删除器函数/function对象等,用于释放对象指针
  • 这里使用了decltype(myClose)用于获取closeSocket函数的类型,表明它是一个指针类型,即函数指针,它传入参数是int*。你也可以使用注释中的方式。

  • 即便后面执行出现异常时,这个socket连接也能够正确关闭。

  • 后面我们也可以看到,与shared_ptr不同,unique_ptr在编译时绑定删除器,避免了运行时开销。

释放指向的对象

  • 一般来说,unique_ptr被销毁时(如离开作用域),对象也就自动释放了,也可以通过其他方式下显示释放对象。如:

    1
    2
    3
    up = nullptr;//置为空,释放up指向的对象
    up.release();//放弃控制权,返回裸指针,并将up置为空
    up.reset();//释放up指向的对象
  • 可以看到release和reset的区别在于,前者会释放控制权,返回裸指针,你还可以继续使用。而后者直接释放了指向对象。

unique_ptr不支持普通的拷贝和赋值

  • 需要特别注意的是,由于unique_ptr“独有”的特点,它不允许进行普通的拷贝或赋值,例如:
    1
    2
    3
    4
    std::unique_ptr<int> up0;
    std::unique_ptr<int> up1(new int(1111));
    up0 = up1 //错误,不可赋值
    std::unique_ptr<int> up2(up1);//错误,不支持拷贝
  • 总之记住,既然unique_ptr是独享对象,那么任何可能被共享的操作都是不允许的,但是可以移动。

移动unique_ptr的对象

  • 虽然unique_ptr独享对象,但是也可以移动,即转移控制权。如:

    1
    2
    std::unique_ptr<int> up1(new int(42));
    std::unique_ptr<int> up2(up1.release());
  • up2接受up1 release之后的指针,或者:

    1
    2
    3
    std::unique_ptr<int> up1(new int(42));
    std::unique_ptr<int> up2;
    up2.reset(up1.release());
  • 或者使用move:

    1
    2
    std::unique_ptr<int> up1(new int(42));
    std::unique_ptr<int> up2(std::move(up1));

在函数中的使用

  • 既然unique_ptr独享对象,那么就无法直接作为参数,应该怎么办呢?

作为参数

  • 如果函数以unique_ptr作为参数呢?如果像下面这样直接把unique_ptr作为参数肯定就报错了,因为它不允许被复制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include<iostream>
    #include<memory>

    void test(std::unique_ptr<int> p)
    {
    *p = 10;
    }

    int main()
    {
    std::unique_ptr<int> up(new int(42));
    test(up);//试图传入unique_ptr,编译报错
    std::cout<<*up<<std::endl;
    return 0;
    }
  • 上面的代码编译将直接报错。

  • 当然我们可以向函数中传递普通指针,使用get函数就可以获取,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include<iostream>
    #include<memory>

    void test(int *p)
    {
    *p = 10;
    }

    int main()
    {
    std::unique_ptr<int> up(new int(42));
    test(up.get());//传入裸指针作为参数
    std::cout<<*up<<std::endl;//输出10
    return 0;
    }
  • 或者使用引用作为参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include<iostream>
    #include<memory>

    void test(std::unique_ptr<int> &p)
    {
    *p = 10;
    }

    int main()
    {
    std::unique_ptr<int> up(new int(42));
    test(up);
    std::cout<<*up<<std::endl;//输出10
    return 0;
    }
  • 当然如果外部不再需要使用了,那么你完全可以转移,将对象交给你调用的函数管理,这里可以使用move函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include<iostream>
    #include<memory>
    void test(std::unique_ptr<int> p)
    {
    *p = 10;
    }
    int main()
    {
    std::unique_ptr<int> up(new int(42));
    test(std::unique_ptr<int>(up.release()));
    //test(std::move(up));//这种方式也可以
    return 0;
    }

作为返回值

  • unique_ptr可以作为参数返回:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include<iostream>
    #include<memory>

    std::unique_ptr<int> test(int i)
    {
    return std::unique_ptr<int>(new int(i));
    }

    int main()
    {
    std::unique_ptr<int> up = test(10);
    //std::shared_ptr<int> up = test(10);
    std::cout<<*up<<std::endl;
    return 0;
    }
  • 你还可以把unique_ptr转换为shared_ptr使用,如注释行所示。

为什么优先选用unique_ptr

  • 回到标题的问题,问什么优先选用unique_ptr。

    • 避免内存泄露
    • 避免更大开销
  • 第一点相信很好理解,自动管理,不需要时即释放,甚至可以防止下面这样的情况:

    1
    2
    3
    int * p = new int(1111);
    /*do something*/
    delete p;
  • 如果在do something的时候,出现了异常,退出了,那delete就永远没有执行的机会,就会造成内存泄露,而如果使用unique_ptr就不会有这样的困扰了。

  • 第二点为何这么说?因为相比于shared_ptr,它的开销更小,甚至可以说和裸指针相当,它不需要维护引用计数的原子操作等等。

  • 所以说,如果有可能,优先选用unique_ptr。

总结

  • 本文介绍了uniqueptr的基本使用情况和使用场景,它能够有效地避免内存泄露并且效率可控,因此如果能够满足需求,则优先选择unique\ptr。