当前位置: 首页 > news >正文

C++11:unique_ptr的基本用法、使用场景和最佳使用指南

文章目录

  • 1. 简介
  • 2. 基本语法和用法
    • 2.1. 创建unique_ptr
    • 2.2. 访问指向的对象
    • 2.3. 所有权管理
  • 3. 自定义删除器
  • 4. 数组支持
  • 5. 常见使用场景
    • 5.1. RAII资源管理
    • 5.2. 工厂模式
    • 5.3. 容器中存储多态对象
    • 5.4. Pimpl(指针到实现)习惯用法
  • 6. 与其他智能指针的比较
    • 6.1. unique_ptr vs shared_ptr
    • 6.2. unique_ptr vs 原始指针
  • 7. 最佳实践指南
    • 7.1. 创建对象
    • 7.2. 函数参数传递
    • 7.3. 函数返回值
    • 7.4. 需要避免的反模式
  • 8. 总结

1. 简介

unique_ptr是C++11引入的智能指针,它具有对动态分配内存对象的独占所有权。是自动内存管理的核心工具,提供了异常安全的RAII(资源获取即初始化)语义,自动管理对象的生命周期,防止内存泄漏。

unique_ptr 遵循移动语义,只能被移动,不能被复制。这样就确保了在任何时候只有一个unique_ptr 拥有特定对象的所有权。

使用 unique_ptr 有以下几个优点

  • 内存安全:自动防止内存泄漏,是现代C++的核心特性
  • 零开销:提供智能指针的便利性而不牺牲性能
  • 异常安全:即使在异常情况下也能正确管理资源

2. 基本语法和用法

掌握基本语法是使用unique_ptr的基础,不同的创建和访问方式适用于不同的场景,了解它们有助于写出安全高效的代码。

2.1. 创建unique_ptr

可以通过原生指针、make_uniquenew 指针和默认初始化的方式创建 unique_ptr

#include <memory>// 方法1:使用new(不推荐)
std::unique_ptr<int> ptr1(new int(42));// 方法2:使用make_unique(推荐,C++14)
std::unique_ptr<int> ptr2 = std::make_unique<int>(42);// 方法3:默认构造(空指针)
std::unique_ptr<int> ptr3;// 方法4:从原始指针构造
int* raw_ptr = new int(100);
std::unique_ptr<int> ptr4(raw_ptr);

应该使用哪种创建方式比较好?

  • make_unique最佳:提供异常安全,避免内存泄漏,代码更简洁
  • 避免直接new:直接使用new容易在异常时造成内存泄漏
  • 空指针的用途:用于延迟初始化或条件性对象创建
  • 原始指针转换:用于接管已有的原始指针,但要确保不会重复删除

2.2. 访问指向的对象

创建一个 unique_ptr 之后,需要通过这个指针访问所指向的对象。
访问对象的内容有两种常见的方式:

  • 解引用操作符:直接访问对象的值,适用于简单类型
  • 箭头操作符:访问对象的成员,特别是对于类对象
std::unique_ptr<int> ptr = std::make_unique<int>(42);// 解引用操作符
int value = *ptr;                // 获取值: 42
std::cout << *ptr << std::endl;  // 输出: 42// 箭头操作符(对于对象指针)
class Person {
public:std::string name;void speak() { std::cout << name << " is speaking" << std::endl; }
};std::unique_ptr<Person> person = std::make_unique<Person>();
person->name = "Alice";
person->speak();  // Alice is speaking// get()方法获取原始指针
int* raw = ptr.get();  // 获取原始指针,但不转移所有权

2.3. 所有权管理

由于 unique_ptr 是独占所有权,所以所有权只能转移或者消亡。那么,有哪些引起所有权变化的操作呢?

  • std::move:转移一个指针的所有权
  • release():释放 unique_ptr 的所有权到原生指针,此时必须手动释放原生指针所指向的内存。
  • reset() 或者 reset(make_unique<int>(10)) :前者删除当前对象,并将指针设置为 nullptr;后者删除当前对象,指向新对象。
  • swap() :交换两者指针的所有权。
// 移动所有权
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1);  // ptr1变为nullptr,ptr2拥有所有权// 释放所有权
std::unique_ptr<int> ptr = std::make_unique<int>(42);
int* raw_ptr = ptr.release();  // ptr变为nullptr返回原始指针
// 注意:必须手动delete raw_ptr// 重置指针
ptr.reset();                    // 删除当前对象,设为nullptr
ptr.reset(new int(100));        // 删除当前对象,指向新对象// 交换两个unique_ptr
std::unique_ptr<int> ptr1 = std::make_unique<int>(1);
std::unique_ptr<int> ptr2 = std::make_unique<int>(2);
ptr1.swap(ptr2);  // 或 std::swap(ptr1, ptr2);

API 使用场景

  • 移动语义:高效转移所有权,避免不必要的复制,体现独占所有权语义
  • 谨慎使用release():在需要与C风格API交互时使用,但容易引入内存泄漏
  • reset()灵活管理:动态改变指向的对象,提供运行时灵活性
  • swap()高效交换:避免临时对象,性能优化的需要

3. 自定义删除器

默认删除器只能处理用new分配的对象,但实际开发中经常需要管理各种资源(文件、网络连接、系统句柄等),自定义删除器提供了统一的RAII管理方式。

unique_ptr允许自定义删除器,用于特殊的清理需求。例如下面的例子中,FILE 文件指针需要使用 fclose 函数关闭,这个时候就可以自定义文件删除器删除,避免手动调用 fclose 函数。当超出 file_ptr 的作用域时,会自动调用 FileDeleter

// 自定义删除器 - 函数对象
struct FileDeleter {void operator()(FILE* f) {if (f) {std::fclose(f);std::cout << "文件已关闭" << std::endl;}}
};std::unique_ptr<FILE, FileDeleter> file_ptr(std::fopen("test.txt", "w"));

4. 数组支持

动态数组在C++中很常见,unique_ptr 提供的数组支持解决了数组内存管理的痛点,自动使用正确的 delete[] 操作符,避免未定义行为。

unique_ptr专门支持动态数组:

// 动态数组
std::unique_ptr<int[]> arr = std::make_unique<int[]>(10);// 使用下标访问
for (int i = 0; i < 10; ++i) {arr[i] = i * i;std::cout << arr[i] << " ";
}// 注意:数组版本不支持解引用和箭头操作符
// *arr;     // 编译错误
// arr->x;   // 编译错误

使用原因

  • 正确删除:自动使用delete[]而不是delete,避免未定义行为
  • 类型安全:编译期防止在数组指针上使用解引用操作
  • 内存安全:自动管理数组生命周期,防止内存泄漏
  • 性能优化:避免使用vector的开销,适合简单的数组需求

5. 常见使用场景

了解典型使用场景有助于在实际开发中正确选择 unique_ptr,下面这些模式是工业级代码的常见实践,掌握它们能显著提高代码质量。

5.1. RAII资源管理

结合 unique_ptr 和 RAII 管理文件资源、数据库连接、网络连接等。下面是一个管理文件资源的例子。

struct FileDeleter {void operator()(FILE* f) {if (f) {std::fclose(f);std::cout << "文件已关闭" << std::endl;}}
};std::unique_ptr<FILE, FileDeleter> file_ptr(std::fopen("test.txt", "w"));

RAII核心优势

  • 自动资源管理:构造时获取资源,析构时自动释放,无需手动管理;
  • 异常安全保证:即使在异常情况下,unique_ptr 也确保资源正确清理;
  • 组合资源管理:可以在同一类中管理多种不同类型的资源;
  • 移动语义支持:支持高效的资源所有权转移,避免不必要的复制。

5.2. 工厂模式

class Shape {
public:virtual ~Shape() = default;virtual void draw() = 0;
};class Circle : public Shape {
public:void draw() override { std::cout << "绘制圆形" << std::endl; }
};class Rectangle : public Shape {
public:void draw() override { std::cout << "绘制矩形" << std::endl; }
};// 工厂函数返回unique_ptr
std::unique_ptr<Shape> create_shape(const std::string& type) {if (type == "circle") {return std::make_unique<Circle>();} else if (type == "rectangle") {return std::make_unique<Rectangle>();}return nullptr;
}// 使用
auto shape = create_shape("circle");
if (shape) {shape->draw();  // 绘制圆形
}

使用原因

  • 明确所有权:工厂返回unique_ptr明确表示调用者拥有对象
  • 多态支持:基类指针可以指向派生类对象,支持多态
  • 内存安全:对象自动管理,无需手动delete
  • 空值语义:返回nullptr表示创建失败,语义清晰

5.3. 容器中存储多态对象

std::vector<std::unique_ptr<Shape>> shapes;shapes.push_back(std::make_unique<Circle>());
shapes.push_back(std::make_unique<Rectangle>());for (const auto& shape : shapes) {shape->draw();
}
// 容器销毁时,所有对象自动释放

使用原因

  • 多态容器:可以在同一容器中存储不同类型的对象
  • 自动清理:容器销毁时所有对象自动释放,无内存泄漏
  • 移动语义:对象在容器中移动而不是复制,性能更好
  • 异常安全:即使在容器操作中发生异常,已创建的对象也会被正确清理

5.4. Pimpl(指针到实现)习惯用法

Pimpl的核心思想是在类的公有接口(通常在.h头文件中声明)中,不直接包含私有成员变量和私有成员函数的具体声明,而是只包含一个指向不完整类型(通常是一个内部类或结构体,称为Impl类)的指针。这个Impl类则包含了所有原先的私有成员和实现细节。

实现逻辑:

  1. 头文件:
    • 前向声明一个内部实现类,例如 class Impl
    • 持有一个指向该实现类的只能指针,通常是 std::unique_ptr<Impl>,因为Pimpl通常意味着独占所有权;
  2. 实现文件:
    • 定义完整的内部Impl类,包含所有私有数据成员和辅助函数;
    • 实现外部类的构造函数、析构函数、拷贝构造函数等;
    • 实现外部类的公有函数,函数体通过Impl指针调用内部类的函数。

有什么好处?

  1. 减少编译依赖。这是Pimpl最主要的优点。当类的私有成员(尤其是那些依赖于其他复杂头文件的成员)发生改变时,只需要重新编译该类的.cpp实现文件,而不需要重新编译所有包含该类头文件的客户端代码。
  2. 隐藏实现细节。类的用户只能看到公有接口,完全不知道其内部实现细节(如私有成员变量的类型和数量),这增强了封装性。例如,我们为甲方开发了一套库,但是不希望甲方知道算法的内部逻辑。

有什么缺点?

  1. 增加了构造和析构函数的开销。需要额外在堆上为Impl对象分配内存(通过std::make_unique),并进行构造。虽然 std::unique_ptr 能很好地管理生命周期,但堆分配本身有开销。
  2. 增加了调试的复杂度。

代码例子:

// Widget.h - 头文件
class Widget {
public:Widget();Widget(int value, const std::string& name);~Widget();// 拷贝和移动操作需要特别处理Widget(const Widget& other);Widget& operator=(const Widget& other);Widget(Widget&& other) noexcept;Widget& operator=(Widget&& other) noexcept;// 公共接口void do_something();void set_value(int value);int get_value() const;std::string get_name() const;private:class Impl;  // 前向声明,不暴露实现细节std::unique_ptr<Impl> pImpl;  // 指向实现的智能指针
};

下面是实现文件:

// Widget.cpp - 实现文件
#include "Widget.h"
#include <iostream>
#include <vector>
#include <map>
#include <complex_third_party_library.h>  // 只在.cpp中包含// 实现类定义(完全隐藏)
class Widget::Impl {
public:Impl(int val, const std::string& n) : value(val), name(n) {}void do_something() {std::cout << "处理 " << name << " 的值: " << value << std::endl;// 复杂的实现逻辑...process_data();use_third_party_library();}void set_value(int val) { value = val; }int get_value() const { return value; }std::string get_name() const { return name; }private:int value;std::string name;std::vector<double> data;  // 复杂的数据结构std::map<std::string, int> cache;ThirdPartyObject complex_obj;  // 第三方库对象void process_data() {// 复杂的内部逻辑}void use_third_party_library() {// 使用第三方库的代码}
};// 公共接口的实现
Widget::Widget() : pImpl(std::make_unique<Impl>(0, "default")) {}Widget::Widget(int value, const std::string& name) : pImpl(std::make_unique<Impl>(value, name)) {}Widget::~Widget() = default;  // unique_ptr自动清理// 拷贝构造函数
Widget::Widget(const Widget& other) : pImpl(std::make_unique<Impl>(*other.pImpl)) {}// 拷贝赋值操作符
Widget& Widget::operator=(const Widget& other) {if (this != &other) {*pImpl = *other.pImpl;}return *this;
}// 移动构造函数
Widget::Widget(Widget&& other) noexcept = default;// 移动赋值操作符
Widget& Widget::operator=(Widget&& other) noexcept = default;// 委托给实现类的方法
void Widget::do_something() {pImpl->do_something();
}void Widget::set_value(int value) {pImpl->set_value(value);
}int Widget::get_value() const {return pImpl->get_value();
}std::string Widget::get_name() const {return pImpl->get_name();
}

6. 与其他智能指针的比较

了解不同智能指针的特点有助于在合适的场景选择合适的工具,避免过度工程或性能损失,这是高级C++程序员必备的知识。

6.1. unique_ptr vs shared_ptr

特性unique_ptrshared_ptr
所有权独占共享
内存开销低(通常只有一个指针大小)高(需要引用计数)
性能高(无引用计数开销)较低(原子操作开销)
线程安全移动操作需要同步引用计数是线程安全的
使用场景明确单一所有者需要共享所有权

选择原因

  • unique_ptr优先:大多数情况下对象只需要一个所有者;
  • 性能考虑:unique_ptr零开销,shared_ptr有引用计数开销;
  • 设计清晰:unique_ptr强制明确所有权关系,设计更清晰;
  • 特定需求:只有真正需要共享所有权时才使用shared_ptr。

6.2. unique_ptr vs 原始指针

// 原始指针的问题
void problematic_function() {int* ptr = new int(42);if (some_condition) {return;  // 内存泄漏!}risky_operation();  // 如果抛出异常,内存泄漏!delete ptr;  // 可能永远执行不到
}// unique_ptr解决方案
void safe_function() {auto ptr = std::make_unique<int>(42);if (some_condition) {return;  // 自动清理,无泄漏}risky_operation();  // 异常安全,自动清理// 函数结束时自动清理
}

选择原因

  • 内存安全:unique_ptr防止内存泄漏,原始指针容易泄漏
  • 异常安全:unique_ptr提供强异常安全保证
  • 代码简洁:无需手动管理内存,减少样板代码
  • 性能相等:unique_ptr零开销,性能与原始指针相同

7. 最佳实践指南

为什么重要:最佳实践是多年经验的总结,遵循这些指导原则可以避免常见陷阱,写出高质量、可维护的代码,这对团队协作和项目维护至关重要。

7.1. 创建对象

// 推荐:使用make_unique
auto ptr = std::make_unique<MyClass>(args);// 不推荐:使用new
std::unique_ptr<MyClass> ptr(new MyClass(args));

避免直接使用 new 创建智能指针,因为它无法提供异常安全,而make_unique提供异常安全。

7.2. 函数参数传递

// 传递所有权:按值传递
void take_ownership(std::unique_ptr<Widget> widget) {// 函数拥有widget的所有权
}// 借用使用:传递原始指针或引用
void use_widget(Widget* widget) {// 临时使用,不改变所有权
}void use_widget_ref(const Widget& widget) {// 只读使用
}// 调用示例
auto widget = std::make_unique<Widget>();
use_widget(widget.get());         // 借用
use_widget_ref(*widget);          // 借用(引用)
take_ownership(std::move(widget)); // 转移所有权
// widget现在是nullptr

使用原因

  • 意图明确:参数类型清楚表达函数是否需要所有权
  • 性能优化:借用时避免不必要的所有权转移
  • 接口设计:清晰的接口设计减少误用
  • 兼容性:原始指针参数与现有代码兼容

7.3. 函数返回值

// 推荐:返回unique_ptr表明所有权转移
std::unique_ptr<Widget> create_widget() {return std::make_unique<Widget>();
}// 工厂函数的典型模式
std::unique_ptr<Shape> shape_factory(ShapeType type) {switch (type) {case ShapeType::Circle:return std::make_unique<Circle>();case ShapeType::Rectangle:return std::make_unique<Rectangle>();default:return nullptr;  // 表示创建失败}
}

使用原因

  • 所有权转移:明确表示调用者获得对象所有权
  • 异常安全:返回过程中的异常不会导致内存泄漏
  • 错误处理:nullptr表示创建失败,语义清晰
  • 移动语义:高效的对象传递,避免复制

7.4. 需要避免的反模式

// 反模式1:不要从unique_ptr创建shared_ptr
std::unique_ptr<Widget> unique_widget = std::make_unique<Widget>();
// 不推荐
std::shared_ptr<Widget> shared_widget(unique_widget.release());// 反模式2:不要将同一个原始指针给多个unique_ptr
Widget* raw = new Widget();
std::unique_ptr<Widget> ptr1(raw);  // 危险!
std::unique_ptr<Widget> ptr2(raw);  // 双重删除!// 反模式3:不要保存get()返回的指针
auto ptr = std::make_unique<Widget>();
Widget* raw = ptr.get();
ptr.reset();  // 现在raw是悬空指针!
// raw->do_something();  // 未定义行为!

避免原因

  • 双重删除:多个智能指针管理同一对象会导致双重删除
  • 悬空指针:保存get()返回的指针容易产生悬空指针
  • 设计混乱:混用不同的智能指针类型破坏设计清晰性
  • 难以调试:这些反模式产生的bug往往难以定位和修复

8. 总结

unique_ptr是现代C++中内存管理的基石,它提供了:

  1. 自动内存管理:无需手动调用 delete;
  2. 异常安全:即使在异常情况下也能正确清理资源;
  3. 移动语义:高效的所有权转移;
  4. 零开销:运行时性能与原始指针相当。

使用unique_ptr的关键原则:

  • 优先使用make_unique创建对象;
  • 通过移动语义转移所有权;
  • 使用原始指针或引用进行临时访问;
  • 在容器中存储unique_ptr实现多态;
  • 避免混合使用智能指针和原始指针。

掌握unique_ptr是编写现代C++代码的必备技能,它能有效防止内存泄漏,提高代码的安全性和可维护性。

相关文章:

  • day32-系统编程之 进程间通信IPC
  • 蓝绿部署解析
  • 转战web3远程工作的英语学习的路线规划
  • Windows下将Nginx设置注册安装为服务方法!
  • 半导体行业-研发设计管理数字化转型案例分享
  • C/S医学影像系统源码,全院一体化PACS系统源码,实现全院检查预约和信息共享互通
  • CppCon 2014 学习: Less Code = More Software
  • 春雪食品×MTC AI助手:创新驱动再升级,效率革命正当时!
  • python中可以对数组使用的所有方法
  • 基于VLC的Unity视频播放器(四)
  • qt控制台程序与qt窗口程序在读取数据库中文字段的差异!!巨坑
  • 大模型 提示模板 设计
  • 腾讯 ovCompose 开源,Kuikly 鸿蒙和 Compose DSL 开源,腾讯的“双”鸿蒙方案发布
  • 大模型赋能:金融智能革命中的特征工程新纪元
  • AutoGenTestCase - 借助AI大模型生成测试用例
  • 更新已打包好的 Spring Boot JAR 文件中的 class 文件
  • 项目开发:【悟空博客】基于SSM框架的博客平台
  • html基础01:前端基础知识学习
  • 古典密码学介绍
  • SpringAI系列 - MCP篇(三) - MCP Client Boot Starter
  • 个人博客页面设计图/优化关键词的方法
  • 太原网站上排名/百度在线问答
  • 做网站要切图吗/百度竞价包年推广是怎么回事
  • 绑定ip地址的网站/自动点击竞价广告软件
  • 怎么做网站教程 建站视频/电商怎么注册开店
  • 冷饮店怎么做网站/百度收录申请入口