目录

一、智能指针概述

1. 为什么需要智能指针?

2. 智能指针类型

二、unique_ptr 详解

1. 基本使用

2. unique_ptr 的特性

3. unique_ptr 的常用操作

4. unique_ptr 与数组

三、shared_ptr 详解

1. 基本用法

2. 关键特性

3. 陷阱:循环引用

4. 自定义删除器

四、weak_ptr 详解

1. 解决循环引用

2. 核心方法

五、智能指针的使用原则

总结


引言

智能指针是 C++ 标准库提供的RAII风格工具,核心作用是自动管理动态内存,避免手动调用delete导致的内存泄漏、野指针、重复释放等问题。它本质是封装了原始指针的类模板,利用析构函数的自动调用特性,在智能指针对象生命周期结束时(如出作用域、被销毁)自动释放所管理的内存。让我全面深入地介绍 C++ 的智能指针。

RAII(资源获取即初始化)是 C++ 中一种非常重要的编程惯用法(idiom)。它的核心思想是:

将资源的生命周期与对象的生命周期绑定。

具体来说:

  • 获取资源:在对象的构造函数中分配资源(如:动态内存、文件句柄、锁、网络连接等)。

  • 释放资源:在对象的析构函数中释放资源。

  • 自动管理:当对象离开作用域时,C++ 会保证其析构函数被自动调用,从而确保资源被正确释放 —— 即使发生异常也是如此。

为什么需要 RAII?

在没有 RAII 的传统 C 风格代码中,程序员需要手动配对资源获取与释放(如 malloc/freefopen/fclose)。这极易导致:

  • 资源泄漏:忘记调用释放函数。

  • 重复释放:逻辑错误导致多次释放。

  • 异常不安全:如果函数在获取资源后、释放资源前抛出异常,释放代码永远不会被执行。

RAII 通过构造函数+析构函数+栈展开机制,从根本上解决了这些问题。

一、智能指针概述

1. 为什么需要智能指针?

传统指针的问题,一些操作可能抛出异常或提前返回(比如忘记 delete,内存泄漏!)

// 使用智能指针
#include <memory>

void smartPointer() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    // 不需要手动 delete,离开作用域自动释放
    // 即使抛出异常,也能正确释放
}

2. 智能指针类型

C++11 及之后标准中,常用的智能指针有 3 种:std::unique_ptrstd::shared_ptrstd::weak_ptr,均定义在<memory>头文件中。

智能指针 所有权 引用计数 可拷贝 适用场景
std::unique_ptr 独占 独占所有权,轻量级
std::shared_ptr 共享 多个所有者共享资源
std::weak_ptr 弱引用 无(配合shared_ptr) 解决循环引用,观察者

二、unique_ptr 详解

unique_ptr独占所有权的智能指针:同一时间,只有一个unique_ptr能指向某块内存,不允许拷贝(C++11 仅支持移动),销毁时释放内存。

1. 基本使用

  • 方式1:直接new
  • 方式2:make_unique (C++14)
  • 方式3:构造函数
#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource released\n"; }
    void doSomething() { std::cout << "Doing something\n"; }
};

// 创建 unique_ptr
std::unique_ptr<Resource> ptr1(new Resource());  // 方式1:直接new
auto ptr2 = std::make_unique<Resource>();        // 方式2:make_unique (C++14)
auto ptr3 = std::unique_ptr<Resource>(new Resource()); // 方式3:构造函数

// C++14 之前需要自己实现 make_unique
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

2. unique_ptr 的特性

  1. 独占所有权,不能拷贝
  2. 可以作为函数返回值
  3. 可以放入容器
  4. 自定义删除器
// 1. 独占所有权,不能拷贝
auto ptr = std::make_unique<int>(42);
// auto ptr2 = ptr;  // 错误:不能拷贝
auto ptr2 = std::move(ptr);  // 正确:转移所有权
// ptr 现在为空

// 2. 可以作为函数返回值
std::unique_ptr<Resource> createResource() {
    return std::make_unique<Resource>();  // 移动语义自动应用
}

// 3. 可以放入容器
std::vector<std::unique_ptr<Resource>> resources;
resources.push_back(std::make_unique<Resource>());
resources.push_back(std::make_unique<Resource>());

// 遍历
for (auto& res : resources) {
    res->doSomething();
}

// 4. 自定义删除器
auto customDeleter = [](FILE* f) {
    std::cout << "Closing file\n";
    if (f) fclose(f);
};

std::unique_ptr<FILE, decltype(customDeleter)> 
    filePtr(fopen("test.txt", "w"), customDeleter);

3. unique_ptr 的常用操作

auto ptr = std::make_unique<int>(42);

// 获取原始指针
int* rawPtr = ptr.get();
std::cout << *rawPtr << std::endl;  // 42

// 释放所有权但不删除对象
int* releasedPtr = ptr.release();  // ptr 变为空
delete releasedPtr;  // 需要手动删除

// 重置指针
ptr.reset();  // 删除原对象,ptr 变为空
ptr.reset(new int(100));  // 删除原对象,指向新对象

// 检查是否为空
if (ptr) {
    std::cout << "ptr is not null\n";
}

// 交换
auto ptr1 = std::make_unique<int>(1);
auto ptr2 = std::make_unique<int>(2);
ptr1.swap(ptr2);  // ptr1 现在指向 2,ptr2 指向 1

4. unique_ptr 与数组

// C++11 支持数组
std::unique_ptr<int[]> arrPtr(new int[10]);

// C++14 make_unique 也支持数组
auto arrPtr2 = std::make_unique<int[]>(10);

// 使用
for (int i = 0; i < 10; ++i) {
    arrPtr2[i] = i * i;
}

// 不需要手动 delete[]

三、shared_ptr 详解

shared_ptr共享所有权的智能指针:多个shared_ptr可以指向同一块内存,内部通过引用计数(refcount)管理 —— 当最后一个指向该内存的shared_ptr销毁时,才释放内存。

1. 基本用法

#include <iostream>
#include <memory>

//Test是一个简单演示的类

int main() {
    // 1. 创建shared_ptr
    std::shared_ptr<Test> ptr1(new Test(10));
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出1

    // 2. 拷贝(共享所有权,引用计数+1)
    std::shared_ptr<Test> ptr2 = ptr1;
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出2

    // 3. 重置(引用计数-1,若为0则释放内存)
    ptr1.reset();
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出1

    // 4. 推荐创建方式:std::make_shared(更高效,避免内存碎片)
    std::shared_ptr<Test> ptr3 = std::make_shared<Test>(20);

    return 0; // ptr2、ptr3销毁,引用计数归0,Test(10)、Test(20)析构
}

2. 关键特性

  • 引用计数:每次拷贝shared_ptr,计数 + 1;析构 / 重置,计数 - 1;
  • 线程安全
    • 引用计数的修改是原子操作(多线程拷贝 / 销毁shared_ptr安全);
    • 但指向的对象本身不是线程安全的(多线程修改对象需加锁);
  • 不支持数组(C++17 前):若管理数组需自定义删除器,如std::shared_ptr<int> arr(new int[5], [](int* p) { delete[] p; });
  • 常用场景:管理共享资源(如多个对象共享同一个动态资源)。

3. 陷阱:循环引用

shared_ptr的最大问题是循环引用—— 两个shared_ptr互相指向对方,导致引用计数无法归 0,内存泄漏。示例:

class A;
class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // A引用B
    b->a_ptr = a; // B引用A
    // 函数结束时,a和b的引用计数都是2,无法析构,内存泄漏
    return 0;
}

解决方案:用std::weak_ptr打破循环引用。

4. 自定义删除器

// 文件资源管理
auto fileDeleter = [](FILE* file) {
    std::cout << "Closing file\n";
    if (file) {
        fclose(file);
    }
};

std::shared_ptr<FILE> filePtr(fopen("test.txt", "w"), fileDeleter);

// 数组资源
auto arrayDeleter = [](int* p) {
    std::cout << "Deleting array\n";
    delete[] p;
};

std::shared_ptr<int> arrPtr(new int[100], arrayDeleter);

// 使用默认的 default_delete
std::shared_ptr<int> arrPtr2(new int[100], std::default_delete<int[]>());

四、weak_ptr 详解

weak_ptr是为解决shared_ptr循环引用设计的弱引用指针:

  • 不拥有内存所有权,仅观察shared_ptr管理的内存;
  • 不增加引用计数,不影响内存的释放;
  • 可以从shared_ptr构造,也可以转换为shared_ptr(通过lock())。

1. 解决循环引用

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 改为weak_ptr
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为weak_ptr
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a;
    // weak_ptr不增加引用计数,a和b的引用计数仍为1,函数结束时正常析构
    return 0;
}

2. 核心方法

  • lock():将weak_ptr转换为std::shared_ptr,若原内存已释放则返回空shared_ptr(安全访问对象的关键);
  • expired():判断weak_ptr指向的内存是否已释放(返回true表示已释放);
  • use_count():获取对应的shared_ptr的引用计数(仅观察,不修改)。

示例:安全访问对象

std::weak_ptr<Test> wp;
{
    std::shared_ptr<Test> sp = std::make_shared<Test>(10);
    wp = sp;
    std::cout << "expired: " << wp.expired() << std::endl; // false

    // 转换为shared_ptr并访问
    if (auto temp_sp = wp.lock()) {
        temp_sp->show(); // 安全访问
    }
} // sp销毁,内存释放
std::cout << "expired: " << wp.expired() << std::endl; // true
if (auto temp_sp = wp.lock()) {
    temp_sp->show(); // 不会执行,temp_sp为空
}

五、智能指针的使用原则

  1. 优先使用std::unique_ptr:独占场景下效率最高,语义清晰;
  2. 避免用智能指针管理非动态内存(如栈上对象):std::unique_ptr<int> ptr(&x);(x 是栈变量,析构时会重复释放,崩溃);
  3. 避免裸指针与智能指针混用:裸指针可能被智能指针自动释放,导致野指针;
  4. std::make_shared优先于直接构造:std::make_shared<Test>(10)std::shared_ptr<Test>(new Test(10)) 更高效(一次内存分配),且更安全(避免异常导致内存泄漏);
  5. 自定义删除器:若管理的资源不是new分配的(如malloc、文件句柄),需自定义删除器;

总结

  1. 核心作用:智能指针通过 RAII 自动管理动态内存,避免内存泄漏,核心析构函数自动释放资源;
  2. 类型选择
    • unique_ptr:独占所有权,轻量级,优先使用;
    • shared_ptr:共享所有权,通过引用计数管理,注意循环引用;
    • weak_ptr:弱引用,解决shared_ptr循环引用,不影响内存释放;
  3. 最佳实践:优先用std::make_shared创建shared_ptr,避免裸指针与智能指针混用,不管理栈内存。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐