智能指针理论学习笔记

4 阅读7分钟

一、智能指针概述

智能指针是C++11引入的RAII(资源获取即初始化)机制,用于自动管理动态内存的生命周期,避免内存泄漏。

核心原理

  • RAII机制:在构造函数中获取资源,在析构函数中释放资源
  • 重载运算符:重载 *-> 运算符,使其行为像普通指针
  • 自动释放:智能指针离开作用域时自动调用析构函数释放内存

二、unique_ptr - 独占所有权

2.1 基本概念

  • 独占所有权:同一时刻只能有一个 unique_ptr 指向某个对象
  • 不可复制:删除了拷贝构造函数和拷贝赋值运算符
  • 可移动:支持移动语义,所有权可以转移

2.2 核心实现原理

template<typename T>
class unique_ptr {
private:
    T* ptr;
    
public:
    // 构造函数
    explicit unique_ptr(T* p = nullptr) : ptr(p) {}
    
    // 析构函数 - 自动释放内存
    ~unique_ptr() {
        delete ptr;
    }
    
    // 删除拷贝构造函数
    unique_ptr(const unique_ptr&) = delete;
    
    // 删除拷贝赋值运算符
    unique_ptr& operator=(const unique_ptr&) = delete;
    
    // 移动构造函数
    unique_ptr(unique_ptr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    
    // 移动赋值运算符
    unique_ptr& operator=(unique_ptr&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
    
    // 重载解引用运算符
    T& operator*() const {
        return *ptr;
    }
    
    // 重载箭头运算符
    T* operator->() const {
        return ptr;
    }
    
    // 获取原始指针
    T* get() const {
        return ptr;
    }
    
    // 释放所有权
    T* release() {
        T* temp = ptr;
        ptr = nullptr;
        return temp;
    }
    
    // 重置指针
    void reset(T* p = nullptr) {
        delete ptr;
        ptr = p;
    }
};

2.3 使用示例

#include <memory>
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "构造函数\n"; }
    ~MyClass() { std::cout << "析构函数\n"; }
    void doSomething() { std::cout << "执行操作\n"; }
};

int main() {
    // 创建unique_ptr
    std::unique_ptr<MyClass> ptr1(new MyClass());
    ptr1->doSomething();
    
    // 移动所有权
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
    // ptr1现在为nullptr
    if (!ptr1) {
        std::cout << "ptr1已释放所有权\n";
    }
    
    // 使用make_unique创建(C++14)
    auto ptr3 = std::make_unique<MyClass>();
    
    // 自定义删除器
    std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "w"), fclose);
    
    return 0;  // 自动调用析构函数
}

2.4 关键点总结

  • 理解unique_ptr独占所有权:同一时间只能有一个unique_ptr拥有对象
  • 性能最优:没有引用计数开销,与裸指针性能相当
  • 线程安全:单个unique_ptr的读写不是线程安全的,但不同unique_ptr之间是独立的

三、shared_ptr - 共享所有权

3.1 基本概念

  • 共享所有权:多个 shared_ptr 可以指向同一个对象
  • 引用计数:维护一个引用计数器,记录有多少个 shared_ptr 指向该对象
  • 自动释放:当引用计数降为0时,自动释放对象

3.2 引用计数机制

引用计数工作流程:
┌─────────────┐
│ shared_ptr1 │ ──┐
└─────────────┘   │
                  ├──→ [对象] ←── [控制块: ref_count=3]
┌─────────────┐   │       [weak_count=0]
│ shared_ptr2 │ ──┘
└─────────────┘
┌─────────────┐
│ shared_ptr3 │ ──┘
└─────────────┘

当某个shared_ptr析构时:
1. ref_count--
2. 如果ref_count == 0,释放对象

3.3 核心实现原理

template<typename T>
class shared_ptr {
private:
    T* ptr;
    int* ref_count;  // 引用计数
    
public:
    // 构造函数
    explicit shared_ptr(T* p = nullptr) 
        : ptr(p), ref_count(new int(1)) {}
    
    // 析构函数
    ~shared_ptr() {
        (*ref_count)--;
        if (*ref_count == 0) {
            delete ptr;
            delete ref_count;
        }
    }
    
    // 拷贝构造函数 - 增加引用计数
    shared_ptr(const shared_ptr& other) 
        : ptr(other.ptr), ref_count(other.ref_count) {
        (*ref_count)++;
    }
    
    // 拷贝赋值运算符
    shared_ptr& operator=(const shared_ptr& other) {
        if (this != &other) {
            // 减少当前对象的引用计数
            (*ref_count)--;
            if (*ref_count == 0) {
                delete ptr;
                delete ref_count;
            }
            
            // 复制新对象
            ptr = other.ptr;
            ref_count = other.ref_count;
            (*ref_count)++;
        }
        return *this;
    }
    
    // 移动构造函数
    shared_ptr(shared_ptr&& other) noexcept 
        : ptr(other.ptr), ref_count(other.ref_count) {
        other.ptr = nullptr;
        other.ref_count = nullptr;
    }
    
    // 重载解引用运算符
    T& operator*() const {
        return *ptr;
    }
    
    // 重载箭头运算符
    T* operator->() const {
        return ptr;
    }
    
    // 获取引用计数
    int use_count() const {
        return ref_count ? *ref_count : 0;
    }
    
    // 获取原始指针
    T* get() const {
        return ptr;
    }
};

3.4 使用示例

#include <memory>
#include <iostream>

class MyClass {
public:
    MyClass(int id) : id_(id) { 
        std::cout << "构造函数: " << id_ << "\n"; 
    }
    ~MyClass() { 
        std::cout << "析构函数: " << id_ << "\n"; 
    }
    void show() { 
        std::cout << "对象ID: " << id_ << "\n"; 
    }
private:
    int id_;
};

int main() {
    // 创建shared_ptr
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(1);
    std::cout << "引用计数: " << ptr1.use_count() << "\n";  // 1
    
    // 拷贝构造 - 引用计数增加
    std::shared_ptr<MyClass> ptr2 = ptr1;
    std::cout << "引用计数: " << ptr1.use_count() << "\n";  // 2
    
    {
        std::shared_ptr<MyClass> ptr3 = ptr1;
        std::cout << "引用计数: " << ptr1.use_count() << "\n";  // 3
    }  // ptr3离开作用域,引用计数减为2
    
    std::cout << "引用计数: " << ptr1.use_count() << "\n";  // 2
    
    // reset操作
    ptr2.reset();
    std::cout << "引用计数: " << ptr1.use_count() << "\n";  // 1
    
    return 0;  // ptr1离开作用域,引用计数减为0,对象被释放
}

3.5 关键点总结

  • 理解shared_ptr引用计数:通过控制块维护引用计数,计数为0时释放对象
  • 线程安全:引用计数的增减是原子操作,线程安全
  • 性能开销:比unique_ptr有额外的引用计数开销

四、weak_ptr - 解决循环引用

4.1 基本概念

  • 弱引用:不增加引用计数,不影响对象生命周期
  • 观察者:用于观察shared_ptr管理的对象
  • 解决循环引用:打破shared_ptr之间的循环引用

4.2 循环引用问题

// 循环引用示例 - 内存泄漏!
class Node {
public:
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "析构函数\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;  // node2引用计数: 2
    node2->next = node1;  // node1引用计数: 2
    
    // 离开作用域时,node1和node2的引用计数都为1,不会释放!
    return 0;  // 内存泄漏
}

4.3 weak_ptr解决方案

// 使用weak_ptr解决循环引用
class Node {
public:
    std::weak_ptr<Node> next;  // 使用weak_ptr
    ~Node() { std::cout << "析构函数\n"; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;  // node2引用计数: 1
    node2->next = node1;  // node1引用计数: 1
    
    // 离开作用域时,node1和node2的引用计数都为0,正常释放
    return 0;  // 正常释放
}

4.4 weak_ptr核心方法

#include <memory>
#include <iostream>

int main() {
    auto sp = std::make_shared<int>(42);
    std::weak_ptr<int> wp = sp;
    
    // 检查对象是否还存在
    if (!wp.expired()) {
        std::cout << "对象仍然存在\n";
    }
    
    // lock()获取shared_ptr
    if (auto locked = wp.lock()) {
        std::cout << "值: " << *locked << "\n";
        std::cout << "引用计数: " << locked.use_count() << "\n";
    }
    
    sp.reset();  // 释放对象
    
    if (wp.expired()) {
        std::cout << "对象已被释放\n";
    }
    
    return 0;
}

4.5 关键点总结

  • 理解weak_ptr循环引用解决:weak_ptr不增加引用计数,可以打破循环引用
  • lock()方法:将weak_ptr转换为shared_ptr,如果对象已释放则返回空
  • expired()方法:检查对象是否已被释放

五、三种智能指针对比

特性unique_ptrshared_ptrweak_ptr
所有权独占共享弱引用
引用计数
可复制
可移动
性能最优有开销有开销
用途单一所有权多个所有者观察者/解决循环引用

六、最佳实践

6.1 优先使用unique_ptr

// 推荐
auto ptr = std::make_unique<MyClass>();

// 不推荐
auto ptr = std::unique_ptr<MyClass>(new MyClass());

6.2 需要共享时使用shared_ptr

// 推荐
auto ptr = std::make_shared<MyClass>();

// 不推荐
auto ptr = std::shared_ptr<MyClass>(new MyClass());

6.3 避免循环引用

// 父子关系:父持有shared_ptr,子持有weak_ptr
class Parent;
class Child {
public:
    std::weak_ptr<Parent> parent;  // weak_ptr避免循环引用
};

class Parent {
public:
    std::vector<std::shared_ptr<Child>> children;
};

6.4 不要混用裸指针和智能指针

// 危险!
int* raw = new int(42);
std::shared_ptr<int> sp(raw);
delete raw;  // 重复释放!

// 正确做法
auto sp = std::make_shared<int>(42);

七、常见面试题

Q1: shared_ptr的线程安全性如何?

A:

  • 引用计数的增减是原子操作,线程安全
  • 指向的对象本身不是线程安全的,需要额外同步
  • 多个线程读写同一个shared_ptr对象需要加锁

Q2: make_shared比直接构造有什么优势?

A:

  • 性能更好:一次性分配内存(对象+控制块)
  • 异常安全:不会出现内存泄漏
  • 缓存友好:对象和控制块在连续内存中

Q3: weak_ptr如何知道对象是否被释放?

A:

  • weak_ptr通过控制块中的weak_count和expired标志
  • 当shared_ptr引用计数为0时,对象被释放,但控制块保留
  • 当weak_ptr引用计数也为0时,控制块才被释放