0

0

为什么现代C++推荐使用std::make_unique来创建unique_ptr

P粉602998670

P粉602998670

发布时间:2025-09-10 08:19:01

|

992人浏览过

|

来源于php中文网

原创

推荐使用std::make_unique创建unique_ptr,因其将对象构造与智能指针创建封装为原子操作,避免因函数参数求值顺序不确定导致的异常安全问题,同时提升代码简洁性与可读性。

为什么现代c++推荐使用std::make_unique来创建unique_ptr

现代C++中推荐使用

std::make_unique
来创建
unique_ptr
,这主要是因为它能有效提升代码的异常安全性,同时让代码更简洁、易读。直接使用
new
来构造
unique_ptr
,在某些复杂的表达式中,可能会引入难以察觉的资源泄露风险。

解决方案

当我们谈论

unique_ptr
的创建,很多人可能习惯性地写成
std::unique_ptr ptr(new MyClass());
。这在大多数简单场景下看起来没问题,但实际上,这种写法在某些特定情况下存在一个微妙但重要的缺陷,那就是潜在的异常安全性问题。

考虑一个函数调用,其中包含多个参数,例如:

some_function(std::unique_ptr(new MyClass()), another_function_that_might_throw());

C++标准对函数参数的求值顺序并没有严格规定,只知道在调用

some_function
之前,所有的参数都必须被求值完毕。这意味着,编译器可能会以以下某种顺序执行操作:

  1. 调用
    new MyClass()
    分配内存并构造对象。
  2. 调用
    another_function_that_might_throw()
  3. 调用
    std::unique_ptr(...)
    构造智能指针,接管
    MyClass
    对象的管理。

如果执行顺序是1 -> 2 -> 3,并且

another_function_that_might_throw()
在步骤2中抛出了异常,那么步骤3(
unique_ptr
的构造)将永远不会发生。此时,步骤1中通过
new MyClass()
分配的内存和构造的对象将无人管理,从而导致内存泄露。因为
new
操作已经完成,但
unique_ptr
还没来得及接管。

立即学习C++免费学习笔记(深入)”;

std::make_unique
的设计,正是为了解决这个问题。它将对象的分配和
unique_ptr
的构造封装成一个原子操作。当你写
std::make_unique()
时,
MyClass
对象的创建和
unique_ptr
对它的接管,要么一起成功,要么一起失败,中间不会留下悬空的原始指针。这保证了在存在异常的情况下,资源能够得到妥善管理,避免了上述的泄露风险。它强制了“分配和拥有”这两个动作的紧密耦合。

std::make_unique是如何提供异常安全性的?

std::make_unique
提供异常安全性的核心机制在于它将内存分配(
new
)和
unique_ptr
的构造视为一个单一的、不可分割的操作。我们之前提到的潜在泄露场景,其根本原因在于C++标准允许编译器在求值函数参数时,对子表达式的求值顺序有一定自由度。

例如,对于

func(A(), B(), C())
这样的调用,A、B、C的求值顺序是不确定的。如果A是
new MyObject()
,B是
another_risky_call()
,C是
std::unique_ptr(...)
,那么在
new MyObject()
执行完毕后,
another_risky_call()
可能在
std::unique_ptr
构造前抛出异常,导致
MyObject
泄露。

std::make_unique
通过在内部完成
new
操作并直接返回一个已构造好的
unique_ptr
实例,绕开了这个“参数求值顺序不确定”的陷阱。当调用
std::make_unique(args...)
时,它会:

  1. 在内部调用
    new MyClass(args...)
    来分配内存并构造对象。
  2. 立即将这个新创建的原始指针传递给
    unique_ptr
    的构造函数。 整个过程被封装在一个函数调用中,使得从外部看来,
    make_unique
    要么返回一个有效的
    unique_ptr
    ,要么在内部抛出异常(例如,如果
    MyClass
    的构造函数抛出),但绝不会在
    new
    了一个对象后,又因为外部其他操作的异常而导致该对象无人管理。

这使得像下面这样的代码变得安全:

void process_data(std::unique_ptr p, int value);
void potentially_failing_operation();

// 不安全的方式
// process_data(std::unique_ptr(new Data()), potentially_failing_operation());
// 如果 Data 的 new 完成了,但 potentially_failing_operation() 抛出,Data 会泄露。

// 安全的方式
process_data(std::make_unique(), potentially_failing_operation());
// make_unique 确保 Data 对象要么被 unique_ptr 拥有,要么在创建过程中失败,不会出现中间状态的泄露。

这种设计极大地简化了对异常安全性的考量,让开发者能更专注于业务逻辑,而不是底层内存管理的复杂性。

除了异常安全,使用std::make_unique还有哪些实际好处?

除了异常安全性这个核心优势,

std::make_unique
在日常编码中还带来了不少其他实际的好处,让代码更具可读性和维护性。

Artifact News
Artifact News

由AI驱动的个性化新闻推送

下载

首先,代码的简洁性和可读性得到了显著提升。对比一下两种创建方式:

// 旧方式
std::unique_ptr ptr1(new MyComplexType(arg1, arg2, arg3));

// 推荐方式
std::unique_ptr ptr2 = std::make_unique(arg1, arg2, arg3);

很明显,第二种方式更加简洁。它避免了重复写入类型名称

MyComplexType
,减少了冗余,也使得代码的意图——“创建一个
MyComplexType
的唯一所有权智能指针”——更加清晰。这种“不重复自己”(DRY原则)的体现,在类型名称较长或模板类型时尤其明显。

其次,它与

std::make_shared
保持了风格上的一致性。在现代C++中,
std::make_shared
是创建
std::shared_ptr
的推荐方式,其原因也包括异常安全性和潜在的性能优化(通过一次内存分配同时为对象和控制块分配内存)。
std::make_unique
的引入,使得智能指针的创建方式趋于统一,降低了学习曲线,也让代码库看起来更加协调和专业。

虽然对于

unique_ptr
而言,
make_unique
在性能上的优势通常不如
make_shared
那么显著(
unique_ptr
没有独立的控制块),但编译器和库实现者仍然有可能在某些情况下,通过优化
make_unique
的内部实现,实现微小的性能提升,例如减少内存分配器的调用次数。但这通常不是选择
make_unique
的主要驱动因素,其主要价值仍在异常安全性和代码清晰度上。

在哪些情况下,我可能仍然需要直接使用new来创建unique_ptr?

尽管

std::make_unique
是创建
unique_ptr
的首选方式,但在某些特定场景下,我们仍然需要或更适合直接使用
new
操作符来构造
unique_ptr
。这些情况通常涉及更高级的内存管理需求或与C风格API的交互。

最常见的情况是当需要使用自定义deleter时

std::make_unique
不提供直接指定自定义deleter的接口。
unique_ptr
的构造函数有一个重载版本,允许你传入一个原始指针和一个deleter对象或函数指针。 例如,如果你想管理一个C语言的
FILE*
,并确保它被
fclose
正确关闭:

#include 
#include 

// 自定义 deleter 函数
void file_closer(FILE* f) {
    if (f) {
        fclose(f);
    }
}

int main() {
    // 无法使用 make_unique
    // std::unique_ptr log_file = std::make_unique(fopen("log.txt", "w"), &file_closer); // 错误

    // 必须直接使用 new (或者这里是 fopen 返回的指针)
    std::unique_ptr log_file(fopen("log.txt", "w"), &file_closer);
    if (log_file) {
        fprintf(log_file.get(), "Hello from unique_ptr!\n");
    }
    // log_file 在离开作用域时会自动关闭文件
    return 0;
}

这里,

fopen
返回的是一个原始
FILE*
指针,我们需要
unique_ptr
来接管它,并指定
file_closer
作为其析构时的操作。

另一个场景是当需要从一个已存在的原始指针接管所有权时。这可能发生在与遗留C++代码库交互,或者当一个工厂函数返回一个

new
出来的原始指针时。

// 假设这是一个C风格的API,返回一个 new 出来的对象
MyClass* create_my_class_raw() {
    return new MyClass();
}

int main() {
    MyClass* raw_ptr = create_my_class_raw();
    // 此时不能用 make_unique,因为它会再次 new 一个对象
    std::unique_ptr managed_ptr(raw_ptr);
    // managed_ptr 现在拥有了 raw_ptr 指向的对象
    return 0;
}

在这种情况下,我们不是要“创建”一个新的对象,而是要“接管”一个已经存在的对象的所有权,所以直接将原始指针传递给

unique_ptr
的构造函数是唯一的选择。

最后,当处理

unique_ptr
管理数组且需要自定义deleter时
std::make_unique
有一个重载版本用于创建数组(
std::make_unique(size)
),它会使用
delete[]
来释放内存。但如果你需要一个特殊的数组deleter,比如一个内存池的释放函数,你就需要直接
new T[size]
并结合自定义deleter来构造
unique_ptr

这些情况虽然相对小众,但确实存在,提醒我们

std::make_unique
并非万能,理解其局限性与优势同样重要。选择哪种方式,最终还是取决于具体的编程需求和上下文。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

401

2023.06.20

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

619

2023.07.25

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

354

2023.08.02

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

259

2023.08.09

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

603

2023.09.05

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

527

2023.09.20

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

645

2023.09.20

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

602

2023.09.22

c++ 根号
c++ 根号

本专题整合了c++根号相关教程,阅读专题下面的文章了解更多详细内容。

58

2026.01.23

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 4.8万人学习

Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

Go 教程
Go 教程

共32课时 | 4.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号