文章目录
之前使用智能指针一直是一知半解,现在有时间了,尝试从源码的角度来理解它。
一、unique_ptr 概述
std::unique_ptr
是 C++11 引入的智能指针,用于管理动态分配的内存资源,其核心特性是独占所有权——同一时间只能有一个 unique_ptr
指向特定对象。当 unique_ptr
离开作用域或被销毁时,它所管理的对象会自动释放,从而有效避免内存泄漏。
核心优势
- 自动资源管理:无需手动调用
delete
,降低内存泄漏风险 - 异常安全:即使在异常抛出的情况下,仍能保证资源正确释放
- 移动语义支持:可通过移动操作转移所有权,避免浅拷贝问题
- 自定义删除器:支持非默认的资源释放逻辑(如文件句柄、网络连接)
二、unique_ptr 原理深度解析
2.1 独占所有权模型
unique_ptr
的核心设计思想是独占性,这意味着:
- 不允许拷贝构造和拷贝赋值(通过
= delete
显式禁用) - 仅允许移动构造和移动赋值(通过右值引用实现所有权转移)
- 当
unique_ptr
被销毁时,其管理的对象也会被自动删除
2.2 简化版 unique_ptr 实现
下面通过实现一个简化版的 unique_ptr
来理解其工作原理:
#include <utility> // 用于 std::move
// 默认删除器
template <typename T>
struct DefaultDeleter {
void operator()(T* ptr) const {
delete ptr; // 对单对象使用 delete
}
};
// 数组特化版本的删除器
template <typename T>
struct DefaultDeleter<T[]> {
void operator()(T* ptr) const {
delete[] ptr; // 对数组使用 delete[]
}
};
// 简化版 unique_ptr 实现
template <typename T, typename Deleter = DefaultDeleter<T>>
class UniquePtr {
private:
T* ptr_; // 管理的原始指针
Deleter deleter_; // 删除器对象
public:
// 构造函数:接受原始指针
explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
// 析构函数:释放资源
~UniquePtr() {
if (ptr_) {
deleter_(ptr_); // 调用删除器释放资源
}
}
// 禁用拷贝构造函数
UniquePtr(const UniquePtr&) = delete;
// 禁用拷贝赋值运算符
UniquePtr& operator=(const UniquePtr&) = delete;
// 移动构造函数:转移所有权
UniquePtr(UniquePtr&& other) noexcept
: ptr_(other.ptr_), deleter_(std::move(other.deleter_)) {
other.ptr_ = nullptr; // 源指针置空,避免二次释放
}
// 移动赋值运算符:转移所有权
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
if (ptr_) {
deleter_(ptr_); // 释放当前资源
}
ptr_ = other.ptr_; // 转移指针
deleter_ = std::move(other.deleter_); // 转移删除器
other.ptr_ = nullptr; // 源指针置空
}
return *this;
}
// 解引用运算符
T& operator*() const {
return *ptr_;
}
// 成员访问运算符
T* operator->() const {
return ptr_;
}
// 数组访问运算符(特化版本中实现)
T& operator[](size_t index) const;
// 获取原始指针
T* get() const {
return ptr_;
}
// 释放所有权
T* release() {
T* temp = ptr_;
ptr_ = nullptr;
return temp;
}
// 重置指针
void reset(T* new_ptr = nullptr) {
if (ptr_) {
deleter_(ptr_); // 释放当前资源
}
ptr_ = new_ptr; // 指向新资源
}
// 交换管理的资源
void swap(UniquePtr& other) noexcept {
std::swap(ptr_, other.ptr_);
std::swap(deleter_, other.deleter_);
}
// 检查是否管理资源
explicit operator bool() const {
return ptr_ != nullptr;
}
};
// 数组版本的 operator[] 实现
template <typename T, typename Deleter>
class UniquePtr<T[], Deleter> {
// 实现与上述类似,但增加数组访问运算符
public:
T& operator[](size_t index) const {
return ptr_[index];
}
// 其他成员函数与单对象版本类似...
};
2.3 关键技术点解析
删除器设计
- 默认使用
DefaultDeleter
,对单对象使用delete
,对数组使用delete[]
- 支持自定义删除器,满足特殊资源释放需求(如文件、网络连接)
- 默认使用
移动语义实现
- 通过右值引用(
&&
)实现移动构造和移动赋值 - 转移所有权后将源指针置空,确保资源唯一管理
- 通过右值引用(
禁止拷贝
- 通过
= delete
显式禁用拷贝构造和拷贝赋值 - 确保同一时间只有一个智能指针管理资源
- 通过
三、unique_ptr 使用详解
3.1 基本用法
#include <memory>
#include <iostream>
struct MyClass {
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destroyed\n"; }
void do_something() { std::cout << "Doing something\n"; }
};
int main() {
// 创建 unique_ptr,管理动态分配的对象
std::unique_ptr<MyClass> ptr1(new MyClass());
// 使用 make_unique 创建(C++14 引入,更安全)
auto ptr2 = std::make_unique<MyClass>();
// 访问成员函数
ptr1->do_something();
(*ptr2).do_something();
// 检查是否为空
if (ptr1) {
std::cout << "ptr1 is not null\n";
}
// 转移所有权
std::unique_ptr<MyClass> ptr3 = std::move(ptr1);
if (!ptr1) { // ptr1 现在为空
std::cout << "ptr1 is null after move\n";
}
// 释放资源
ptr3.reset(); // 显式释放,此时会调用析构函数
if (!ptr3) {
std::cout << "ptr3 is null after reset\n";
}
return 0;
}
3.2 数组管理
unique_ptr
对数组有专门的特化版本,支持 operator[]
访问:
// 管理动态数组
auto arr_ptr = std::make_unique<int[]>(5); // 创建包含5个int的数组
// 访问数组元素
for (int i = 0; i < 5; ++i) {
arr_ptr[i] = i * 10;
std::cout << arr_ptr[i] << " ";
}
// 输出:0 10 20 30 40
// 数组版本会自动使用 delete[] 释放资源
3.3 自定义删除器
当管理非内存资源(如文件句柄、网络套接字)时,可使用自定义删除器:
#include <cstdio>
// 自定义文件删除器
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << "File closed\n";
}
}
};
int main() {
// 使用自定义删除器的 unique_ptr
std::unique_ptr<FILE, FileDeleter> file_ptr(
std::fopen("example.txt", "w"), FileDeleter()
);
if (file_ptr) {
std::fputs("Hello, unique_ptr!", file_ptr.get());
}
// 离开作用域时自动调用 FileDeleter 关闭文件
return 0;
}
也可使用 lambda 表达式作为删除器:
auto ptr = std::unique_ptr<MyClass, void(*)(MyClass*)>(
new MyClass(),
[](MyClass* p) {
std::cout << "Custom deleter called\n";
delete p;
}
);
3.4 与多态结合使用
unique_ptr
支持基类指针指向派生类对象,实现多态:
struct Base {
virtual void foo() { std::cout << "Base::foo\n"; }
virtual ~Base() = default; // 基类析构函数必须为虚函数
};
struct Derived : Base {
void foo() override { std::cout << "Derived::foo\n"; }
};
int main() {
std::unique_ptr<Base> base_ptr = std::make_unique<Derived>();
base_ptr->foo(); // 多态调用,输出 "Derived::foo"
return 0;
}
注意:基类析构函数必须为虚函数,否则会导致未定义行为。
四、高级应用场景
4.1 作为函数返回值
unique_ptr
可作为函数返回值,自动转移所有权:
std::unique_ptr<MyClass> create_object() {
return std::make_unique<MyClass>(); // 自动移动返回
}
int main() {
auto obj = create_object(); // 接收返回的 unique_ptr
obj->do_something();
return 0;
}
4.2 在容器中使用
unique_ptr
可存储在支持移动语义的容器中(如 std::vector
):
#include <vector>
int main() {
std::vector<std::unique_ptr<MyClass>> objects;
// 添加元素(需要使用 std::move)
objects.push_back(std::make_unique<MyClass>());
objects.emplace_back(new MyClass()); // 更高效
// 遍历容器
for (const auto& ptr : objects) {
ptr->do_something();
}
return 0;
}
4.3 实现 PImpl 惯用法
unique_ptr
非常适合实现 PImpl(Pointer to Implementation)惯用法,隐藏实现细节:
// 头文件 MyClass.h
class MyClass {
private:
// 前向声明实现类
class Impl;
std::unique_ptr<Impl> pimpl; // 指向实现的 unique_ptr
public:
MyClass();
~MyClass(); // 需要在 cpp 文件中定义,因为 Impl 在此处不完整
void do_something();
};
// 实现文件 MyClass.cpp
class MyClass::Impl {
public:
void do_something() {
// 实际实现
}
};
MyClass::MyClass() : pimpl(std::make_unique<Impl>()) {}
MyClass::~MyClass() = default; // 此时 Impl 已完整定义
void MyClass::do_something() { pimpl->do_something(); }
五、注意事项与最佳实践
5.1 避免的操作
不要将原始指针交给多个 unique_ptr
// 错误示例:多个 unique_ptr 管理同一资源 int* raw_ptr = new int(10); std::unique_ptr<int> ptr1(raw_ptr); std::unique_ptr<int> ptr2(raw_ptr); // 严重错误!双重释放
不要将 unique_ptr 管理的指针手动释放
// 错误示例:手动释放导致二次释放 auto ptr = std::make_unique<int>(10); delete ptr.get(); // 错误!unique_ptr 析构时会再次释放
避免在 C 风格函数中使用 unique_ptr
// 错误示例:C 函数不知道 unique_ptr 的存在 void c_function(int* p) { /* ... */ } auto ptr = std::make_unique<int>(10); c_function(ptr.release()); // 正确:释放所有权 // c_function(ptr.get()); // 危险:如果函数存储了指针会导致问题
5.2 最佳实践
优先使用 std::make_unique
- 更安全:避免资源泄漏(如在构造函数抛出异常时)
- 更高效:减少一次指针拷贝
- 语法更简洁
auto ptr1 = std::make_unique<MyClass>(); // 推荐 auto ptr2 = std::unique_ptr<MyClass>(new MyClass()); // 不推荐
使用移动语义转移所有权
auto ptr1 = std::make_unique<MyClass>(); auto ptr2 = std::move(ptr1); // 正确:转移所有权 // auto ptr3 = ptr2; // 错误:禁止拷贝
正确处理数组
- 使用
unique_ptr<T[]>
特化版本 - 避免使用
unique_ptr<T>
管理数组
- 使用
自定义删除器注意事项
- 当使用函数指针作为删除器时,
unique_ptr
体积会增大 - 优先使用函数对象或 lambda 作为删除器
- 当使用函数指针作为删除器时,
六、总结
std::unique_ptr
是 C++ 中管理独占资源的首选智能指针,通过独占所有权和移动语义,在保证性能的同时有效避免内存泄漏。其核心特点包括:
- 独占性:同一时间只有一个
unique_ptr
管理资源 - 轻量级:大小与原始指针相同,无额外性能开销
- 安全性:自动释放资源,提供异常安全保证
- 灵活性:支持自定义删除器,适应不同资源管理需求
在实际开发中,应优先考虑使用 unique_ptr
而非原始指针,只有在需要共享所有权时才考虑 std::shared_ptr
。