目录

一、一个手动管理的痛点

二、RAII 核心思想

三、auto_ptr:C++98 的尝试与缺陷

auto_ptr 的核心缺陷

四、unique_ptr:真正的独占式智能指针

基本用法

常用成员函数

五、unique_ptr 与数组

六、自定义删除器

七、make_unique(C++14)

八、完整例子:对比手动管理 vs unique_ptr

九、unique_ptr 的常见误区

误区1:试图拷贝 unique_ptr

误区2:用 get() 获取裸指针后 delete

误区3:release() 后忘记释放内存

误区4:用裸指针重新赋值 unique_ptr

十、这一篇的收获


一、一个手动管理的痛点

cpp

void riskyFunction() {
    int* p = new int[1000];
    // ... 做一些操作 ...
    if (something_wrong) {
        return;   // 忘记 delete[] → 内存泄漏!
    }
    delete[] p;
}

问题在于:delete 很容易被遗忘,特别是在有多个返回路径或异常抛出的情况下。

RAII 解决方案:让对象自动管理资源。

cpp

void safeFunction() {
    vector<int> v(1000);   // 内存由 vector 管理
    // ... 无论怎么返回,v 的析构函数都会释放内存
}

这就是 RAII 的精髓:将资源的生命周期与对象的生命周期绑定


二、RAII 核心思想

RAII(Resource Acquisition Is Initialization)的含义:

  1. 获取资源即初始化:在构造函数中获取资源(分配内存、打开文件、加锁等)

  2. 释放资源即析构:在析构函数中释放资源(释放内存、关闭文件、解锁等)

  3. 对象生命周期结束,资源自动释放

cpp

class FileHandle {
    FILE* file;
public:
    FileHandle(const char* filename) {
        file = fopen(filename, "r");
    }
    ~FileHandle() {
        if (file) fclose(file);   // 自动关闭
    }
    // 禁止拷贝(后面解释)
};

只要 FileHandle 对象存在,文件就是打开的;对象销毁时,文件自动关闭。不再需要手动调用 fclose


三、auto_ptr:C++98 的尝试与缺陷

C++98 引入了 auto_ptr,试图实现 RAII 式的动态内存管理。

cpp

#include <memory>  // auto_ptr 在这里
auto_ptr<int> p1(new int(42));
auto_ptr<int> p2 = p1;   // 看起来像拷贝,但实际是转移所有权!
// 此时 p1 已经变成 nullptr

auto_ptr 的核心缺陷

auto_ptr 的拷贝构造函数和赋值运算符会转移所有权(被复制的指针变成 nullptr)。这违反了正常的拷贝语义:

cpp

auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2(p1);   // p1 被置空
cout << *p1;               // 崩溃!p1 已经是空指针

更严重的问题:auto_ptr 不能用于 STL 容器,因为容器要求元素有正常的拷贝行为。

cpp

vector<auto_ptr<int>> vec;  // 能编译,但绝对不要这样做!
vec.push_back(auto_ptr<int>(new int(1)));
vec.push_back(auto_ptr<int>(new int(2)));
// 容器内部拷贝时,所有权转移,导致原始指针变空,非常容易出错

结论auto_ptr 在 C++11 中被正式废弃(deprecated),C++17 中已移除。永远不要用它。


四、unique_ptr:真正的独占式智能指针

unique_ptr 是 auto_ptr 的现代替代品,核心特点:

  • 独占所有权:一个资源只能被一个 unique_ptr 拥有

  • 禁止拷贝:拷贝构造函数和拷贝赋值被 delete

  • 支持移动:通过移动语义转移所有权

  • 零开销:与裸指针大小相同(通常 8 字节),没有额外虚函数开销

基本用法

cpp

#include <memory>
using namespace std;

unique_ptr<int> p1(new int(42));
unique_ptr<int> p2 = move(p1);   // 转移所有权,p1 变为空
if (!p1) cout << "p1 为空" << endl;
cout << *p2 << endl;   // 42

// ❌ 编译错误:不能拷贝
// unique_ptr<int> p3 = p1;

常用成员函数

函数 作用
get() 返回裸指针(不转移所有权)
release() 释放所有权,返回裸指针(不 delete)
reset() 释放当前对象并(可选)接管新指针
reset(nullptr) 释放当前对象,置空
operator bool() 判断是否为空

cpp

unique_ptr<int> p(new int(100));

int* raw = p.release();   // p 放弃所有权,变为 nullptr
delete raw;               // 需要手动释放

p.reset(new int(200));    // 释放旧对象,接管新对象
p.reset();                // 释放对象,置空

if (p) { cout << "p 非空" << endl; }

五、unique_ptr 与数组

unique_ptr 支持数组版本,使用 T[] 模板参数:

cpp

unique_ptr<int[]> arr(new int[10]);
arr[0] = 42;   // 可以用下标访问

// 不需要手动 delete[]
// 离开作用域时自动调用 delete[]

注意unique_ptr<T> 用 deleteunique_ptr<T[]> 用 delete[],编译器会自动选择正确的释放方式。


六、自定义删除器

有时资源不是 new 分配的(如 fopenmallocsocket),需要自定义释放方式。

cpp

#include <memory>
#include <cstdio>
using namespace std;

// 自定义删除器:函数对象
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            fclose(f);
            cout << "文件已关闭" << endl;
        }
    }
};

int main() {
    unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "r"));
    // 自动调用 fclose,不需要手动关闭
    return 0;
}

使用 lambda 作为删除器(更简洁):

cpp

auto deleter = [](FILE* f) { if (f) fclose(f); };
unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "r"), deleter);

七、make_unique(C++14)

C++14 提供了 make_unique,更安全、更高效地创建 unique_ptr

cpp

// C++11 方式
unique_ptr<int> p1(new int(42));

// C++14 方式(推荐)
auto p2 = make_unique<int>(42);
auto p3 = make_unique<int[]>(10);  // 数组版本

优势

  1. 异常安全(避免 new 和 unique_ptr 构造之间的空隙)

  2. 代码更简洁

  3. 减少重复类型名


八、完整例子:对比手动管理 vs unique_ptr

cpp

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Resource {
    int id;
public:
    Resource(int i) : id(i) {
        cout << "Resource " << id << " 已获取" << endl;
    }
    ~Resource() {
        cout << "Resource " << id << " 已释放" << endl;
    }
    void use() {
        cout << "使用 Resource " << id << endl;
    }
};

// 手动管理:容易出错
void manualManagement() {
    cout << "=== 手动管理 ===" << endl;
    Resource* r = new Resource(1);
    r->use();
    // 忘记 delete → 内存泄漏!
    // 即使记得,如果中间抛异常也会泄漏
}

// unique_ptr 管理:自动释放
void uniquePtrManagement() {
    cout << "\n=== unique_ptr 管理 ===" << endl;
    auto r = make_unique<Resource>(2);
    r->use();
    // 函数结束,自动释放
}

// 所有权转移示例
void ownershipTransfer() {
    cout << "\n=== 所有权转移 ===" << endl;
    unique_ptr<Resource> p1 = make_unique<Resource>(3);
    cout << "p1 拥有资源" << endl;
    
    unique_ptr<Resource> p2 = move(p1);  // 转移所有权
    cout << "转移后: p1 = " << (p1 ? "非空" : "空") << endl;
    cout << "p2 拥有资源" << endl;
    p2->use();
    // p2 释放资源
}

// 放入容器
void containerUsage() {
    cout << "\n=== 放入容器 ===" << endl;
    vector<unique_ptr<Resource>> vec;
    vec.push_back(make_unique<Resource>(4));
    vec.push_back(make_unique<Resource>(5));
    vec.push_back(make_unique<Resource>(6));
    
    for (auto& p : vec) {
        p->use();
    }
    // 容器销毁时,所有 unique_ptr 自动释放资源
}

int main() {
    // manualManagement();    // 会泄漏,不运行
    uniquePtrManagement();
    ownershipTransfer();
    containerUsage();
    return 0;
}

输出:

text

=== unique_ptr 管理 ===
Resource 2 已获取
使用 Resource 2
Resource 2 已释放

=== 所有权转移 ===
Resource 3 已获取
p1 拥有资源
转移后: p1 = 空
p2 拥有资源
使用 Resource 3
Resource 3 已释放

=== 放入容器 ===
Resource 4 已获取
Resource 5 已获取
Resource 6 已获取
使用 Resource 4
使用 Resource 5
使用 Resource 6
Resource 6 已释放
Resource 5 已释放
Resource 4 已释放

九、unique_ptr 的常见误区

误区1:试图拷贝 unique_ptr

cpp

unique_ptr<int> p1(new int(5));
unique_ptr<int> p2 = p1;   // ❌ 编译错误

误区2:用 get() 获取裸指针后 delete

cpp

unique_ptr<int> p(new int(5));
int* raw = p.get();
delete raw;   // ❌ 重复释放,p 析构时会再次 delete

误区3:release() 后忘记释放内存

cpp

auto p = make_unique<int>(42);
int* raw = p.release();   // p 放弃所有权
// 没有 delete raw → 内存泄漏

误区4:用裸指针重新赋值 unique_ptr

cpp

unique_ptr<int> p(new int(5));
p = new int(6);   // ❌ 编译错误,不支持隐式转换
p.reset(new int(6));  // ✅ 正确

十、这一篇的收获

你现在应该理解:

  • RAII:构造函数获取资源,析构函数释放资源,对象生命周期绑定资源

  • auto_ptr 缺陷:拷贝转移所有权,违反直觉,不能用于容器,已被废弃

  • unique_ptr:独占所有权,禁止拷贝,支持移动,零开销

  • 常用操作make_uniqueresetreleaseget、移动语义转移所有权

  • 数组支持unique_ptr<T[]> 自动调用 delete[]

  • 自定义删除器:可管理 fopenmalloc 等非 new 资源

💡 小作业:写一个 unique_ptr<FILE, CustomDeleter> 管理打开的文件,验证函数返回或异常时文件自动关闭。然后尝试写一段代码演示 move 转移所有权后原指针变为空。


下一篇预告:第31篇《智能指针(二):shared_ptr与weak_ptr——循环引用破解》——多个对象需要共享同一资源时用 shared_ptr。但两个 shared_ptr 互相引用会导致资源永远无法释放,weak_ptr 正是破解循环引用的利器。下篇详解。

Logo

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

更多推荐