c++11 条款21:尽量使用std::make_unique和std::make_shared而不直接使用new

2023-05-16

条款21:尽量使用std::make_unique和std::make_shared而不直接使用new


        让我们从对齐std::make_unique 和 std::make_shared这两块开始。std::make_shared是c++11的一部分,但很可惜std::make_unique不是。它是在c++14里加入标准库的。假如你在使用c++11,也别担心,你很容易写出一个基本的版本。看这里:

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_unique完美传递了参数给对象的构造函数,从一个原始指针构造出一个std::unique_ptr,返回创建的std::unique_ptr。这个形式的函数不支持数组和定制删除器(见条款18),但它证明了一点点的努力就可以根据需要创建一个make_unique。要记住的是不要把你的版本放到std命名空间里,因为你不想当升级到c++14后会和库提供的标准实现冲突吧。

        std::make_unique 和 std::make_shared是三个make函数中的两个,make函数用来把一个任意参数的集合完美转移给一个构造函数从而生成动态分配内存的对象,并返回一个指向那个对象的灵巧指针。第三个make是std::allocate_shared。它像std::make_shared一样,除了第一个参数是一个分配器对象,用来进行动态内存分配。

        优先使用make函数的第一个原因即使用最简单的构造灵巧指针也能看出来。考虑如下代码:

auto upw1(std::make_unique<Widget>());     // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func


auto spw1(std::make_shared<Widget>());     // with make func
std::shared_ptr<Widget> spw2(new Widget); // without make func

        我标注了基本的区别:

        使用new的版本重复了被创建对象的键入,但是make函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀,最终产生更难以维护的代码,通常会引起代码不一致,而不一致经常导致bug产生。另外,输入两次比输入一次要费力些,谁都想减少敲键盘的负担。

        优先使用make函数的第二个原因是和异常安全有关。假设我们有个函数来根据一些优先级处理一个Widget对象:

void processWidget(std::shared_ptr<Widget> spw, int priority);

        对std::shared_ptr进行传值看上去有些疑问,但是条款41解释了如果processWidget始终构造一个std::shared_ptr的拷贝(比如保存在一个数据结构里,该数据结构跟踪已经被处理过的Widget对象),这可能是个合理的选择。

        现在我们假设有个函数来计算相对优先级

int computePriority();

        我们在一个调用processWidget时使用new而不使用std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget), // potential
                         computePriority());       // resource
                                                   // leak!

        就像注释说明的,这段代码可能会因为new而引起内存泄漏。但怎么引起的呢?调用代码和被调函数都是在使用std::shared_ptr,这本来就是设计成避免内存泄漏的。它可以保证在对象不被使用时销毁所指的对象。只要一直都在使用std::shared_ptr,怎么会泄漏呢?

        答案是和编译器翻译代码到目标代码有关。在运行期,传递给函数的参数必须先计算,然后才发生函数调用。因此在调用processWidget时,下面的事情必须在processWidget开始执行前发生:

1.表达式"new Widget"必须先计算,一个Widget对象必须先创建在堆上;

2.负责new出来的对象指针的std::shared_ptr<Widget>的构造函数必须执行;

3.computePriority必须运行。


        没有人要求编译器必须按这样的顺序来生成代码。“new Widget”必须在 std::shared_ptr构造函数前被调用,因为new的结果用作构造函数的参数,但是computePriority可以在上述调用前、后或者中间执行。也就是说编译器可能会产生这样的代码来按如下顺序执行:

1.执行“new Widget”;

2.执行computePriority;

3.调用std::shared_ptr构造函数。

        假如这样的代码生成,在运行期,computePriority产生了异常,那么第一步生成的对象会被泄漏掉,因为没有在第3步被保存到 std::shared_ptr。

        使用std::make_shared可以避免这个问题。调用代码如下:

processWidget(std::make_shared<Widget>(),       // no potential
                         computePriority());    // resource leak

        在运行期,std::make_shared或者computePriority被首先调用。如果是std::make_shared被首先调用,指向动态内存对象的原始指针会被安全的保存在返回的std::shared_ptr对象中,然后是computePriority被调用 。如果computePriority产生了异常,那std::shared_ptr析构会知道于是它所拥有的对象会被销毁。如果computePriority先调用并产生了异常,std::make_shared不会被调用,因此也不会有动态分配的内存担心。

        假如我们把std::shared_ptr和std::make_shared替换成std::unique_ptr 和std::make_unique,会发生相同的事情。使用std::make_unique来代替new在写异常安全的代码里是和使用std::make_shared一样重要。

        另一个使用std::make_shared的优势(和直接使用new相比)是会提升性能。使用std::make_shared会让编译器产生更小更快的代码,从而产生更简洁的数据结构。考虑如下直接使用new的代码:

std::shared_ptr<Widget> spw(new Widget);

        很显然这段代码会引起一个内存分配,但实际上是有两次内存分配。条款19解释了每个std::shared_ptr会指向一个控制块,这个控制块除了其他一些东西,包含了所指对象的引用计数。控制块的内存分配是std::shared_ptr的构造函数汇总进行的。这样直接用new需要为Widget来分配一次内存,还要为控制块再分配一次内存。

        假如用std::make_shared来代替new,

auto spw = std::make_shared<Widget>();

一次内存分配就足够了。那是因为std::make_shared会分配一块独立的内存既保存Widget对象又保存控制块。这个优化减小了程序的静态尺寸,因为代码只包含一次内存分配的调用,同时增加了代码执行速度,因为只有一次内存分配。另外,使用std::make_shared会避免控制块中的一些记录信息,潜在的减少了程序中内存的使用。

        对std::make_shared的性能分析同样适用于std::allocated_shared,因此std::make_shared的性能优势也同样存在于std::allocated_shared。

        make函数的参数相对直接使用new来说也更健壮。尽管有如此多的工程特性、异常安全以及效率优势,我们这个条款是“尽量”使用make函数,而没有说排除其他情况。那是因为还有情况不能或者不应该使用make函数。

        比如,make函数都不允许使用定制删除器(见条款18,条款19),但是std::unique_ptr和std::shared_ptr的构造函数都可以给Widget对象一个定制删除器。

auto widgetDeleter = [](Widget* pw) { … };

        直接使用new来构造一个有定制删除器的灵巧指针:

std::unique_ptr<Widget, decltype(widgetDeleter)>
    upw(new Widget, widgetDeleter);

std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

用make函数没法做到这一点。

        make函数的第二个限制是无法从实现中获得句法细节。条款7解释了当创建一个对象时,如果其类型通过std::initializer_list参数列表来重载构造函数的,尽量用大括号来创建对象而不是std::initializer_list构造函数。相反,用圆括号创建对象时,会调用non-std::initializer_list构造函数。make函数完美传递了参数列表到对象的构造函数,但它们在使用圆括号或大括号时,也是如此吗?对某些类型来说,这个问题的答案有很大不同。比如:

auto upv = std::make_unique<std::vector<int>>(10, 20);

auto spv = std::make_shared<std::vector<int>>(10, 20);

结果指针是指向一个10个元素的数组每个元素值是20,还是指向2个元素的数组其值分别是10和20 ?或者无限制?


        好消息是并非无限制的 :两个调用都是构造了10元素的数组,每个元素值都是20。说明在make函数里,转移参数的代码使用了圆括号,而不是大括号。坏消息是,假如你想使用大括号初始化器( braced initializer)来创建自己的指向对象的指针,你必须直接使用new。使用make函数需要能够完美传递一个大括号初始化器的能力,但是,如条款30中所说的,大括号初始化器不能够完美传递。但条款30也给出了一个补救方案:从大括号初始化器根据auto类型推导来创建一个 std::initializer_list对象,然后把auto对象传递给make函数:

// create std::initializer_list
auto initList = { 10, 20 };

// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);

  

        对于std::unique_ptr来说,其make函数就只在这两种场景(定制删除器和大括号初始化器)有问题。对于std::shared_pr来说,其make函数的问题会更多一些。这两种都是边缘情况,但是一些开发者就喜欢处理边缘情况,你也许也是其中之一。

        一些类会定义自己的opeator new和operator delete。这表示全局的内存分配和释放函数对该对象不合适。通常情况下,类特定的这两个函数被设计成精确的分配或释放类大小的内存块,比如,类Widget的operator new和operator delete仅仅处理sizeof(Widget)大小的内存块。这两个函数作为定制的分配器(通过std::allocate_shared)和解析器(通过定制解析器),对std::shared_ptr的支持并不是很好的选择。因为std::allocate_shared需要的内存数量并不是动态分配的对象的大小,而是对象的大小加上控制块的大小。因此,对于某些对象,其类有特定的operate new和operator delete,使用make函数去创建并不是很好的选择。

        std::make_shared在尺寸和速度上的优点同直接使用new相比,阻止了std::shared_ptr的控制块作为管理对象在同样的内存块上分配。当对象的引用计数变为0,对象被销毁(析构函数被调)。然而,直到控制块同样也被销毁,它所拥有的内存才被释放,因为两者都在同一块动态分配的内存上。

        我前面提到过,控制块除了引用计数本身还包含了其他一些信息。引用计数记录了有多少std::shared_ptr指针指向控制块。另外控制块中还包含了第二个引用计数,记录了有多少个std::weak_ptr指针指向控制块。这第二个引用计数被称作weak count。当一个std::weak_ptr检查是否过期时(见条款19),它会检查控制块里的引用计数(并不是weak count)。假如引用计数为0(假如被指对象没有std::shared_ptr指向了从而已经被销毁),则过期,否则就没过期。

        只要有std::weak_ptr指向一个控制块(weak count大于0),那控制块就一定存在。只要控制块存在,包含它的内存必定存在。这样通过std::shared_ptr的make函数分配的函数则在最后一个std::shared_ptr和最后一个std::weak_ptr被销毁前不能被释放。

        假如对象类型很大,以至于最后一个std::shared_ptr和最后一个std::weak_ptr的销毁之间的时间不能忽略时,对象的销毁和内存的释放间会有个延迟发生。

class ReallyBigType { … };


auto pBigObj =  // create very large

     std::make_shared<ReallyBigType>(); // object via
                                        //  std::make_shared


… // create std::shared_ptrs and std::weak_ptrs to

// large object, use them to work with it

… // final std::shared_ptr to object destroyed here,

// but std::weak_ptrs to it remain

… // during this period, memory formerly occupied

// by large object remains allocated

… // final std::weak_ptr to object destroyed here;

// memory for control block and object is released

        当直接使用new时,ReallyBigType对象的内存可以在最后一个std::shared_ptr销毁时被释放:

class ReallyBigType { … };       // as before


std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);

                                                   // create very large
                                                   // object via new

…    // as before, create std::shared_ptrs and
     // std::weak_ptrs to object, use them with it

…    // final std::shared_ptr to object destroyed here,
     // but std::weak_ptrs to it remain;
     // memory for object is deallocated

…    // during this period, only memory for the
     // control block remains allocated


…    // final std::weak_ptr to object destroyed here;
     // memory for control block is released

        你有没有发现,你处在一个不可能或者不适合用std::make_shared的情况下,你会确保避免之前我们见到的这类异常安全问题。最好的办法是确保你直接用new的时候,立即把new的结果传递给一个灵巧指针的构造函数,别的什么先不做。这样会阻止编译器生成代码,避免在new和灵巧指针的构造函数(会接管new出来的对象)直接产生异常。

举个例子,考虑一个对processWidget函数(我们之前测试过)的非异常安全的调用,这次我们定义一个定制删除器:

void processWidget(std::shared_ptr<Widget> spw, // as before
                                                int priority);

void cusDel(Widget *ptr);      // custom
                               // deleter

        这里有个非异常安全的调用:


processWidget(                                           // as before,
            std::shared_ptr<Widget>(new Widget, cusDel), // potential
            computePriority()                            // resource
);                                                       // leak!

         回忆下:假如computePriority函数在new Widget之后,但是在std::shared_ptr的构造函数之前被调用,如果computePriority抛了异常,那么动态分配的Widget会被泄露。

        这里因为使用了定制删除器,所以不能使用std::make_shared,这里避免问题的方法是把Widget分配内存和构造std::shared_ptr放置到自己的语句中,然后再用std::shared_ptr去调用processWidget。这是这个技巧的本质,当然我们后面会看到我们可以提升其性能:

std::shared_ptr<Widget> spw(new Widget, cusDel);


processWidget(spw, computePriority()); // correct, but not
                                       // optimal; see below

        因为std::shared_ptr拥有从构造函数传递给它的原始指针,即使在构造函数产生异常时,所以上述代码运行正常。在这个例子中,如果spw的构造函数抛异常(比如因为不能够为控制块分配到动态内存),它仍然会保证调用cusDel去析构new Widget返回的结果。

        不同之处在于,我们在非异常安全的代码里给processWidget传递了一个右值。

processWidget(
    std::shared_ptr<Widget>(new Widget, cusDel), // arg is rvalue
     computePriority()
);

        而在异常安全的调用中,我们传递了一个左值

processWidget(spw, computePriority()); // arg is lvalue

        因为processWidget的std::shared_ptr参数是通过传值的,从一个右值去构造仅仅需要一个move,而从左值去构造需要一个拷贝。对std::shared_ptr来说,这个区别很重要,因为拷贝一个std::shared_ptr需要对其引用计数进行加1的原子操作,而移动一个std::shared_ptr根本不需要对引用计数进行操作。对于这段异常安全的代码如果要达到非异常安全的代码的性能,我们在spw上应用std::move,而把它转化成一个右值(见条款23):

processWidget(std::move(spw),               // both efficient and
                        computePriority()); // exception safe

        这个很有趣,也应该知道。但是同时也无关紧要。因为你应该很少有理由不直接使用make函数。除非你有特别的理由不去用它,否则你应该使用make函数来完成你要做的。

                                                   

需要记住的事情:

1.同直接使用new相比,make函数减小了代码重复,提高了异常安全,并且对于std::make_shared和std::allcoated_shared,生成的代码会更小更快。

2.不能使用make函数的情况包括我们需要定制删除器和期望直接传递大括号初始化器。

3.对于std::shared_ptr,额外的不建议使用make函数的情况包括:

  (1)定制内存管理的类,

  (2)关注内存的系统,非常大的对象,以及生存期比 std::shared_ptr长的std::weak_ptr。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c++11 条款21:尽量使用std::make_unique和std::make_shared而不直接使用new 的相关文章

  • python学习笔记(7)数据类型转换

    转载于 https www cnblogs com wuzm p 11533108 html
  • 性能实战分析-问题分析(三)

    问题四 xff1a 数据库连接池不释放 搭e6mall需要使用tomcat7搭建 过程 xff1a 压测一个商品的详情页请求 xff0c 看看报错如何 xff1f 按照上面方法分析 1 先访问tomcat的初始页面 xff0c 可以访问 x
  • 解决Navicat无法连接到MySQL的问题

    解决Navicat无法连接到MySQL的问题 问题一 xff1a 本地IP xff08 xxx xxx xxx xxx xff09 没有访问远程数据库的权限 于是下面开启本地IP xff08 xxx xxx xxx xxx xff09 对远
  • Linux下用于查看系统当前登录用户信息的4种方法

    https www cnblogs com weijiangbao p 7868965 html 转载于 https www cnblogs com wuzm p 11377948 html
  • python学习

    https www cnblogs com dinghanhua tag python default html page 61 2 转载于 https www cnblogs com wuzm p 11381519 html
  • python学习笔记(8)迭代器和生成器

    迭代器 迭代是Python最强大的功能之一 xff0c 是访问集合元素的一种方式 迭代器是一个可以记住遍历的位置的对象 迭代器对象从集合的第一个元素开始访问 xff0c 直到所有的元素被访问完结束 迭代器只能往前不会后退 迭代器有两个基本的
  • 基于立体视觉和GPU加速的视觉里程系统(VINS)

    注意 xff1a 本文只适用于 Kerloud SLAM Indoor无人机产品 Kerloud SLAM Indoor配备有Nvidia TX2模块和Intel Realsense D435i立体摄像头 凭借更强大的GPU内核 xff0c
  • python学习笔记(9)函数(一)

    定义一个函数 你可以定义一个由自己想要功能的函数 xff0c 以下是简单的规则 xff1a 函数代码块以 def 关键词开头 xff0c 后接函数标识符名称和圆括号 任何传入参数和自变量必须放在圆括号中间 xff0c 圆括号之间可以用于定义
  • python学习笔记(10)函数(二)

    xff08 函数的参数 amp 递归函数 xff09 一 函数的参数 Python的函数定义非常简单 xff0c 但灵活度却非常大 除了正常定义的必选参数外 xff0c 还可以使用默认参数 可变参数和关键字参数 xff0c 使得函数定义出来
  • python学习笔记(2)数据类型-字符串

    字符串是 Python 中最常用的数据类型 我们可以使用引号 39 或 34 来创建字符串 创建字符串很简单 xff0c 只要为变量分配一个值即可 例如 xff1a var1 61 39 Hello World 39 var2 61 34
  • python学习笔记(11)文件操作

    一 读文件 读写文件是最常见的IO操作 Python内置了读写文件的函数 xff0c 用法和C是兼容的 读写文件前 xff0c 我们先必须了解一下 xff0c 在磁盘上读写文件的功能都是由操作系统提供的 xff0c 现代操作系统不允许普通的
  • 作业2

    作业2 xff1a 写一个随机产生138开头手机号的程序 1 输入一个数量 xff0c 产生xx条手机号 prefix 61 39 138 39 2 产生的这些手机号不能重复 转载于 https www cnblogs com wuzm p
  • mysql索引详细介绍

    博客 xff1a https blog csdn net tongdanping article details 79878302 E4 B8 89 E3 80 81 E7 B4 A2 E5 BC 95 E7 9A 84 E5 88 86
  • 作业1

    作业一 xff1a 写一个登录的程序 xff0c 1 最多登陆失败3次 2 登录成功 xff0c 提示欢迎xx登录 xff0c 今天的日期是xxx xff0c 程序结束 3 要检验输入是否为空 账号和密码不能为空 4 账号不区分大小写 im
  • 常用的SQL优化

    转自 xff1a https www cnblogs com Cheney222 articles 5876382 html 一 优化 SQL 语句的一般步骤 1 通过 show status 命令了解各种 SQL 的执行频率 MySQL
  • B+tree

    https www cnblogs com nullzx p 8729425 html 简介 xff1a 本文主要介绍了B树和B 43 树的插入 删除操作 写这篇博客的目的是发现没有相关博客以举例的方式详细介绍B 43 树的相关操作 xff
  • Mysql监控调优

    一 Mysql性能介绍 1 什么是Mysql xff1f 它有什么优点 xff1f MySQL是一个关系型数据库管理系统 xff0c 由瑞典MySQL AB公司开发 xff0c 目前属于Oracle公司 MySQL是一种关联数据库管理系统
  • [云讷科技] Kerloud PX4飞控的EKF2程序导航

    一 介绍 EKF拓展卡尔曼滤波器是px4开源飞控框架采用的核心状态估计方法 xff0c EKF2是px4飞控中的对应的软件模块 xff0c 可以支持各类传感器信号 xff0c 包括IMU xff0c 磁感计 xff0c 激光测距仪 xff0
  • 第5.4节 Python函数中的变量及作用域

    一 函数中的变量使用规则 函数执行时 xff0c 使用的全局空间是调用方的全局空间 xff0c 参数及函数使用的局部变量存储在函数单独的局部名字空间内 xff1b 函数的形参在函数中修改了值时 xff0c 并不影响调用方本身的数据 xff0
  • PX4 IO [14] serial [转载]

    PX4 IO 14 serial PX4 IO 14 serial 转载请注明出处 更多笔记请访问我的博客 xff1a merafour blog 163 com 2014

随机推荐

  • 《Windows核心编程》第3章——深入理解handle

    本文借助windbg来理解程序中的函数如何使用handle对句柄表进行查询的 所以先要开启Win7下Windbg的内和调试功能 解决win7下内核调试的问题 win7下debug默认无法进行内核调试 xff08 xff01 process等
  • CentOS7中firewalld的安装与使用详解

    一 软件环境 root 64 Geeklp201 cat etc redhat release CentOS Linux release 7 4 1708 Core 二 安装firewalld 1 firewalld提供了支持网络 防火墙区
  • IMU数据融合:互补,卡尔曼和Mahony滤波

    编写者 xff1a 龙诗科 邮箱 xff1a longshike2010 64 163 com 2016 06 29 本篇博客主要是参照国外的一篇文章来整理写的 xff0c 自己觉得写的非常好 xff0c 以此整理作为以后的学习和参考 国外
  • ogeek线下赛web分析1-python-web

    1 python from flask import Flask request render template send from directory make response from Archives import Archives
  • java学习杂记-更新

    1 maven添加下载依赖jar文件 xff1a maven官方仓库 xff0c 需要哪个jar文件直接找到对应的依赖标签 https search maven org 直接将 lt dependency gt 放到pom xml文件中 x
  • javascript/Jquery 将字符串转换成变量名

    var a 61 39 a 39 39 b 39 39 c 39 var obj 61 for i 61 0 i lt a length i 43 43 obj a i 61 34 abc 34 43 1 alert obj a alert
  • Navicat 看历史执行SQL

    Navicat可以通过这个框口看手动操作所执行的代码操作 转载于 https www cnblogs com sunxun p 5286657 html
  • MWC四轴飞行器代码解读

    MWC v2 2 代码解读annexCode 红色是一些暂时没去顾及的部分 xff0c 与我现在关心的地方并无太大关系 函数对rcDate进行处理 xff08 去除死区 xff0c 根据油门曲线 xff0c roll pitch曲线 xff
  • 云讷科技推出Kerloud数传电台

    介绍 Kerloud Telemetry是由云讷科技推出的一款面向无人系统 传输可靠的短距离无线传输电台 产品基于ISM Sub G频段 xff0c 采用FSK调制 抗干扰能力强 xff0c 具备Uart USB通用接口 xff0c 支持P
  • Requests方法 --- post 请求body的四种类型

    常见的 post 提交数据类型有四种 xff1a 1 第一种 xff1a application json xff1a 这是最常见的 json 格式 xff0c 也是非常友好的深受小伙伴喜欢 的一种 xff0c 如下 34 input1 3
  • 中文转拼音 (utf8版,gbk转utf8也可用)

    中文转拼音 utf8版 gbk转utf8也可用 https git oschina net cik pinyin php 64 param string str utf8字符串 64 param string ret format 返回格式
  • Pycharm激活方法(license server方法)

    strong pycharm所有版本 span class hljs string http span class hljs comment www jetbrains com pycharm download previous html
  • idc函数大全

    A80 addc A80 addcix A80 addciy A80 addix A80 addiy A80 cmpd A80 cmpi A80 im0 A80 im1 A80 im2 A80 jrc A80 jrnc A80 jrnz A
  • 视觉SLAM漫淡

    视觉SLAM漫谈 1 前言 开始做SLAM xff08 机器人同时定位与建图 xff09 研究已经近一年了 从一年级开始对这个方向产生兴趣 xff0c 到现在为止 xff0c 也算是对这个领域有了大致的了解 然而越了解 xff0c 越觉得这
  • 无人机基础知识点总结

    一 xff0e 基本概念 飞控 xff1a 飞机的控制系统 xff0c 其中硬件包含传感器部分IMU和控制部分的MCU xff0c 软件部分包含控制算法 俯仰 xff1a pitch xff0c 绕坐标系y轴旋转 xff0c 想象一下平时的
  • 谈一谈编程中遇到的一些死循环(递归死循环,AOP死循环,业务死循环)

    最简单最基础死循环 xff0c 一般都是这样的 while 1 while true for 然而在编程中常常会用到一些并不是那么基础的死循环 xff0c 这里列举一些我在编程中所遇到的一些死循环 方法已经不记得了 xff0c 只是大概说明
  • 简历上的项目经历怎么写?这3条原则不可忽视!

    阅读本文大概需要 5 分钟 作者 xff1a 黄小斜 文章来源 xff1a 微信公众号 程序员江湖 作为一个程序员 xff0c 想必大家曾经都做过一些项目 xff0c 可能现在手头上也还有一些项目 不过还是有很多学生朋友来问我 没有项目怎么
  • “四通一达”本一家,这家人是如何“承包”中国快递半壁江山的?

    微博上有张图火到不行了 看明白没 xff1f 原来 xff0c 这些年为我们奔走送快递的申通 中通 圆通 韵达 xff0c 这四家公司 xff0c 以及汇通 天天等快递公司 xff0c 都有一个共同的老家 xff1a 仅有40万人口的浙江桐
  • 在远方

    远方不是脚到达的地方 xff0c 而是心超越的地方 剧情简介 姚远在孤儿院长大 xff0c 后被二叔接出 xff0c 早早进入社会 xff0c 从底层快递员做起 在被邮政执法堵截损失惨重后 xff0c 他设局结识国营邮政稽查负责人的千金路晓
  • c++11 条款21:尽量使用std::make_unique和std::make_shared而不直接使用new

    条款21 xff1a 尽量使用std make unique和std make shared而不直接使用new 让我们从对齐std make unique 和 std make shared这两块开始 std make shared是c 4