第23课 优先选用make系列函数
一. make系列函数
(一)三个make函数
1. std::make_shared:用于创建shared_ptr。GCC编译器中,其内部是通过调用std::allocate_shared来实现的。
2. std::make_unique:C++14中加入标准库。
3. std::allocate_shared:行为和std::make_shared一样,只不过第1个实参是个用以动态分配内存的分配器对象。
//make_unique的模拟实现
template<typename T, typename...Ts>
std::unique_ptr<T> make_unique(Ts&&...params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
} //make_shared的实现(GCC编译器)
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args)
{
typedef typename std::remove_const<_Tp>::type _Tp_nc;
return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
std::forward<_Args>(__args)...);
}
std::make_unique和std::make_shared的实现
(二)与new相比,make系列函数的优势
1. 避免代码冗余:创建智能指针时,被创建对象的类型只需写1次。如make_shared<T>(),而用new创建智能指针时,需要写2次。
2. 异常安全:make系列函数可编写异常安全代码,改进了new的异常安全性。
3. 提升性能:编译器有机会利用更简洁的数据结构产生更小更快的代码。使用make_shared<T>时会一次性进行内存分配,该内存单块(single chunck)既保存了T对象又保存与其相关联的控制块。而直接使用new表达式,除了为T分配一次内存,还要为与其关联的控制块再进行一次内存分配。
二. make系列函数的局限
(一)所有的make系列函数都不允许自定义删除器。
(二)make系列函数创建对象时,不能接受{}初始化列表。(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。因此无法将“{}”推导为initializer_list,具体见《完美转发》一课)。换言之,make系列只能将圆括号内的形参完美转发。
(三)自定义内存管理的类(如重载了operator new 和operator delete),不建议使用make_shared来创建。原因如下:
1. 重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块。
2. 而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小。
3. 因此,不建议使用make函数为那些重载了operator new和operator delete的类创建对象。
(四)对象的内存可能无法及时回收
1. make_shared 只分配一次内存,减少了内存分配的开销。使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。
2. 当强引用计数为0时,托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。
3.因此,当内存紧张且托管对象非常大时,如果weak_ptr的生命期比shared_ptr更长时,不建议使用make_shared。
【编程实验】make系列函数的优劣
#include <iostream>
#include <memory> //for smart pointer
#include <vector> using namespace std; class Widget
{
public:
Widget(){}
Widget(int x, int y){ cout << "Widget(int x, int y)" << endl; }
Widget(const std::initializer_list<int> li) { cout << "Widget(std::initializer_list<int> li)"<< endl; }
}; void processWidget(std::shared_ptr<Widget> spw, int priority){}
int computePriority() { /*throw 1;*/ return ; }//假设该函数会抛出异常 class ReallyBigType {};//大对象 int main()
{
//1. make系列函数的优势
//1.1 避免代码冗余,减少重复书写类型
auto upw1(std::make_unique<Widget>()); //使用make系列函数,Widget只需写一次
std::unique_ptr<Widget> upw2(new Widget); //使用new,Widget需写二次。 //1.2 make系统异常安全性更高
//在将实参传递processWidget前,各个参数时必须先被计算出来,假设顺序如下(因编译器和调用约定而异)
//A. 先new Widget,即一个Widget对象在堆上创建。
//B. 执行computePriority,但假设此时该函数产生异常,那上面的堆对象就会泄漏。
//C. 正常流程下,应执行shared_ptr构造函数,但由于第2步的异常,使得第1步分配的堆对象永远不会被这个
// shared_ptr接管(实际上该shared_ptr自己都没有机会创建),于是资源泄漏!
processWidget(shared_ptr<Widget>(new Widget), computePriority());//潜在资源泄漏! //异常安全!
processWidget(make_shared<Widget>(), computePriority()); //如果make_shared首先被调用当computePriority
//发生异常时,则之前的shared_ptr会被释放,
//从而释放Widget对象。如果computePriority先
//调用,则make_shared没有机会被调用,也就不会
//有资源泄漏!
//1.3 make_shared一次性分配内存
auto spw1 = std::make_shared<Widget>(); //一次性分配一个内存单块,可容纳Widget对象和控制块内存
std::shared_ptr<Widget> spw2(new Widget); //2次分配:new和分配控制块各一次。 //2. make系列函数的局限性
//2.1 make不能自定义删除器
auto widgetDeleter = [](Widget* pw) {delete pw; };
std::unique_ptr<Widget, decltype(widgetDeleter)> upw3(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw3(new Widget, widgetDeleter); //2.2 make系列函数不能接受{}初始化
auto upv = std::make_unique<std::vector<int>>(, ); //10个元素,每个都是20。而不是只有两个元素
auto spv = std::make_shared<std::vector<int>>(, ); //同上 auto pw1 = new Widget(, ); //使用圆括号,匹配Widget(int x, int y)
auto pw2 = new Widget{ , }; //使用大括号,匹配Widget(initializer_list)
delete pw1;
delete pw2; auto spw = std::make_shared<Widget>(, ); //使用圆括号,匹配Widget中非initializer_list形参的构造函数
//auto spw = std::make_shared<Widget>({10,20}); //error,make无法转发大括号初始化列表(原因见《完美转发》一课)
auto initList = { , }; //initList推导为initializer_list<int>
auto splst = std::make_shared<Widget>(initList); //ok,匹配Widget(const std::initializer_list<int> li) //2.3 对象的内存可能无法及时回收
auto pBigObj = std::make_shared<ReallyBigType>(); //通过make_shared创建大对象
//... //创建指向大对象的多个std::shared_ptr和std::weak_ptr,并使用这些智能指针来操作对象
//... //最后一个指向大对象的shard_ptr在此析构,但若干weak_ptr仍然存在
//... //此时,内存块只析构,还没回收。因为weak_ptr还共享着内存块中的控制块
//... //最后一个指向大对象的weak_ptr析构,内存块(托管对象+控制块)才被回收。由于weak_ptr的生命期比shared_ptr长,
//出现了内存块延迟回收的现象。 //使用new方法则不会出现上述现象
shared_ptr<ReallyBigType> pBigObj2(new ReallyBigType); //通过new,而不是make_shared创建
//... //同前,创建指向多个指向大对象的shared_ptr和weak_ptr。
//... //最后一个指向大对象的shard_ptr在此析构,但若干weak_ptr仍然存在。此时大对象的内存由于强引用为0,被回收。
//... //此阶段,仅控制块占用的内存处于未回收状态。
//... //最后一个指向该对象的weak_ptr析构,控制块被回收。 return ;
}
第23课 优先选用make系列函数的更多相关文章
- epoll 系列函数简介、与select、poll 的区别
一.epoll 系列函数简介 #include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags) ...
- php curl_multi系列函数实现多线程抓取网页
最近几天在做一个多搜索引擎关键字排名查询工具,用于及时方便的了解关键词在各大搜索引擎的排名. 在抓取360搜索的时候,发现360搜索每页只支持显示10个搜索结果,如果想获取100个搜索结果数据,就得搜 ...
- exec系列函数和system函数
一.exec替换进程映象 在进程的创建上Unix采用了一个独特的方法,它将进程创建与加载一个新进程映象分离.这样的好处是有更多的余地对两种操作进行管理.当我们创建 了一个进程之后,通常将子进程替换成新 ...
- 线程模型、pthread 系列函数 和 简单多线程服务器端程序
一.线程有3种模型,分别是N:1用户线程模型,1:1核心线程模型和N:M混合线程模型,posix thread属于1:1模型. (一).N:1用户线程模型 “线程实现”建立在“进程控制”机制之上,由用 ...
- POSIX 共享内存和 系列函数
在前面介绍了system v 共享内存的相关知识,现在来稍微看看posix 共享内存 和系列函数. 共享内存简单来说就是一块真正的物理内存区域,可以使用一些函数将这块区域映射到进程的地址空间进行读写, ...
- POSIX 消息队列 和 系列函数
一.在前面介绍了system v 消息队列的相关知识,现在来稍微看看posix 消息队列. posix消息队列的一个可能实现如下图: 其实消息队列就是一个可以让进程间交换数据的场所,而两个标准的消息队 ...
- 【洛谷日报#26】GCC自带位运算系列函数
文章转自 洛谷 谈到GCC的黑科技,大家想到的一定是这句: #pragma GCC optimize (3)//吸氧 抑或是这句: #pragma GCC diagnostic error " ...
- PHP进程通信基础——shmop 、sem系列函数使用
PHP进程通信基础--shmop .sem系列函数使用 进程通信的原理就是在系统中开辟出一个共享区域,不管是管道也好,还是共享内存,都是这个原理.如果心中有了这个概念,就会很方便去理解代码.由于官网上 ...
- PHP 使用 curl_* 系列函数和 curl_multi_* 系列函数进行多接口调用时的性能对比
在页面中调用的服务较多时,使用并行方式,即使用 curl_multi_* 系列函数耗时要小于 curl_* 系列函数. 测试环境 操作系统:Windows x64 Server:Apache PHP: ...
随机推荐
- 【MySQL】对数据库和表的增删改查
数据库的基本概念 数据库的英文单词: DataBase 简称 : DB 什么是数据库? 用于存储和管理数据的仓库. 数据库的特点: 持久化存储数据的.其实数据库就是一个文件系统 方便存储和管理数据 使 ...
- VS工具箱不显示DEV控件解决方法
VS工具箱中不显示DEV控件解决方法 之前先装vs,再装dev控件,vs工具栏中自动会加载并显示dev相关组件,但是,在更新vs(我用2017版)后,原先安装好的dev控件库不显示在vs的工具栏中了. ...
- csp 201809-1卖菜
问题描述 在一条街上有n个卖菜的商店,按1至n的顺序排成一排,这些商店都卖一种蔬菜. 第一天,每个商店都自己定了一个价格.店主们希望自己的菜价和其他商店的一致,第二天,每一家商店都会根据他自己和相邻商 ...
- docker操作命令大全和后台参数
一.命令行 可以通过运行 docker ,或者 docker help 命令得到命令行的帮助信息(我们以 CentOS 为操作环境为例): [root@iz2ze2bn5x2wqxdeq65wlpz ...
- i春秋四周年福利趴丨一纸证书教你赢在起跑线
i春秋四周年庆典狂欢已接近尾声 作为压轴福利 CISP-PTE认证和 CISAW-Web安全认证 迎来了史无前例的超低折扣 每个行业都有特定的精英证书,例如会计行业考取的是注册会计师证,建筑行业是一级 ...
- 实战讲解XXE漏洞的利用与防御策略
现在许多不同的客户端技术都可以使用XMl向业务应用程序发送消息,为了使应用程序使用自定义的XML消息,应用程序必须先去解析XML文档,并且检查XML格式是否正确.当解析器允许XML外部实体解析时,就会 ...
- maven 学习---Maven 编译打包时如何忽略测试用例
本文地址:http://blog.csdn.net/wirelessqa/article/details/14057305 跳过测试阶段: mvn package -DskipTests 临时性跳过测 ...
- NBIOT实现UDP协议的发送和接收(包含软件升级)
源码下载: nbiot_module程序(java netbean) -> 提取码 UdpServer程序(C# vs2010) -> 提取码 QQ:505645074 前提条件:开NB卡 ...
- Nginx配置实验反向代理
l 实验要求 浏览器访问 8083.mine.com:8081 地址,(Nginx端口是8081)通过Nginx服务器反向代理监听请求,将请求转发到tomcat服务器上,实现真正内容的访问. l ...
- JavaScript深入浅出第5课:Chrome是如何成功的?
摘要: Chrome改变世界. <JavaScript深入浅出>系列: JavaScript深入浅出第1课:箭头函数中的this究竟是什么鬼? JavaScript深入浅出第2课:函数是一 ...