专栏首页编程珠玑为何优先选用unique_ptr而不是裸指针?

为何优先选用unique_ptr而不是裸指针?

在《拥抱智能指针,告别内存泄露》中说到了内存泄漏问题,也提到了C++中的智能指针基本原理,今天就来说说类模板unique_ptr。 在此之前,先回答读者的一个提问:C语言中该怎么办?有几点建议:

  • 编写时尽量遵循函数内申请,函数内释放的原则
  • 注意成对编写malloc和free
  • 使用静态扫描工具,如《pclint检查》
  • 使用内存检测工具,如valgrind

相关阅读《常见内存问题》。

unique_ptr

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

#include<memory>

基本使用

常见方式有:

std::unique_ptr<int> up;//可以指向int的unique_ptr,不过是空的
up = std::unique_ptr<int>(new int(12));

此时它是一个空的unique_ptr,即没有指向任何对象。

//unique_ptr<T>
std::unique_ptr<string> up1(new string("bianchengzhuji"));
std::unique_ptr<int[]> up2(new int[10]);//数组需要特别注意

也可以指向一个new出来的对象。

你也可以结合上面两种方式,如:

std::unique_ptr<int> up;//声明空的unique_ptr
int *p= new int(1111);
up.reset(p);//令up指向新的对象,p为内置指针

通常来说,在销毁对象的时候,都是使用delete来销毁,但是也可以使用指定的方式进行销毁。举个简单的例子,假如你打开了一个连接,获取到了一个文件描述符,现在你想通过unique_ptr来管理,希望在不需要的时候,能够借助unique_ptr帮忙关闭它。

//来源:公众号【编程珠玑】
#include<iostream>
#include<unistd.h>
#include<memory>
void myClose(int *fd)
{
    close(*fd);
}
int main()
{
    int socketFd = 10;//just for example
    std::unique_ptr<int,decltype(myClose)*> up(&socketFd,myClose);
    /*下面是另外两种写法,后面一种是使用lambda表达式*/
    //std::unique_ptr<int,void(*)(int*)> up(&socketFd,myClose);
    //std::unique_ptr<int,void(*)(int*)> ip(&socketFd,[](int *fd){close(*fd);});
    return 0;
}

它的用法如下:

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)*用于获取myClose函数的类型,*表明它是一个指针类型,即函数指针,它传入参数是int*。你也可以使用注释中的方式。关于函数指针,可参考《高级指针话题-函数指针》。

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

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

释放指向的对象

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

up = nullptr;//置为空,释放up指向的对象
up.release();//放弃控制权,返回裸指针,并将up置为空
up.reset();//释放up指向的对象

可以看到release和reset的区别在于,前者会释放控制权,返回裸指针,你还可以继续使用。而后者直接释放了指向对象。

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

需要特别注意的是,由于unique_ptr“独有”的特点,它不允许进行普通的拷贝或赋值,例如:

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独享对象,但是也可以移动,即转移控制权。如:

std::unique_ptr<int> up1(new int(42));
std::unique_ptr<int> up2(up1.release());

up2接受up1 release之后的指针,或者:

std::unique_ptr<int> up1(new int(42));
std::unique_ptr<int> up2;
up2.reset(up1.release());

或者使用move:

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

在函数中的使用

还记得在《传值和传指针有什么区别?》讲的吗?既然unique_ptr独享对象,那么就无法直接作为参数,应该怎么办呢?

作为参数

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

//来源:公众号【编程珠玑】
#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函数就可以获取裸指针,如:

//来源:公众号【编程珠玑】
#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;
}

或者使用引用作为参数:

//来源:公众号【编程珠玑】
#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函数:

//来源:公众号【编程珠玑】
#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可以作为参数返回:

//来源:公众号【编程珠玑】
#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。

  • 避免内存泄露
  • 避免更大开销

第一点相信很好理解,自动管理,不需要时即释放,甚至可以防止下面这样的情况:

int * p = new int(1111);
/*do something*/
delete p;

如果在do something的时候,出现了异常,退出了,那delete就永远没有执行的机会,就会造成内存泄露,而如果使用unique_ptr就不会有这样的困扰了。

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

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

总结

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

本文分享自微信公众号 - 编程珠玑(shouwangxiansheng),作者:守望先生

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 不可不知的三种缓冲类型

    为什么有时候写入文件的内容却没有?没什么printf打印在终端的内容看不到?这一切背后有着怎样早为人知的秘密?

    编程珠玑
  • C语言入坑指南-整型的隐式转换与溢出

    我们知道整型有无符号数和有符号数之分。如果我们对无符号数和有符号数处理不当,就可能造成难以预测的结果,尤其是在作为循环条件的时候,可能导致死循环。整型之间的运算...

    编程珠玑
  • 善用shared_ptr,远离内存泄漏(文末福利)

    《为何优先选用unique_ptr而不是裸指针?》中说到,如果有可能就使用unique_ptr,然后很多时候对象是需要共享的,因此shared_ptr也就会用得...

    编程珠玑
  • android-Ultra-Pull-To-Refresh基础教程

    * 6个参数可配置: * 阻尼系数:默认1.7f,越大,感觉下拉时越吃力。 * 触发刷新时移动的位置比例:默认,1.2f,移动达到头部高度1.2倍时可触发刷...

    用户4458175
  • leveldb uint32压缩方法

    leveldb 采用 protocalbuffer 里使用的变长整形编码方案,以节省空间;

    awk
  • STL四种智能指针

    STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr,auto_ptr是C++98提供的解决方案,...

    Dabelv
  • 通俗易懂学习C++智能指针

    智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

    海盗船长
  • printf函数的求值顺序问题

    printf的参数,函数printf从左往右读取,然后将先读取放到栈底,最后读取的放在栈顶,处理时候是从栈顶开始的,所有从右边开始处理的。

    ccf19881030
  • 网络杂谈——聊聊NDS解析

          在浏览器中输入一个地址,点击回车之后发生了什么?这是一个面试中常见的问题 ,这个看似常见简单的操作,其中却隐藏了大量复杂的互联网技术。本篇博客,我们...

    珲少
  • C++避坑指南

    ? 导语:如果,将编程语言比作武功秘籍,C++无异于《九阴真经》。《九阴真经》威力强大、博大精深,经中所载内功、轻功、拳、掌、腿、刀法、剑法、杖法、鞭法、指爪...

    腾讯技术工程官方号

扫码关注云+社区

领取腾讯云代金券