文章目录
- 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_unique
、new
指针和默认初始化的方式创建 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
类则包含了所有原先的私有成员和实现细节。
实现逻辑:
- 头文件:
- 前向声明一个内部实现类,例如
class Impl
; - 持有一个指向该实现类的只能指针,通常是
std::unique_ptr<Impl>
,因为Pimpl通常意味着独占所有权;
- 前向声明一个内部实现类,例如
- 实现文件:
- 定义完整的内部
Impl
类,包含所有私有数据成员和辅助函数; - 实现外部类的构造函数、析构函数、拷贝构造函数等;
- 实现外部类的公有函数,函数体通过
Impl
指针调用内部类的函数。
- 定义完整的内部
有什么好处?
- 减少编译依赖。这是Pimpl最主要的优点。当类的私有成员(尤其是那些依赖于其他复杂头文件的成员)发生改变时,只需要重新编译该类的.cpp实现文件,而不需要重新编译所有包含该类头文件的客户端代码。
- 隐藏实现细节。类的用户只能看到公有接口,完全不知道其内部实现细节(如私有成员变量的类型和数量),这增强了封装性。例如,我们为甲方开发了一套库,但是不希望甲方知道算法的内部逻辑。
有什么缺点?
- 增加了构造和析构函数的开销。需要额外在堆上为Impl对象分配内存(通过
std::make_unique
),并进行构造。虽然std::unique_ptr
能很好地管理生命周期,但堆分配本身有开销。 - 增加了调试的复杂度。
代码例子:
// 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_ptr | shared_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++中内存管理的基石,它提供了:
- 自动内存管理:无需手动调用 delete;
- 异常安全:即使在异常情况下也能正确清理资源;
- 移动语义:高效的所有权转移;
- 零开销:运行时性能与原始指针相当。
使用unique_ptr
的关键原则:
- 优先使用
make_unique
创建对象; - 通过移动语义转移所有权;
- 使用原始指针或引用进行临时访问;
- 在容器中存储
unique_ptr
实现多态; - 避免混合使用智能指针和原始指针。
掌握unique_ptr
是编写现代C++代码的必备技能,它能有效防止内存泄漏,提高代码的安全性和可维护性。