C++11 unique_ptr 原理与详细教程

发布于:2025-07-04 ⋅ 阅读:(17) ⋅ 点赞:(0)

之前使用智能指针一直是一知半解,现在有时间了,尝试从源码的角度来理解它。

一、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 关键技术点解析

  1. 删除器设计

    • 默认使用 DefaultDeleter,对单对象使用 delete,对数组使用 delete[]
    • 支持自定义删除器,满足特殊资源释放需求(如文件、网络连接)
  2. 移动语义实现

    • 通过右值引用(&&)实现移动构造和移动赋值
    • 转移所有权后将源指针置空,确保资源唯一管理
  3. 禁止拷贝

    • 通过 = 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 避免的操作

  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); // 严重错误!双重释放
    
  2. 不要将 unique_ptr 管理的指针手动释放

    // 错误示例:手动释放导致二次释放
    auto ptr = std::make_unique<int>(10);
    delete ptr.get(); // 错误!unique_ptr 析构时会再次释放
    
  3. 避免在 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 最佳实践

  1. 优先使用 std::make_unique

    • 更安全:避免资源泄漏(如在构造函数抛出异常时)
    • 更高效:减少一次指针拷贝
    • 语法更简洁
    auto ptr1 = std::make_unique<MyClass>(); // 推荐
    auto ptr2 = std::unique_ptr<MyClass>(new MyClass()); // 不推荐
    
  2. 使用移动语义转移所有权

    auto ptr1 = std::make_unique<MyClass>();
    auto ptr2 = std::move(ptr1); // 正确:转移所有权
    // auto ptr3 = ptr2; // 错误:禁止拷贝
    
  3. 正确处理数组

    • 使用 unique_ptr<T[]> 特化版本
    • 避免使用 unique_ptr<T> 管理数组
  4. 自定义删除器注意事项

    • 当使用函数指针作为删除器时,unique_ptr 体积会增大
    • 优先使用函数对象或 lambda 作为删除器

六、总结

std::unique_ptr 是 C++ 中管理独占资源的首选智能指针,通过独占所有权和移动语义,在保证性能的同时有效避免内存泄漏。其核心特点包括:

  • 独占性:同一时间只有一个 unique_ptr 管理资源
  • 轻量级:大小与原始指针相同,无额外性能开销
  • 安全性:自动释放资源,提供异常安全保证
  • 灵活性:支持自定义删除器,适应不同资源管理需求

在实际开发中,应优先考虑使用 unique_ptr 而非原始指针,只有在需要共享所有权时才考虑 std::shared_ptr