C++智能指针介绍

好的,非常乐意为你详细讲解 C++ 智能指针。这是现代 C++(C++11 及之后)中最重要的特性之一,它彻底改变了 C++ 的内存管理方式。

我们将从为什么 需要它开始,然后逐一介绍三种核心的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr


🎯 1. 为什么需要智能指针?(传统指针的问题)

在 C++ 中,我们经常使用 new 在**堆 (Heap)**上动态分配内存。

C++

scss 复制代码
void old_way() {
    MyClass* raw_ptr = new MyClass(); // 1. 分配内存
    raw_ptr->do_something();
    // ...
    delete raw_ptr; // 2. 必须手动释放
}

这种"裸指针" (Raw Pointer) 的管理方式非常危险,极易出错:

  1. 内存泄漏 (Memory Leaks)

    • 忘记 delete :最常见的问题。raw_ptr 是一个局部变量,当 old_way() 函数结束时,raw_ptr 变量本身被销毁了,但它所指向 的在"堆"上的 MyClass 对象依然存在,这块内存永远无法被回收。
    • 提前返回 :如果在 newdelete 之间有 returnbreakcontinuedelete 语句可能被跳过。
    • 发生异常 :如果在 do_something() 中抛出了异常,delete raw_ptr; 这一行永远不会被执行,内存泄漏。
  2. 悬挂指针 (Dangling Pointers)
    悬空指针

    • 当一个指针被 delete 后,它并没有自动变为 nullptr,它依然指向那块"已释放"的内存。如果后续代码不慎再次使用了这个指针("Use-After-Free"),会导致未定义行为(通常是程序崩溃)。
  3. 所有权 (Ownership) 不明

    • 当一个裸指针在多个函数或对象之间传递时,到底 负责 delete 它?
    • 如果两个模块都尝试 delete 同一个指针,会引发二次释放 (Double Free) ,导致程序崩溃。
    • 如果谁都不 delete,就是内存泄漏。

💡 2. 核心思想:RAII 与智能指针

为了解决这些问题,C++ 引入了智能指针 (Smart Pointers)

智能指针不是 指针,它是一个对象 (一个类模板)。它封装(wrap)了一个裸指针,并利用了 C++ 的一个核心特性:RAII (Resource Acquisition Is Initialization) ,即"资源获取即初始化"。

RAII 的核心思想:

  1. 一个对象在它的构造函数 中获取资源(这里是 new 出来的内存)。
  2. 这个对象在它的析构函数 中释放资源(这里是 delete 封装的裸指针)。
  3. C++ 保证,一个在栈 (Stack)上创建的对象,当它离开作用域时(例如函数返回),它的析构函数必定会被自动调用(即使发生异常,也会"栈展开")。

智能指针就是这样一个 RAII 对象 。你把 new 得到的指针交给它,你就可以"忘记"delete 了。当智能指针对象生命周期结束时,它会在析构函数中自动帮你 delete 掉它所管理的指针。

C++

c 复制代码
#include <memory> // 智能指针的头文件

void smart_way() {
    // std::make_unique 是创建 unique_ptr 的推荐方式 (C++14)
    std::unique_ptr<MyClass> smart_ptr = std::make_unique<MyClass>();
    
    smart_ptr->do_something();

    // 当 smart_way() 函数返回时,smart_ptr 离开作用域
    // 它的析构函数被自动调用
    // 析构函数会自动执行 delete smart_ptr.get()
} // <-- 内存在这里被自动释放,即使发生异常也一样!

🔧 3. std::unique_ptr (独占指针)

std::unique_ptr 是最常用、最高效、最"轻量级"的智能指针。

  • 核心概念: 独占所有权 (Exclusive Ownership)

  • 含义: 在任何时刻,只有一个 unique_ptr 可以指向某个特定的对象。它"拥有"这个对象。

  • 特性:

    1. 轻量:它的大小和裸指针一样(没有额外开销),因为它不需要存储"引用计数"之类的东西。
    2. 不可复制 (Non-Copyable) :你不能"拷贝"一个 unique_ptr,因为这会违反"独占"所有权。
    3. 可以移动 (Movable) :你可以将所有权从一个 unique_ptr 转移 (move) 给另一个。

示例:创建、使用和转移

C++

c 复制代码
#include <memory>
#include <utility> // for std::move

// 推荐的创建方式 (C++14)
std::unique_ptr<MyClass> p1 = std::make_unique<MyClass>();

// 像裸指针一样使用
p1->do_something();
std::cout << (*p1).value;

// 1. 编译错误:不允许复制!
// std::unique_ptr<MyClass> p2 = p1; // ERROR: copy constructor is deleted

// 2. 正确:转移所有权 (Move)
// p1 将放弃所有权,并变为空 (nullptr)
std::unique_ptr<MyClass> p2 = std::move(p1);

if (p1 == nullptr) {
    std::cout << "p1 现在是空的" << std::endl;
}
if (p2 != nullptr) {
    std::cout << "p2 拥有了该对象" << std::endl;
    p2->do_something();
} // <-- p2 在这里离开作用域,释放 MyClass 对象

// 3. 从函数返回 (所有权转移)
std::unique_ptr<MyClass> create_object() {
    // 工厂函数返回一个对象的所有权
    return std::make_unique<MyClass>();
}
std::unique_ptr<MyClass> p3 = create_object();

// 4. 作为参数传递 (转移所有权)
void take_ownership(std::unique_ptr<MyClass> p) {
    p->do_something();
} // <-- p 在这里析构,释放对象
take_ownership(std::move(p3));
// p3 在此之后也变为空

什么时候用 unique_ptr

答案:默认首选! 只要你不需要多个指针"共享"一个对象,就应该用 unique_ptr

  • 作为类的成员变量(Pimpl 惯用法)。
  • 在函数中创建并返回一个堆上对象(工厂模式)。
  • 存储在 STL 容器中(例如 std::vector<std::unique_ptr<MyClass>>)。

🤝 4. std::shared_ptr (共享指针)

std::shared_ptr 解决了"多个指针需要指向同一对象"的场景。

  • 核心概念: 共享所有权 (Shared Ownership)

  • 含义: 多个 shared_ptr 可以指向同一个对象。它们通过引用计数 (Reference Counting) 来管理这个对象的生命周期。

  • 特性:

    1. 引用计数:内部有一个"控制块",其中包含一个计数器。
    2. 每当一个 shared_ptr 被复制 (或赋值给另一个 shared_ptr)时,计数器 +1
    3. 每当一个 shared_ptr 被析构 (或被赋新值)时,计数器 -1
    4. 计数器减到 0 时,意味着最后一个拥有该对象的 shared_ptr 消失了,它会在析构时 delete 掉被管理的对象。
    5. 开销 :比 unique_ptr 重。它需要额外分配"控制块"的内存,并且增减计数器必须是原子操作(线程安全),这有性能开销。

示例:创建和共享

C++

c 复制代码
#include <memory>

// 推荐的创建方式 (C++11)
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
std::cout << "当前引用计数: " << sp1.use_count() << std::endl; // 输出: 1

{
    // 复制构造函数,sp2 也指向 sp1 的对象
    std::shared_ptr<MyClass> sp2 = sp1;
    std::cout << "当前引用计数: " << sp1.use_count() << std::endl; // 输出: 2
    
    // sp2 和 sp1 都可以使用
    sp2->do_something();

} // <-- sp2 在这里离开作用域,析构
  // 引用计数 -1,变为 1

std::cout << "当前引用计数: " << sp1.use_count() << std::endl; // 输出: 1

// 为什么用 make_shared?
// 1. 性能:MyClass 对象和"控制块"的内存可以一次性分配,减少内存分配次数。
// 2. 异常安全:避免了在 `new MyClass()` 成功但 `shared_ptr` 构造失败时导致的内存泄漏。

什么时候用 shared_ptr

当你无法明确界定"谁是唯一的所有者"时。

  • 你希望将一个对象存储在多个数据结构中,并且希望它在"所有引用它的地方都失效"后才被销毁。
  • 异步回调(例如,this 指针可能在回调被调用前失效,使用 shared_from_this)。

🚫 5. std::weak_ptr (弱指针)

shared_ptr 有一个致命问题:循环引用 (Circular Reference)

问题:循环引用

C++

c 复制代码
struct NodeB;

struct NodeA {
    std::shared_ptr<NodeB> b_ptr;
    ~NodeA() { std::cout << "NodeA 析构\n"; }
};

struct NodeB {
    std::shared_ptr<NodeA> a_ptr;
    ~NodeB() { std::cout << "NodeB 析构\n"; }
};

void circular_reference_problem() {
    auto a = std::make_shared<NodeA>(); // a 的计数 = 1
    auto b = std::make_shared<NodeB>(); // b 的计数 = 1

    a->b_ptr = b; // b 的计数 = 2 (a 持有 b)
    b->a_ptr = a; // a 的计数 = 2 (b 持有 a)

} // <-- 函数结束,a 和 b 两个 shared_ptr 离开作用域
// a 的计数从 2 -> 1
// b 的计数从 2 -> 1

// **内存泄漏!**
// a 的计数是 1 (因为 b 还指着它),所以 NodeA 不会被析构。
// b 的计数是 1 (因为 a 还指着它),所以 NodeB 也不会被析构。
// 它们互相"续命",导致谁也无法释放。

解决方案:std::weak_ptr

weak_ptr 是一种非拥有 (Non-owning) 的智能指针。

  • 核心概念: 它只是一个"观察者"。

  • 含义: 它可以指向 shared_ptr 管理的对象,但它不会增加引用计数。

  • 特性:

    1. 它不能阻止所指向的对象被销毁。
    2. 不能 直接通过 weak_ptr 使用对象(没有 ->* 操作符)。
    3. 你需要检查它指向的对象是否还"存活"。

示例:打破循环

C++

c 复制代码
struct NodeB_Fixed;

struct NodeA_Fixed {
    std::shared_ptr<NodeB_Fixed> b_ptr; // A 强引用 B
    ~NodeA_Fixed() { std::cout << "NodeA 析构\n"; }
};

struct NodeB_Fixed {
    // B 弱引用 A
    std::weak_ptr<NodeA_Fixed> a_ptr; // <-- 关键!
    ~NodeB_Fixed() { std::cout << "NodeB 析构\n"; }
};

void circular_reference_solution() {
    auto a = std::make_shared<NodeA_Fixed>(); // a 计数 = 1
    auto b = std::make_shared<NodeB_Fixed>(); // b 计数 = 1

    a->b_ptr = b; // b 计数 = 2
    b->a_ptr = a; // a 计数 = 1 (weak_ptr 不增加计数)
}
// 函数结束:
// 1. a (shared_ptr) 离开作用域,a 计数从 1 -> 0。
// 2. NodeA 被析构!
// 3. 在 NodeA 的析构函数中,a->b_ptr (shared_ptr) 被销毁。
// 4. b 计数从 2 -> 1。
// 5. b (shared_ptr) 离开作用域,b 计数从 1 -> 0。
// 6. NodeB 被析构!
// (顺序可能是 5->6->3->4->1->2,但结果一样)
// **内存被正确释放!**

如何使用 weak_ptr

你需要先把它"锁住" (.lock()),尝试获取一个临时的 shared_ptr

C++

c 复制代码
// 假设你有一个 weak_ptr
std::weak_ptr<MyClass> wp = ...;

// 尝试提升 (lock) 为 shared_ptr
if (std::shared_ptr<MyClass> sp = wp.lock()) {
    // 提升成功!
    // 这意味着在 if 语句块内,对象是存活的
    // sp 保证了对象在此期间不会被释放
    sp->do_something();
} else {
    // 提升失败,对象已经被销毁了
    std::cout << "对象已不存在" << std::endl;
}

什么时候用 weak_ptr

  1. 打破 shared_ptr 的循环引用(如上例中的父子/A-B 关系)。
  2. 缓存 (Caching) :你有一个缓存系统,希望缓存的对象在"别处都不用"时自动销毁,但你又想能随时访问它。std::map<Key, std::weak_ptr<Value>> 就很合适。
  3. 观察者模式Subject 可以持有 std::vector<std::weak_ptr<Observer>>,当 Observer 自行销毁时,Subject 不需要知道,它在通知时只需检查 lock() 是否成功。

💡 6. 智能指针最佳实践(总结)

  1. 首选 std::make_unique (C++14) :当你创建对象时,优先使用 unique_ptr

  2. 需要共享时才用 std::make_shared :当你明确知道所有权需要被共享时,才使用 shared_ptr

  3. 使用 weak_ptr 打破 shared_ptr 循环 :当你发现 shared_ptr 构成了循环(例如树结构中的"子节点指向父节点"),使用 weak_ptr 作为"非拥有"的一方。

  4. 不要混用裸指针和智能指针

    • 绝对禁止MyClass* raw = new MyClass();
    • std::shared_ptr<MyClass> p1(raw);
    • std::shared_ptr<MyClass> p2(raw);
    • 这会导致 p1p2 都有各自的引用计数(都为1),它们都会在析构时尝试 delete raw,导致二次释放
  5. 如何向函数传递智能指针?(非常重要)

    • 情况1:函数只是"使用"对象,不关心所有权。

      • 最佳实践: 传递裸指针 (MyClass*) 或引用 (MyClass&)。
      • 原因: 这最快,并且解耦。调用者可以用 unique_ptrshared_ptr 或普通栈对象。
      C++ 复制代码
      void use_object(MyClass* obj) { obj->do_something(); }
      // 调用:
      use_object(my_unique_ptr.get());
      use_object(my_shared_ptr.get());
    • 情况2:函数需要"共享"所有权(例如,把它存为成员)。

      • 最佳实践:常量引用 传递 shared_ptr

      C++

      c 复制代码
      void store_object(const std::shared_ptr<MyClass>& ptr) {
          m_member_ptr = ptr; // 复制,引用计数+1
      }
    • 情况3:函数需要"夺取"或"转移"所有权。

      • 最佳实践: 传递 unique_ptr(需要 std::move)。

      C++

      c 复制代码
      void take_ownership(std::unique_ptr<MyClass> ptr) {
          m_member_ptr = std::move(ptr); // 接收所有权
      }
      // 调用:
      take_ownership(std::move(my_unique_ptr));
相关推荐
·白小白2 小时前
力扣(LeetCode) ——43.字符串相乘(C++)
c++·leetcode
咬_咬2 小时前
C++仿muduo库高并发服务器项目:Poller模块
服务器·开发语言·c++·epoll·muduo
FMRbpm2 小时前
链表5--------删除
数据结构·c++·算法·链表·新手入门
Kimser3 小时前
QT C++ QWebEngine与Web JS之间通信
javascript·c++·qt
QT 小鲜肉3 小时前
【QT/C++】Qt样式设置之CSS知识(系统性概括)
linux·开发语言·css·c++·笔记·qt
Elias不吃糖3 小时前
NebulaChat 框架学习笔记:深入理解 Reactor 与多线程同步机制
linux·c++·笔记·多线程
转基因4 小时前
命名空间与匿名空间
c++
煤球王子4 小时前
学而时习之:C++中的动态内存管理
c++
云知谷4 小时前
【经典书籍】《代码整洁之道》第六章“对象与数据结构”精华讲解
c语言·开发语言·c++·软件工程·团队开发