1. 开篇
今年定了 5 篇技术文章的指标, 为了不打脸, 国庆期间抓紧写一篇, 先从简单的基础开始.
2. 背景
对于 C++ 的程序员来说, 标准库提供的 unique_ptr 应该不陌生, 但很多时候其实是没有一个清晰的使用原则, 比如什么时候该用, 用了会有什么限制, 有什么收益, 似乎就是用了也就是用了, 不用也没什么.
本文尝试从几个不同的场景出发总结一下 unique_ptr 的一些特性和使用的收益和约束, 如果有理解不到位的地方欢迎各位大佬加以指正.
3. unique_ptr 介绍
unique_ptr 是 C++ 标准库提供的一种智能指针的实现, 拥有和管理一个对象
3.1 std::make_unique
make_unique 是用来构造一个 unique_ptr 对象的方法, 但需要在 C++14 之后才能使用
为什么 c11 没有这个? 据说是大佬们忘记加了
// 头文件增加 include
#include
...
// 通过 make_unique 构造
std::unique_ptr u_p1 = std::make_unique();
// 通过 get 返回管理资源的裸指针
Test* p1 = u_p1.get();
3.2 释放资源
当 unique_ptr 销毁或者调用 reset 的时候会调用删除器去尝试清理资源
{
// 自动释放资源
std::unique_ptr u_p1 = std::make_unique();
}
// 通过 reset 释放资源
std::unique_ptr u_p2 = std::make_unique();
u_p2.reset();
// release 释放了所有权, 但没有删除资源, 需要手动删除,
std::unique_ptr u_p3 = std::make_unique();
Test* p3 = u_p3.release();
delete p3;
3.3 移交所有权
unique_ptr 可以进行 move 操作, move 操作会移交管理资源的所有权
// 通过 move 操作, 移交所有权, 移交后, u_p1 不再管理资源
std::unique_ptr u_p1;
std::unique_ptr u_p2 = std::move(u_p1);
3.4 copy 操作不合法
unique_ptr 不可以进行 copy 操作, copy 操作会破坏管理资源的所有权唯一性
std::unique_ptr u_p1;
std::unique_ptr u_p2 = u_p1; // 不合法
4. unique_ptr 的使用
我们先假设存在测试类
// Heart
class Heart {};
4.1 做为成员变量
// Human
class Human {
...
private:
// 作为类的成员变量声明
const std::unique_ptr heart_;
};
4.1.1 使用限制
当在头文件里使用 unique_ptr 后, 对于 Heart 这个类, 我们不能使用前置声明的方式, 例如:
// Heart
class Heart;
// Human
class Human {
...
private:
// 如果不指定删除器, 编译器会报错
std::unique_ptr heart_;
};
unique_ptr 是模板类, 默认的删除器需要通过 sizeof 获取泛型数据类型的字节大小.
为了减少麻烦, 如果有比较多的习惯使用 unique_ptr, 那么就减少前置声明的使用
当然有的同学会说, 可以通过自定义删除器绕开一些问题, 但大多数工程使用自定义删除器是很少的个例, 不必要为了个例而且否定一个对于大多数场景是有利的原则.
4.1.2 对比裸指针
一个类声明的成员变量是 unique_ptr 还是裸指针? 理论上都是可以实现的, 但对于一个大型 C++ 工程而言, 这其实是一个影响程序可读性, 可维护性的问题.
先以 Human 这个例子来说, 当创造一个 Human 对象的时候, heart_ 就应该被构造出来了, 因为 heart_ 是 const 类型, 只能在构造函数内的初始化列表中构造, 那么如果有其他的地方需要使用这个 heart_ 所管理的资源, 前提就必须是在这个 Human 对象没有被释放之前.
这很好的声明了内存模型, 程序内部的资源管理关系也能体现出来, 对于读代码的人来说在同等认知情况下就非常容易理解原作者的意图.
我们试想一下, 假如以裸指针的方式声明, 在不去看逻辑是如何实现的前提下, 是不会明白原作者的意图的, 这就降低了程序的可读性和维护性.
4.2 做为函数参数
作为函数的参数, 说明当前的参数已经把所有权移交给当前的函数栈, 如果函数内部不使用持有这个资源, 当退出函数体后, 这个资源就会被释放掉.
这通常是一种移交所有权的方式
void Human::test(std::unique_ptr heart) {
// Human 类声明了 类型为 unique_ptr 的变量 heart_
heart_ = std::move(heart);
}
4.3 作为函数返回值
函数返回 unique_ptr 说明这个方法是一个用来构造资源的工厂方法, 只负责构造资源, 但不持有所有权.
// HeartFactory
class HeartFactory {
std::unique_ptr CreateHeart() {
return std::make_unique();
}
};
4.3.1 对比裸指针
返回值如果是裸指针, 说明函数只是把管理的资源提供给外部使用, 但资源的所有权还在调用的这个对象内部.
5. 总结
可以看到 unique_ptr 使用并不复杂, 但恰当的使用可以使得程序的内存模型结构非常的清晰, 所有权明确, 对于读代码的人来说很好的降低理解成本, 提高可维护性.
可运行的程序是程序员的最低目标, 一个好的程序员应该追求提高可读性, 可维护性