现代 C++ 智能指针与内存管理

一、裸指针的风险与智能指针的诞生
1. 传统内存管理的痛点

在 C++98 时代,手动内存管理存在三大核心问题:

  • 内存泄漏new分配的内存未被delete释放
  • 双重释放:多个指针指向同一内存,多次delete导致崩溃
  • 悬空指针:对象已被释放,但仍有指针引用它

典型案例:

void process() {
    int* ptr = new int(42);
    // 业务逻辑...
    if (condition) return; // 直接返回导致内存泄漏
    delete ptr; // 若condition为true,此句不会执行
}
2. RAII(资源获取即初始化)范式

智能指针通过 RAII 原则解决上述问题:

  • 在构造时获取资源(如分配内存)
  • 在析构时释放资源(如delete内存)
  • 生命周期结束时自动触发析构,确保资源释放
二、std::unique_ptr:独占所有权的智能指针
1. 核心特性
  • 独占性:同一时刻只能有一个unique_ptr指向该对象
  • 不可复制:禁用拷贝构造和赋值运算符
  • 可移动:通过std::move转移所有权
#include 

void demo_unique_ptr() {
    // 创建方式1:直接构造
    std::unique_ptr ptr1(new int(10));
    
    // 创建方式2(推荐):使用make_unique(C++14)
    auto ptr2 = std::make_unique(20);
    
    // 转移所有权
    std::unique_ptr ptr3 = std::move(ptr2); // ptr2变为空
    
    // 访问对象
    std::cout << *ptr3 << std::endl; // 输出20
    
    // 释放所有权
    int* raw_ptr = ptr3.release(); // ptr3变为空,raw_ptr需手动管理
    delete raw_ptr;
}
2. 实战技巧
  • 自定义删除器
// 使用lambda作为删除器
auto deleter = [](int* p) {
    std::cout << "Custom deleting..." << std::endl;
    delete p;
};
std::unique_ptr ptr(new int(42), deleter);

  • 作为容器元素
std::vector> vec;
vec.push_back(std::make_unique(1));
vec.push_back(std::make_unique(2));

  • 返回值优化
std::unique_ptr create_object() {
    return std::make_unique(); // 自动移动语义
}
三、std::shared_ptr:共享所有权的智能指针
1. 引用计数原理
  • 每个shared_ptr关联一个引用计数
  • 拷贝 / 赋值时计数 + 1,析构时计数 - 1
  • 计数为 0 时自动释放对象
#include 

struct MyClass {
    ~MyClass() { std::cout << "Destroying MyClass" << std::endl; }
};

void demo_shared_ptr() {
    auto ptr1 = std::make_shared(); // 计数=1
    {
        auto ptr2 = ptr1; // 计数=2
    } // ptr2析构,计数=1
    
    // ptr1析构,计数=0,对象被销毁
}
2. 循环引用问题

当两个对象通过shared_ptr互相引用时,会导致内存泄漏:

struct B;

struct A {
    std::shared_ptr b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

struct B {
    std::shared_ptr a_ptr;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void cyclic_reference() {
    auto a = std::make_shared();
    auto b = std::make_shared();
    a->b_ptr = b;
    b->a_ptr = a; // 循环引用:a和b的引用计数永远不会为0
} // 内存泄漏!
3. 性能考量
  • 每个shared_ptr比裸指针大(通常为 2 个指针大小)
  • 引用计数操作需要原子性保证,存在性能开销
  • 优先使用unique_ptr,仅在必要时使用shared_ptr
四、std::weak_ptr:解决循环引用的利器
1. 弱引用特性
  • 不增加引用计数,不控制对象生命周期
  • 用于观察shared_ptr管理的对象
  • 通过lock()方法获取临时shared_ptr
struct B;

struct A {
    std::weak_ptr b_ptr; // 使用weak_ptr打破循环引用
    ~A() { std::cout << "A destroyed" << std::endl; }
};

struct B {
    std::shared_ptr a_ptr;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void fix_cyclic_reference() {
    auto a = std::make_shared();
    auto b = std::make_shared();
    a->b_ptr = b; // weak_ptr不增加引用计数
    b->a_ptr = a; // 只有a的引用计数=2
    
    // 使用weak_ptr
    if (auto shared_b = a->b_ptr.lock()) {
        // shared_b是临时的shared_ptr,确保对象存在
    }
} // a和b均正确释放
2. 典型应用场景
  • 缓存系统:weak_ptr观察缓存对象,避免阻止对象被回收
  • 观察者模式:观察者持有被观察对象的weak_ptr
  • 树形结构:子节点用weak_ptr指向父节点
五、智能指针使用最佳实践
  1. 优先使用智能指针

    • 避免手动new/delete,减少内存管理负担
  2. 选择合适的智能指针

    • 独占场景用unique_ptr
    • 共享场景用shared_ptr
    • 打破循环引用用weak_ptr
  3. 避免混合使用裸指针和智能指针

    int* raw = new int;
    std::shared_ptr ptr(raw); // 危险!多个所有者可能释放同一内存
    
  4. 使用 make 系列函数创建智能指针

    • std::make_unique(C++14)和std::make_shared
    • 提供更好的异常安全性和性能优化
  5. 避免在函数参数中使用智能指针

    • 除非明确需要转移所有权或共享所有权
    • 优先传递对象引用或裸指针
六、智能指针与多线程
1. 线程安全特性
  • 引用计数的增减是原子操作(线程安全)
  • 对象的读写操作需要额外同步(与裸指针相同)
2. 跨线程传递智能指针
#include 
#include 

void worker(std::shared_ptr data) {
    // 使用data...
}

int main() {
    auto data = std::make_shared(42);
    std::thread t(worker, data); // 安全传递shared_ptr
    t.join();
    return 0;
}
七、C++20 新增功能
1. std::make_shared_for_overwrite
// 无需值初始化的内存分配(适合POD类型)
auto ptr = std::make_shared_for_overwrite (1000);

2. 智能指针的分配器支持
// 使用自定义分配器的shared_ptr
auto alloc = MyAllocator();
auto ptr = std::allocate_shared(alloc, 42);

总结

智能指针是现代 C++ 最核心的特性之一,通过 RAII 原则和引用计数机制,彻底改变了内存管理方式。合理使用unique_ptrshared_ptrweak_ptr,不仅能消除内存泄漏风险,还能提升代码的健壮性和可维护性。

你可能感兴趣的:(现代 C++ 智能指针与内存管理)