前言
学习C++智能指针。
指针(Pointer)就是一个变量,其存储的是另一个变量的内存地址,理解指针是掌握 C++ 内存管理、数组、对象以及底层操作的关键。
为什么使用指针
1. 动态内存管理:在运行时根据需要申请内存(使用 new 和 delete)。原生数组(如 int a[10])的大小在编译时就确定了,存储在栈(Stack)上。但很多时候,你并不知道程序运行过程中需要多少内存。
- 按需分配:指针允许你在程序运行时使用 new 关键字在堆(Heap)上申请内存。
- 生命周期控制:栈上的变量在函数结束时会自动销毁,而指针指向的堆内存可以跨越函数生命周期存在,直到你手动释放它。
2. 传递大型对象:通过指针传递参数(或引用)可以避免复制整个对象的开销,提高程序性能。
3. 实现复杂数据结构:如链表(每个节点通过指针指向下一个节点)、二叉树、图(父节点通过指针寻找子节点)等,必须依赖指针。
4. 底层操作:直接访问硬件或操作特定的内存区域。
为什么需使用智能指针
原生指针弊端
原生指针存在的问题:内存泄漏、悬空指针(重复释放)、野指针、所有权不清晰。
内存泄漏
用 new 分配了内存但忘记用 delete 释放!
- 指针重定向
C++- int* p = new int(10); // 申请了内存 A
- p = new int(20); // p 现在指向了内存 B,内存 A 的地址丢失了,再也找不回来。
复制代码 - 指针未释放
- void func() {
- int* p = new int(10);
- if (true) return; // 为真,函数直接返回,delete 被跳过!
- delete p;
- p = nullptr;
- }
复制代码 - 异常跳出: 程序运行中抛出异常,导致执行流直接跳转到 catch 块,没能执行到释放内存的代码。
- 内存泄漏的后果,内存泄漏通常不会立刻导致程序崩溃,它的危害是渐进式的:
- 性能下降 随着可用内存变少,操作系统会频繁进行内存交换(Swap),系统变得越来越卡。
- 内存不足而崩溃 当内存被耗尽,新的 new 请求会失败,抛出异常,程序被迫中止。
- 隐蔽性极强 在本地测试可能跑得好好的,但在服务器上连续运行几天甚至几周后,程序会无预兆地突然倒下
悬空指针(重复释放)
指向的内存已经被释放,但指针依然指向那个地址,重复释放指的是对同一块动态分配的内存进行多次释放操作。- int* original = new int(100);
- int* alias = original; // 两个指针指向同一内存
-
- delete original; // ✅ 释放内存
- // 此时 original 和 alias 都成为悬空指针
-
- delete alias; // ❌ 重复释放!同一内存再次释放
复制代码 野指针
指针未初始化,是指向“不可预知”内存区域的指针。它不是 nullptr,也不是指向有效的内存地址,它的值是随机的垃圾值。- //定义一个局部指针变量却不初始化时,它在栈上的值是上一次程序运行留下的残余数据
- int* p; // 野指针!没有初始化,指向一个随机地址(如 0xCC123456)
- *p = 100; // 极其危险!你可能正在修改系统关键数据或其它变量
- //指针跨越了作用域,返回局部变量的地址
- int* getPointer() {
- int x = 10;//x为栈上空间
- return &x; // 错误!x 是局部变量,函数结束就被销毁了
- }
- int* p = getPointer(); // p 变成了野指针(或悬空指针)
复制代码 所有权不清晰
当这个指针不再被需要时,究竟该由谁来负责 delete 它- // 谁该负责释放返回的这个指针?
- Data* fetchData() {
- return new Data();
- }
- void process() {
- Data* ptr = fetchData();
- // 如果我忘了写 delete,内存泄漏
- // 如果我 delete 了,但另一个地方也在用它,程序崩溃
- }
复制代码 智能指针优点
- 自动化的生命周期管理(核心优点),智能指针遵循 RAII(资源获取即初始化)原则。不再需要手动写 delete。当智能指针对象在栈上被销毁(如函数返回、大括号结束、异常抛出)时,它会自动释放所指向的堆内存。
- 防止野指针: 智能指针强制初始化,不会像原生指针那样默认指向随机地址。
- 防止重复释放(Double Free): std::unique_ptr 通过禁止拷贝确保只有一个;std::shared_ptr 通过计数确保只在最后一次被使用时才释放。
- 解决悬空指针: 使用 std::weak_ptr 可以在访问对象前先检查它是否还“活着”,从而避免访问已被释放的内存。
- 所有权 std::unique_ptr独占、std::shared_ptr共享、std::weak_ptr观察。
- 智能指针作为栈对象,即便发生异常,C++ 的“栈解旋(Stack Unwinding)”机制也会确保其析构函数被调用,从而安全地回收内存。这是编写健壮工业级代码的基础。
- std::unique_ptr:具有零开销(Zero-overhead)。它在内存大小和运行速度上与原生指针完全一致,编译器会将其优化为最高效的机器码。
- std::shared_ptr:虽然有引用计数的原子操作开销,但对于大多数业务逻辑来说,这种开销几乎可以忽略不计。
std::shared_ptr
shared_ptr 实际上包含两个指针:
- 指向数据的指针:直接指向你申请的内存对象。
- 指向控制块的指针:控制块是一个动态分配的内存区域,存放着:
- 引用计数(Shared Count):有多少个 shared_ptr 拥有它。当你拷贝一个 shared_ptr 时,计数器加 1;当一个 shared_ptr 销毁时,计数器减 1。计数 > 0:资源保持有效。计数 = 0:最后一个“拥有者”负责调用 delete 销毁资源。
引用计数的增加和减少是原子操作(使用类似 std::atomic 的机制)。这样可以保证在多线程环境下,多个线程同时拷贝或销毁指向同一个对象的 shared_ptr 时,计数器不会乱掉,从而避免内存泄漏或重复释放。
它所指向的对象数据本身并不是线程安全的,多线程读写对象需要额外加锁。
- 弱引用计数(Weak Count):有多少个 weak_ptr 正在观察它。
- 自定义删除器(Deleter):如果需要特殊的释放逻辑。
[code]#include #include struct Widget { Widget() { std::cout |