目录

一、智能指针的核心设计思想:RAII

二、三种核心智能指针详解

1. std::unique_ptr:独占所有权的智能指针

(1)核心特性

(2)基本用法

(3)适用场景

2. std::shared_ptr:共享所有权的智能指针

(1)核心特性

(2)基本用法

(3)循环引用问题与解决

3. std::weak_ptr:弱引用智能指针

(1)核心特性

(2)基本用法(解决循环引用)

(3)访问weak_ptr观察的对象

三、智能指针的使用准则与注意事项

四、三种智能指针的对比总结

五、总结

在 C++ 中,智能指针(Smart Pointer) 是封装了裸指针(raw pointer)的模板类,通过RAII(资源获取即初始化) 机制自动管理动态内存的生命周期 —— 当智能指针离开作用域时,会自动释放其指向的动态内存,从而避免内存泄漏、野指针等问题。C++11 及后续标准引入了三种核心智能指针:std::unique_ptrstd::shared_ptr 和 std::weak_ptr,它们各自适用于不同的 ownership(所有权)场景。

一、智能指针的核心设计思想:RAII

RAII 是智能指针的底层逻辑:将资源(如动态内存)的生命周期与对象(智能指针)的生命周期绑定。具体来说:

  • 智能指针在构造时获取资源(如通过new分配内存,并存储指向该内存的裸指针)。
  • 智能指针在析构时自动释放资源(调用deletedelete[])。
  • 由于对象的析构函数会被编译器自动调用(当对象离开作用域、被销毁时),因此无需手动管理内存释放。

二、三种核心智能指针详解

1. std::unique_ptr:独占所有权的智能指针

std::unique_ptr 是独占式智能指针:同一时间内,一个对象的所有权只能被一个unique_ptr拥有。当unique_ptr被销毁时,其指向的对象也会被自动释放。

(1)核心特性
  • 独占性:不允许复制(拷贝构造和拷贝赋值被禁用),只能通过移动语义std::move)转移所有权。
  • 轻量高效:大小与裸指针相同(通常为 8 字节),无额外性能开销。
  • 支持数组:通过std::unique_ptr<T[]>专门管理动态数组(自动调用delete[])。
(2)基本用法
#include <memory>  // 智能指针所在头文件
#include <iostream>

class MyClass {
public:
    MyClass(int id) : id_(id) {
        std::cout << "MyClass(" << id_ << ") 构造" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass(" << id_ << ") 析构" << std::endl;
    }
    int getId() const { return id_; }
private:
    int id_;
};

int main() {
    // 1. 初始化:推荐用std::make_unique(C++14引入),更安全高效
    std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(1);  // 管理MyClass(1)
    
    // 2. 访问对象:通过->或*操作符,与裸指针类似
    std::cout << "对象ID:" << uptr1->getId() << std::endl;  // 输出:对象ID:1
    
    // 3. 转移所有权(只能通过std::move)
    std::unique_ptr<MyClass> uptr2 = std::move(uptr1);  // uptr1失去所有权,变为nullptr
    if (uptr1 == nullptr) {
        std::cout << "uptr1已失去所有权" << std::endl;
    }
    
    // 4. 管理动态数组(需指定T[])
    std::unique_ptr<MyClass[]> arr_uptr = std::make_unique<MyClass[]>(2);  // 构造2个对象
    // 数组元素通过[]访问
    std::cout << "数组第0个ID:" << arr_uptr[0].getId() << std::endl;  // 输出默认构造的ID(假设为0)
    
    // 5. 手动释放所有权(通过reset())
    uptr2.reset();  // 主动释放对象,调用MyClass(1)的析构函数
    
    return 0;  // 函数结束时,arr_uptr析构,自动释放数组(调用2次析构)
}

输出结果:

MyClass(1) 构造
对象ID:1
uptr1已失去所有权
MyClass(0) 构造  // 数组第0个元素
MyClass(0) 构造  // 数组第1个元素
数组第0个ID:0
MyClass(1) 析构  // uptr2.reset()触发
MyClass(0) 析构  // 数组元素0析构
MyClass(0) 析构  // 数组元素1析构
(3)适用场景
  • 管理独占资源(如局部动态对象、函数返回的动态对象)。
  • 作为容器元素(因可移动,适合std::vector等容器)。
  • 替代裸指针作为类的成员变量,避免手动释放内存。

2. std::shared_ptr:共享所有权的智能指针

std::shared_ptr 是共享式智能指针:多个shared_ptr可以共同拥有同一个对象的所有权,通过引用计数(reference count) 跟踪对象被引用的次数。当最后一个shared_ptr被销毁时,对象才会被释放。

(1)核心特性
  • 共享性:支持复制(拷贝构造和拷贝赋值),每次复制会使引用计数 + 1;销毁时引用计数 - 1。
  • 引用计数:内部维护一个计数器,记录当前有多少个shared_ptr指向该对象。当计数为 0 时,自动调用delete释放对象。
  • 线程安全:引用计数的操作是线程安全的,但对指向的对象的访问需要手动加锁。
(2)基本用法
#include <memory>
#include <iostream>

int main() {
    // 1. 初始化:推荐用std::make_shared(更高效,一次性分配对象和引用计数的内存)
    std::shared_ptr<MyClass> sptr1 = std::make_shared<MyClass>(2);  // 引用计数=1
    
    // 2. 复制:引用计数+1
    std::shared_ptr<MyClass> sptr2 = sptr1;  // 引用计数=2
    std::cout << "当前引用计数:" << sptr1.use_count() << std::endl;  // 输出:2
    
    // 3. 访问对象:与unique_ptr相同
    std::cout << "对象ID:" << sptr2->getId() << std::endl;  // 输出:2
    
    // 4. 手动释放:reset()使当前shared_ptr失去所有权,引用计数-1
    sptr1.reset();  // 引用计数=1
    std::cout << "sptr1 reset后计数:" << sptr2.use_count() << std::endl;  // 输出:1
    
    return 0;  // sptr2析构,引用计数=0 → 调用MyClass(2)的析构函数
}

输出结果:

MyClass(2) 构造
当前引用计数:2
对象ID:2
sptr1 reset后计数:1
MyClass(2) 析构
(3)循环引用问题与解决

shared_ptr的最大陷阱是循环引用(circular reference):两个或多个shared_ptr互相引用,导致引用计数无法归零,对象永久无法释放(内存泄漏)。

示例:循环引用导致内存泄漏

#include <memory>

class B;  // 前向声明

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

class B {
public:
    std::shared_ptr<A> a_ptr;  // B持有A的shared_ptr(形成循环)
    ~B() { std::cout << "B析构" << std::endl; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;  // a引用b → b的计数=2
    b->a_ptr = a;  // b引用a → a的计数=2
    
    // 离开作用域时,a和b的计数各减1(变为1),但循环引用导致计数无法归零
    // 因此A和B的析构函数不会被调用 → 内存泄漏
    return 0;
}

解决方式:用std::weak_ptr打破循环
weak_ptrshared_ptr的 “观察者”,它不拥有对象的所有权,也不增加引用计数,可用于访问shared_ptr管理的对象,但不会导致循环引用。

3. std::weak_ptr:弱引用智能指针

std::weak_ptr 是一种弱引用,它本身不管理对象的生命周期,仅作为shared_ptr的 “观察者” 存在:

  • 必须从shared_ptr或另一个weak_ptr构造。
  • 不增加引用计数,因此不会影响对象的释放。
  • 无法直接访问对象,需通过lock()方法获取一个临时的shared_ptr(若对象已释放,lock()返回nullptr)。
(1)核心特性
  • 无所有权:不影响对象的引用计数,对象的释放仅由shared_ptr决定。
  • 解决循环引用:在循环引用场景中,将一方的shared_ptr改为weak_ptr,打破计数循环。
  • 检查有效性:通过expired()方法判断所观察的对象是否已被释放(true表示已释放)。
(2)基本用法(解决循环引用)
#include <memory>

class B;

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

class B {
public:
    std::weak_ptr<A> a_ptr;  // B持有A的weak_ptr(打破循环)
    ~B() { std::cout << "B析构" << std::endl; }
};

int main() {
    auto a = std::make_shared<A>();  // a计数=1
    auto b = std::make_shared<B>();  // b计数=1
    
    a->b_ptr = b;  // b计数=2
    b->a_ptr = a;  // a计数仍为1(weak_ptr不增加计数)
    
    // 离开作用域时:
    // a析构 → a计数=0 → A析构 → a->b_ptr析构 → b计数=1
    // b析构 → b计数=0 → B析构
    return 0;
}

输出结果:

A析构
B析构  // 循环引用被打破,对象正常释放
(3)访问weak_ptr观察的对象
int main() {
    auto sptr = std::make_shared<MyClass>(3);  // sptr计数=1
    std::weak_ptr<MyClass> wptr = sptr;        // wptr观察sptr
    
    // 通过lock()获取shared_ptr(若对象有效)
    if (auto temp_sptr = wptr.lock()) {  // temp_sptr计数=2
        std::cout << "对象ID:" << temp_sptr->getId() << std::endl;  // 输出:3
    }  // temp_sptr析构,计数=1
    
    sptr.reset();  // 计数=0 → 对象析构
    
    // 检查对象是否已释放
    if (wptr.expired()) {
        std::cout << "对象已释放" << std::endl;
    }
    
    return 0;
}

输出结果:

MyClass(3) 构造
对象ID:3
MyClass(3) 析构
对象已释放

三、智能指针的使用准则与注意事项

  1. 优先使用std::make_uniquestd::make_shared
    这两个函数比直接用new初始化智能指针更安全(避免内存泄漏风险)、更高效(make_shared一次性分配对象和引用计数内存)。

    // 推荐
    auto uptr = std::make_unique<MyClass>(1);
    auto sptr = std::make_shared<MyClass>(2);
    
    // 不推荐(但合法)
    std::unique_ptr<MyClass> uptr(new MyClass(1));
    
  2. 避免将裸指针交给多个智能指针管理
    若多个智能指针同时管理同一个裸指针,会导致重复释放(未定义行为,通常会崩溃)。

    MyClass* raw_ptr = new MyClass(1);
    std::unique_ptr<MyClass> uptr1(raw_ptr);
    // std::unique_ptr<MyClass> uptr2(raw_ptr);  // 错误:同一裸指针被两个智能指针管理,会重复释放
    
  3. unique_ptr的移动语义
    unique_ptr不可复制,只能通过std::move转移所有权,转移后原指针变为nullptr

  4. shared_ptr的循环引用
    始终警惕循环引用问题,用weak_ptr打破双向shared_ptr引用。

  5. 不要手动删除智能指针管理的裸指针
    智能指针会自动释放内存,手动delete会导致重复释放(崩溃)。

  6. 智能指针与数组

    • unique_ptr需显式指定T[]以管理数组(自动调用delete[])。
    • shared_ptr默认用delete释放,管理数组时需手动指定删除器(std::default_delete<T[]>):
      // shared_ptr管理数组的正确方式
      std::shared_ptr<MyClass> arr_sptr(new MyClass[2], std::default_delete<MyClass[]>());
      

四、三种智能指针的对比总结

智能指针类型所有权特性引用计数复制 / 移动支持典型应用场景核心优势
std::unique_ptr独占所有权仅支持移动局部动态对象、容器元素、独占资源轻量高效,无额外开销
std::shared_ptr共享所有权支持复制和移动多所有者共享资源(如容器间共享对象)灵活共享,自动管理生命周期
std::weak_ptr无所有权(仅观察)不影响支持复制和移动解决shared_ptr的循环引用打破循环,安全观察共享对象

五、总结

智能指针是 C++ 动态内存管理的核心工具,通过 RAII 机制自动释放内存,从根本上避免了内存泄漏和野指针问题。实际开发中:

  • 优先用unique_ptr(轻量、高效,适合大多数独占场景)。
  • 需共享资源时用shared_ptr,但警惕循环引用(用weak_ptr解决)。
  • 避免手动管理裸指针,尽可能用智能指针替代,写出更安全、可靠的 C++ 代码。
Logo

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

更多推荐