引言

C++ 程序员最头疼的问题是什么?不是语法、不是模板、不是多线程——而是内存管理

void func() {
    int* p = new int(42);
    // ... 复杂逻辑 ...
    if (某个条件) return;     // 忘记 delete p → 内存泄漏
    // ... 更多逻辑 ...
    delete p;                 // 如果中间抛异常,永远执行不到这里
}

手动 new/delete 有三大痛点:

  1. 忘记 delete → 内存泄漏

  2. 提前 return 或抛异常 → delete 被跳过

  3. 多次 delete → 程序崩溃

智能指针就是来彻底解决这些问题的。从 C++11 开始,标准库提供了三种智能指针:unique_ptrshared_ptrweak_ptr。它们本质上是用 RAII 包装原始指针,让指针在离开作用域时自动释放

本文作为智能指针系列第一篇,聚焦于独占所有权的 unique_ptr

第一部分:为什么需要 unique_ptr

一、原始指针的困境

#include <iostream>
using namespace std;

class Resource {
public:
    Resource()  { cout << "资源获取" << endl; }
    ~Resource() { cout << "资源释放" << endl; }
    void work() { cout << "工作中..." << endl; }
};

void badExample() {
    Resource* p = new Resource();
    
    // 场景1:提前 return,忘记 delete
    if (rand() % 2 == 0) {
        return;  // 内存泄漏!p 没有被释放
    }
    
    // 场景2:抛异常,delete 被跳过
    p->work();
    throw runtime_error("出错了");  // 异常抛出 → delete 不执行
    
    delete p;  // 只有正常情况才能执行到这里
}

三个场景,只有一种情况能正确释放内存。这就是为什么大型 C++ 项目普遍禁止裸 new/delete

二、unique_ptr 的解决方案

#include <memory>

void goodExample() {
    unique_ptr<Resource> p = make_unique<Resource>();
    
    // 场景1:提前 return → p 的析构函数自动调用 → 资源释放 ✅
    if (rand() % 2 == 0) return;
    
    // 场景2:抛异常 → p 的析构函数仍然会被调用 → 资源释放 ✅
    p->work();
    throw runtime_error("出错了");
    
    // 不需要 delete!离开作用域自动释放 ✅
}

第二部分:unique_ptr 的核心特性

一、独占所有权

unique_ptr 独占它指向的对象,不能被拷贝,只能被移动

unique_ptr<int> p1 = make_unique<int>(42);
unique_ptr<int> p2 = p1;             // ❌ 编译错误!不能拷贝
unique_ptr<int> p3 = std::move(p1);  // ✅ 移动后 p1 变成 nullptr

二、三种创建方式

#include <memory>
using namespace std;

int main() {
    // 方式1:make_unique(C++14,最推荐)
    auto p1 = make_unique<int>(42);

    // 方式2:用 new(C++11 兼容)
    unique_ptr<int> p2(new int(42));

    // 方式3:从已有 unique_ptr 移动
    unique_ptr<int> p3 = std::move(p1);  // p1 变成 nullptr

    return 0;
    // p2 和 p3 自动释放,p1 已经为空
}

优先用 make_unique,原因:

  • 代码更简洁(不需要写两次类型名)

  • 异常安全

  • C++14 起可用

三、访问被管理的对象

unique_ptr<string> p = make_unique<string>("hello");

// 像指针一样使用
cout << *p << endl;      // hello(解引用)
cout << p->size() << endl;  // 5(访问成员)
cout << p.get() << endl;    // 获取原始指针(谨慎使用)
操作 含义
*p 解引用,获取对象的引用
p-> 访问对象成员
p.get() 获取原始指针(不转移所有权)
p == nullptr 判断是否为空
if(p) 判断是否为空

四、释放与重置

unique_ptr<int> p = make_unique<int>(42);

p.reset();              // 释放资源,p 变成 nullptr
p.reset(new int(100));  // 释放旧资源,接管新资源

int* raw = p.release();  // 放弃所有权,返回原始指针
// p 变成 nullptr,raw 需要手动 delete!
delete raw;
操作 含义
p.reset() 释放资源,p 为空
p.reset(newPtr) 释放旧资源,接管新资源
p.release() 放弃所有权,返回裸指针(需要手动 delete

第三部分:unique_ptr 的常用场景

一、作为函数返回值(最安全)

unique_ptr<Resource> createResource() {
    return make_unique<Resource>();  // 返回临时对象 → 自动移动
}

int main() {
    auto p = createResource();  // 接收移动
    p->work();
}

二、放入容器

vector<unique_ptr<Resource>> resources;

resources.push_back(make_unique<Resource>());  // 移动进去
resources.emplace_back(make_unique<Resource>());

// 遍历
for (const auto& r : resources) {
    r->work();
}

// 容器销毁时,所有 unique_ptr 自动释放 → 不用手动删!

三、工厂模式

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() = default;
};

class ProductA : public Product {
public:
    void use() override { cout << "使用产品A" << endl; }
};

class ProductB : public Product {
public:
    void use() override { cout << "使用产品B" << endl; }
};

// 工厂函数返回 unique_ptr<基类>
unique_ptr<Product> createProduct(char type) {
    if (type == 'A') return make_unique<ProductA>();
    if (type == 'B') return make_unique<ProductB>();
    return nullptr;
}

int main() {
    auto p = createProduct('A');
    p->use();  // 使用产品A
    // 多态析构!需要基类有虚析构函数
}

第四部分:自定义删除器

一、什么时候需要自定义删除器

默认的删除器是 delete,但有些资源不是用 new 分配的:

// 文件句柄
FILE* f = fopen("test.txt", "r");
// 用完后需要 fclose(f),不是 delete f

// Socket
int sockfd = socket(...);
// 用完后需要 close(sockfd),不是 delete sockfd

// 自定义内存池
void* mem = my_pool_alloc(1024);
// 用完后需要 my_pool_free(mem),不是 delete mem

二、自定义删除器示例

#include <cstdio>

// 文件句柄管理
auto fileDeleter = [](FILE* f) {
    if (f) {
        cout << "关闭文件" << endl;
        fclose(f);
    }
};

int main() {
    FILE* f = fopen("test.txt", "w");
    unique_ptr<FILE, decltype(fileDeleter)> filePtr(f, fileDeleter);
    
    fprintf(filePtr.get(), "hello\n");
    // 离开作用域 → 自动调用 fclose!
}
// Socket 管理
auto socketDeleter = [](int* fd) {
    if (fd && *fd != -1) {
        cout << "关闭 socket" << endl;
        close(*fd);
    }
    delete fd;
};

int main() {
    int* sockfd = new int(socket(AF_INET, SOCK_STREAM, 0));
    unique_ptr<int, decltype(socketDeleter)> sockPtr(sockfd, socketDeleter);
    // 离开作用域 → 自动关闭 socket!
}

三、unique_ptr 管理数组

// 管理动态数组(C++14 起)
auto arr = make_unique<int[]>(10);  // 10 个 int

arr[0] = 1;
arr[1] = 2;
cout << arr[3] << endl;

// 离开作用域 → 自动调用 delete[]
// 注意:shared_ptr 也支持数组(C++17 起)

第五部分:unique_ptr vs 原始指针

对比项 原始指针 unique_ptr
内存释放 手动 delete 自动
所有权 不明确 独占,明确
拷贝 浅拷贝(可能出问题) 禁止拷贝
异常安全 ❌ 可能泄漏 ✅ 保证释放
性能开销 零开销(和原始指针一样)
大小 8 字节 8 字节(无自定义删除器时)

unique_ptr 是零开销抽象:在编译优化后,unique_ptr 的大小和性能与原始指针完全一样。


第六部分:常见错误

// ❌ 错误1:试图拷贝 unique_ptr
unique_ptr<int> p1 = make_unique<int>(42);
unique_ptr<int> p2 = p1;  // 编译错误!

// ❌ 错误2:把同一个裸指针给多个 unique_ptr
int* raw = new int(42);
unique_ptr<int> p3(raw);
unique_ptr<int> p4(raw);  // p3 和 p4 都会尝试 delete raw → 双重释放!

// ❌ 错误3:忘记 release 后要手动 delete
unique_ptr<int> p5 = make_unique<int>(42);
int* raw2 = p5.release();
// raw2 需要手动 delete,否则泄漏
delete raw2;

// ✅ 正确:用 get 只是临时查看,不转移所有权
unique_ptr<int> p6 = make_unique<int>(42);
int* raw3 = p6.get();  // 只是查看,所有权还是 p6 的
func(raw3);            // 可以传给只读函数

总结

一、核心要点

特性 说明
所有权 独占,不可拷贝,只可移动
创建 make_unique<T>(args)
释放 自动(离开作用域)、reset()release()
访问 *pp->p.get()
自定义删除器 非 new 分配的资源(FILE*、socket 等)
性能 零开销(和原始指针相同)
头文件 <memory>

二、一句话记忆

unique_ptr 独占对象所有权,禁止拷贝只允许移动。用 make_unique 创建,离开作用域自动释放。需要管理非 new 资源时用自定义删除器。零开销,和原始指针一样快。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐