搜索

C++ 教程

C++基础

第1章 搭建开发环境与第一个程序 第2章 基本数据类型与变量 第3章 运算符与表达式 第4章 流程控制语句 第5章 数组与字符串 第6章 函数基础 第7章 指针基础 第8章 引用与const限定 第9章 动态内存管理 第10章 结构体与联合体 第11章 函数进阶

面向对象

第12章 面向对象编程入门 第13章 封装与访问控制 第14章 运算符重载 第15章 继承与派生 第16章 多态与虚函数

STL与进阶

第17章 模板编程基础 第18章 标准模板库(STL)容器 第19章 STL算法与迭代器 第20章 异常处理机制 第21章 文件输入输出 第22章 字符串类string 第23章 命名空间与作用域 第24章 类型转换操作符

现代C++

第25章 智能指针 第26章 移动语义与右值引用 第27章 Lambda表达式 第28章 多线程编程基础 第29章 模板元编程入门 第30章 设计模式基础 第31章 C++11/14新特性 第32章 C++17/20核心特性

实战进阶

第33章 性能优化技巧 第34章 调试与测试 第35章 项目结构与构建 第36章 网络编程入门 第37章 数据库连接操作 第38章 图形界面开发基础 第39章 综合项目实战 第40章 进阶学习路线

第25章 智能指针

第25章 智能指针

进阶 👤 已掌握动态内存管理,希望杜绝内存泄漏的开发者 🔧 管理动态分配的对象所有权,构建资源安全的现代C++程序
本章你将学到
  • 理解智能指针如何自动管理内存生命周期
  • 掌握三种标准智能指针(unique_ptr, shared_ptr, weak_ptr)的核心用法与区别
  • 在实际项目中正确选择和使用智能指针,避免常见陷阱
核心概念所有权引用计数

还记得第9章动态内存管理中,我们手动调用new和delete的繁琐与危险吗?一个疏忽就会导致内存泄漏或双重释放。智能指针就是来解决这个问题的“自动管家”。它本质上是一个类模板,包装了原始指针,并利用C++的析构函数特性,在对象生命周期结束时自动释放内存。

核心概念:所有权

生活类比: 就像你从图书馆借了一本书,在你持有(拥有)这本书期间,你有责任保管它,并在归还日期(生命周期结束)时还回去。智能指针就是帮你自动记录“借阅状态”和“归还日期”的图书管理员。
技术定义: 所有权指对动态分配对象的管理责任,包括其生命周期的控制。智能指针的核心就是清晰定义和转移这种所有权,确保在任何时刻,一个对象都有且只有一个明确的“拥有者”负责其释放。

三种智能指针的对比与选择

C++标准库提供了三种主要的智能指针,它们的所有权模型截然不同。选择哪一个,取决于你的对象需要被多少个“拥有者”持有。

类型所有权模型拷贝/赋值行为典型应用场景性能开销
std::unique_ptr独占所有权禁止拷贝,允许移动工厂函数返回值、作为类成员管理独占资源几乎为零(与原始指针相当)
std::shared_ptr共享所有权允许拷贝,引用计数+1多个对象需要共享访问同一数据(如图形资源、缓存)有开销(控制块内存分配,原子操作)
std::weak_ptr弱引用,不拥有所有权不影响引用计数打破shared_ptr的循环引用、观察者模式较小(仅存储弱引用)

我的经验是,优先使用unique_ptr,它强制你思考清晰的所有权流向。只有当逻辑上确实需要共享时,才使用shared_ptr。weak_ptr则是shared_ptr的辅助工具。

独占之王:std::unique_ptr

unique_ptr如其名,独占它所指向的对象。它不可拷贝,但可以通过std::move转移所有权。这是最轻量、最高效的智能指针。

// 创建一个unique_ptr,管理一个int
std::unique_ptr<int> ptr1(new int(42));
// 使用make_unique是更安全、更高效的方式(C++14起)
auto ptr2 = std::make_unique<int>(100);
// 错误!unique_ptr禁止拷贝构造
// std::unique_ptr<int> ptr3 = ptr1;
// 正确:通过移动转移所有权,ptr1变为nullptr
auto ptr3 = std::move(ptr1);

关键点:make_unique的优势

始终优先使用std::make_unique而非直接new。原因有三:1) 避免内存泄漏(如果构造参数时抛出异常,new分配的内存可能泄漏);2) 代码更简洁;3) 可能带来更好的缓存局部性(一次性分配对象和控制块内存)。我在项目中强制要求使用make_unique。

unique_ptr还可以管理数组,并支持自定义删除器。例如,管理一个用C风格API分配的内存:

// 自定义删除器,使用free释放malloc分配的内存
auto deleter = [](void* p) { std::free(p); };
std::unique_ptr<void, decltype(deleter)> cPtr(std::malloc(1024), deleter);

共享与协作:std::shared_ptr

当多个部分需要“共同拥有”一个对象时,shared_ptr登场。它内部维护一个引用计数,记录有多少个shared_ptr指向同一对象。计数归零时,对象自动销毁。

shared_ptr A
→
对象数据
↗
shared_ptr B
→
控制块
↘
shared_ptr C
→

上图展示了shared_ptr的典型结构。多个shared_ptr共享一个控制块,其中包含引用计数(强引用)和弱引用计数。同样,使用std::make_shared是首选,因为它将对象和控制块合并为一次内存分配,效率更高。

class NetworkResource { /* ... */ };
auto resource = std::make_shared<NetworkResource>();
void process(std::shared_ptr<NetworkResource> res) {
    // 函数内,引用计数+1
}
process(resource); // 引用计数变为2
// process函数结束,形参res析构,引用计数变回1

常见误区

  • 循环引用导致内存泄漏: 这是shared_ptr最经典的坑。如果两个对象互相用shared_ptr指向对方,引用计数永远无法归零。解决方案是使用weak_ptr打破循环。
  • 从this指针创建shared_ptr: 在类的成员函数内,直接std::shared_ptr<MyClass>(this)是灾难性的,它会创建一个新的控制块。正确做法是让类继承std::enable_shared_from_this<T>,然后调用shared_from_this()。
  • 性能盲目乐观: shared_ptr的引用计数操作是原子的,以保证线程安全,这会带来开销。在性能关键路径上大量拷贝shared_ptr需谨慎。

观察者与破局者:std::weak_ptr

weak_ptr不单独使用,它总是从shared_ptr创建。它“观察”一个由shared_ptr管理的对象,但不会增加其引用计数。这解决了两个问题:1) 观察对象是否存在而不影响其生命周期;2) 打破shared_ptr的循环引用。

auto shared = std::make_shared<int>(200);
std::weak_ptr<int> weakObs = shared; // 创建弱引用,引用计数仍为1

// 使用前,必须将weak_ptr“提升”为shared_ptr
if (auto tmp = weakObs.lock()) { // lock()尝试提升
    // 提升成功,对象还存在,可以安全使用tmp
    *tmp = 300;
} else {
    // 对象已被释放
}

编者提示: 理解智能指针的所有权转移,特别是std::move的运用,是掌握第26章《移动语义与右值引用》的重要前置知识。移动语义让资源所有权的转移变得高效且明确。

下面用一个典型场景——循环引用,来展示三者的协作关系:

问题场景:循环引用

  • 父节点持有子节点的shared_ptr
  • 子节点也持有父节点的shared_ptr
  • 两者引用计数永远≥1,无法释放

解决方案:weak_ptr破局

  • 子节点持有父节点的weak_ptr
  • 子节点不增加父节点的引用计数
  • 父节点析构时,引用计数归零,可正常释放
  • 子节点通过lock()安全访问父节点

智能指针与多态、数组

智能指针完美支持多态。你可以用基类的智能指针指向派生类对象。对于数组,unique_ptr有特化版本unique_ptr<T[]>,它会调用delete[]。而shared_ptr管理数组则需要自定义删除器。

class Base { virtual ~Base() = default; };
class Derived : public Base {};

std::unique_ptr<Base> polyPtr = std::make_unique<Derived>(); // 多态,正确

// 管理数组
std::unique_ptr<int[]> arrPtr = std::make_unique<int[]>(10); // C++14,大小10的数组
arrPtr[0] = 1;

// shared_ptr管理数组(不推荐,优先用std::vector)
std::shared_ptr<int> sharedArr(new int[10], std::default_delete<int[]>());

动手试一试

1. 实现一个简单的资源管理类: 编写一个FileHandler类,使用unique_ptr配合自定义删除器(调用fclose)来管理一个FILE*文件指针。实现构造函数(打开文件)和析构函数(自动关闭)。
2. 模拟观察者模式: 创建两个类Subject和Observer。Subject包含一个vector<weak_ptr<Observer>>列表。Observer包含一个指向Subject的weak_ptr。编写代码让一个Subject和多个Observer建立双向(但非强持有)联系,并确保所有对象都能正确析构,无内存泄漏。
3. 性能对比实验: 写一个循环(例如100万次),分别测试:a) 原始指针new/delete;b) make_unique;c) make_shared。使用<chrono>库测量耗时,直观感受不同智能指针的开销差异。

检验你的理解

  1. 以下代码片段存在什么问题?会导致什么后果?
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2(p1.get());
  2. 如果你想将一个unique_ptr管理的对象传递给一个函数使用,但该函数调用后你仍需要继续拥有这个对象,函数参数应该声明为什么类型?(值、引用、原始指针?)
  3. 判断对错:weak_ptr的lock()方法如果返回一个空的shared_ptr,说明其所观察的对象一定已经被释放了。
  4. 在什么情况下,使用make_shared创建对象,其内存释放时机可能会比用new创建然后传给shared_ptr构造函数要晚?

本章小结

  • 智能指针是自动管理动态内存生命周期的RAII包装器, 核心价值是明确所有权和防止资源泄漏。
  • 三种智能指针对应三种所有权模型: unique_ptr(独占,高效)、shared_ptr(共享,有开销)、weak_ptr(观察,用于打破循环引用)。
  • 优先使用make_unique和make_shared, 它们更安全、更高效,能避免显式new带来的潜在异常安全问题。
  • 警惕shared_ptr的循环引用陷阱, 这是导致内存泄漏的常见原因,使用weak_ptr是标准解决方案。
  • 智能指针支持多态和自定义删除器, 使其能够管理几乎所有类型的资源,而不仅仅是new分配的内存。

意见反馈

微信二维码 微信扫码关注 抖音二维码 抖音扫码关注