6. C++智能指针(1)


一、智能指针基础

1. 智能指针概述

✅ 什么是智能指针?

C++中的智能指针不是指针 ,本质是一个模板类 ,这个模板类封装了C++的普通裸指针(原始指针,比如int* p = new int ,对外提供和普通指针几乎一致的使用方式(可以用->访问成员、*解引用)。

简单理解:智能指针 = 普通指针 + 自动管家,这个管家会帮你处理指针最头疼的「善后工作」。

✅ 智能指针的作用与核心优势

C++的普通裸指针最大的问题:程序员手动申请内存(new),必须手动释放内存(delete ,一旦出现疏忽,就会引发各种内存问题,而智能指针的核心作用就是:

✅ 自动管理内存的申请与释放,无需程序员手动调用delete

✅ 从根源上避免「内存泄漏、悬挂指针、重复释放内存」三大经典内存问题;

✅ 天然支持异常安全:即使程序抛出异常,智能指针对象销毁时也会自动释放内存,不会内存泄漏。

✅ 智能指针与普通指针的核心区别
对比维度 普通裸指针(原始指针) C++智能指针
本质 变量,存储内存地址 模板类,封装了裸指针
内存释放 必须手动delete,程序员负责 自动释放,超出作用域/无引用时自动调用delete
内存安全 极易内存泄漏、野指针、重复释放 几乎不会出现上述问题,安全性极高
功能 只有「存地址」的基础功能 除指针功能外,还有生命周期管理、引用计数等
开销 无任何开销,极致轻便 unique_ptr几乎无开销;shared_ptr有轻微开销

2. C++11 引入的智能指针

C++的智能指针发展有一个过程,我们只需要掌握现代C++(C++11及以后) 的标准智能指针即可,所有智能指针都在 <memory> 头文件中,必须先引入头文件才能使用#include <memory>

✅ 标准三大核心智能指针
  1. std::unique_ptr :独占式智能指针 ------ 最常用、最轻量,一个资源只能被一个对象独占
  2. std::shared_ptr :共享式智能指针 ------ 一个资源可以被多个对象共享,基于「引用计数」实现
  3. std::weak_ptr :弱引用智能指针 ------ 配合shared_ptr使用,解决其「循环引用」问题,不管理资源生命周期
✅ 补充:被废弃的智能指针 std::auto_ptr(小白必避坑)

C++98中引入了第一个智能指针auto_ptr,它也是「独占式」,但设计有严重缺陷:支持拷贝赋值,拷贝后原指针会变成空指针 ,极易引发程序崩溃,因此C++11直接废弃了auto_ptr ,用unique_ptr完全替代。

面试考点:绝对不要在C++11及以后的代码中使用auto_ptr

3. 智能指针的内存管理

✅ 核心灵魂:RAII 思想(资源获取即初始化,必须掌握)

所有智能指针的底层实现,都基于C++的RAII核心设计思想,这是智能指针能「自动释放内存」的根本原因,小白必须理解这个思想:

RAII 定义 :在对象构造时获取资源 (比如智能指针对象创建时,通过new申请内存),在对象析构时释放资源 (智能指针对象生命周期结束时,析构函数自动调用delete释放内存)。

C++的语法规则:任何对象,只要超出其作用域,就一定会调用析构函数 。智能指针就是利用这个规则,把「内存释放」的逻辑写在析构函数里,从而实现自动释放,无需程序员干预。

✅ 自动管理内存生命周期

智能指针的内存生命周期,完全跟随智能指针对象的生命周期:

  1. 当我们创建一个智能指针对象,让它指向一块动态内存(new出来的内存),智能指针就「接管」了这块内存;
  2. 当智能指针对象:① 超出代码块作用域 ② 被赋值为nullptr ③ 调用reset()方法,那么智能指针会自动释放它接管的内存;
  3. 内存释放后,智能指针会自动置空,不会变成「野指针」。
✅ 智能指针的引用计数(关键概念)

引用计数 是一块「额外的内存空间」,专门用来记录:当前有多少个智能指针,同时指向同一块动态内存

  • ✔ 只有 std::shared_ptr 有引用计数,unique_ptr 没有(独占式,无需计数);
  • weak_ptr 会读取引用计数,但不会修改引用计数(核心特点);
  • ✔ 引用计数的核心规则:
    1. 每当有一个新的shared_ptr指向该内存,引用计数加1
    2. 每当有一个shared_ptr销毁/不再指向该内存,引用计数减1
    3. 当引用计数变为 0 时,说明「没有任何指针使用这块内存了」,此时智能指针自动调用delete释放内存。

4. 智能指针与传统指针的对比(完整补充优缺点)

✅ 智能指针的优点(大纲要求+补充)
  1. 核心优势:自动释放内存,彻底避免内存泄漏
  2. 避免「悬挂指针/野指针」:内存释放后,智能指针自动置空,不会残留无效地址;
  3. 避免「重复释放内存」:智能指针内部有状态管理,不会对同一块内存调用多次delete
  4. 异常安全:即使程序执行中抛出异常,智能指针对象的析构函数依然会执行,内存会被释放;
  5. 代码更简洁:省去手动写delete的繁琐,代码可读性和可维护性大幅提升。
✅ 智能指针的局限
  1. 性能开销:shared_ptr的引用计数是「原子操作」,有轻微的性能损耗;unique_ptr几乎和裸指针无区别,无开销;
  2. 兼容性问题:和C语言接口混用时,部分C函数只接收裸指针,需要用智能指针的get()方法获取裸指针;
  3. 无法解决「循环引用」:shared_ptr的致命缺陷,必须配合weak_ptr解决;
  4. 不能管理「栈内存」:智能指针只能管理堆内存(new/new[]申请的内存),如果指向栈内存,会导致重复释放内存崩溃。

二、std::unique_ptr --- 独占式智能指针

✅ 核心定位:C++中最常用、最推荐优先使用的智能指针 ,轻量级、高效率,几乎无性能开销;

✅ 核心语义:独占所有权 ------ 一块动态内存,只能被一个unique_ptr对象拥有,不允许拷贝、不允许赋值,是「一对一」的绑定关系。

1. std::unique_ptr 的定义与基本语法

✅ 头文件必加
cpp 复制代码
#include <iostream>
#include <memory> // 智能指针的头文件,缺一不可
using namespace std;
✅ 定义unique_ptr的完整语法(3种常用方式)
cpp 复制代码
// 方式1:创建空的unique_ptr,不指向任何内存
unique_ptr<int> p1;

// 方式2:创建时直接指向一块动态内存(推荐)
unique_ptr<int> p2(new int(100)); // p2指向堆内存中的int变量,值为100

// 方式3:C++14推荐写法:make_unique (最安全,避免内存泄漏)
unique_ptr<int> p3 = make_unique<int>(200); 

✅ 补充重点:make_unique 是C++14引入的,比直接new更安全!因为如果在创建智能指针时抛出异常,make_unique能避免内存泄漏,开发中优先用make_unique创建unique_ptr

✅ 核心特性:独占语义,禁止拷贝和赋值

unique_ptr的「独占」是强制的,编译器会直接拒绝拷贝和赋值操作,编译报错,从语法层面杜绝错误:

cpp 复制代码
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = p1; // 编译报错:禁止拷贝
unique_ptr<int> p3(p1);  // 编译报错:禁止拷贝构造
p3 = p1;                 // 编译报错:禁止赋值
✅ 唯一例外:所有权的转移(move语义,大纲重点)

unique_ptr虽然不能拷贝,但可以转移所有权 ------ 把A的内存所有权,转移给B,转移后A变成空指针,B拥有这块内存的所有权。

实现方式:通过 std::move() 函数实现,这是unique_ptr的核心用法之一:

cpp 复制代码
unique_ptr<int> p1 = make_unique<int>(100);
unique_ptr<int> p2 = move(p1); // 转移所有权:p1 → p2

cout << *p2 << endl; // 输出:100 ,p2拥有内存
// cout << *p1 << endl; // 报错!p1已经是空指针,没有所有权了

2. std::unique_ptr 的生命周期管理

✅ 核心特性:超出作用域,自动释放内存

这是智能指针的核心能力,unique_ptr对象生命周期结束时,析构函数自动调用delete,释放指向的内存,无需手动操作:

cpp 复制代码
void test() {
    unique_ptr<int> p = make_unique<int>(100);
    cout << *p << endl; // 正常使用
} // 函数执行结束,p超出作用域,自动释放内存,无需写delete!
✅ 与裸指针的转换

unique_ptr提供了 get() 成员方法,用来获取其封装的裸指针,适用于「需要传裸指针给C函数」的场景,这是唯一能拿到裸指针的方式:

cpp 复制代码
unique_ptr<int> p = make_unique<int>(100);
int* raw_ptr = p.get(); // 获取裸指针
cout << *raw_ptr << endl; // 输出:100

❗ 致命禁忌:绝对不要用get()返回的裸指针,再创建一个智能指针!会导致同一块内存被两个智能指针管理,最终重复释放内存崩溃!

reset()release() 方法的使用(大纲重点+区分+案例)

这是unique_ptr最常用的两个成员方法,功能完全不同,小白必须严格区分,是高频考点!

reset() 方法:重置智能指针
  • 功能1:如果调用reset()不带参数 → 释放当前指向的内存,智能指针置空;
  • 功能2:如果调用reset(new xxx) → 先释放当前指向的内存,再让智能指针指向新的内存;
cpp 复制代码
unique_ptr<int> p = make_unique<int>(100);
p.reset(); // 释放内存,p变为空指针
p.reset(new int(200)); // p现在指向新的内存,值为200
release() 方法:释放所有权(不释放内存)
  • 功能:放弃对内存的所有权,返回封装的裸指针,智能指针本身置空;
  • 关键区别:release() 只「交权」,不会释放内存 ,返回的裸指针需要程序员手动delete,否则内存泄漏;
cpp 复制代码
unique_ptr<int> p = make_unique<int>(100);
int* raw_ptr = p.release(); // p置空,返回裸指针
cout << *raw_ptr << endl;   // 输出:100
delete raw_ptr; // 必须手动释放内存!否则内存泄漏

3. std::unique_ptr 的应用场景

unique_ptr是C++中使用频率最高 的智能指针,因为它轻量、高效、安全,所有「资源独占」的场景都优先用它,优先级 > shared_ptr

✅ 场景1:管理普通的动态分配内存(最基础)

替代裸指针,管理new出来的单个对象/数组,自动释放内存,这是最常用的场景:

cpp 复制代码
// 管理单个对象
unique_ptr<string> p_str = make_unique<string>("hello C++");
// 管理数组(C++11支持,补充大纲)
unique_ptr<int[]> p_arr = make_unique<int[]>(5); // 数组大小为5
p_arr[0] = 10; p_arr[1] = 20;
✅ 场景2:实现 RAII(资源获取即初始化)模式

RAII是智能指针的灵魂,unique_ptr是实现RAII的最佳选择。除了内存,还可以用它管理「文件句柄、网络连接、锁」等资源,只要自定义删除器即可,本质都是「构造时获取资源,析构时释放资源」。

✅ 场景3:在容器中使用 unique_ptr 存储动态分配的对象(大纲重点)

C++的容器(比如vector)中绝对不要存裸指针 ,会导致内存泄漏;存unique_ptr是最优选择,因为容器中的元素是独占的:

cpp 复制代码
vector<unique_ptr<int>> vec;
vec.push_back(make_unique<int>(10));
vec.push_back(make_unique<int>(20));
// 遍历容器
for(auto& p : vec) {
    cout << *p << " "; // 输出:10 20
}
// 容器销毁时,自动释放所有unique_ptr的内存,无泄漏
✅ 补充:项目高频场景
  • 函数的返回值:函数返回unique_ptr时,编译器会自动做「移动语义」,无需手动std::move(),返回的对象独占内存;
  • 类的成员变量:类中如果有指针成员,优先用unique_ptr,避免类对象销毁时忘记释放内存;
  • 局部临时指针:函数内的临时动态内存,用unique_ptr管理,函数结束自动释放。

4. std::unique_ptrstd::shared_ptr 的比较

两者是最核心的两个智能指针,区别是面试必考考点,这里用表格清晰对比,一目了然:

对比维度 std::unique_ptr std::shared_ptr
所有权 独占式,一对一 共享式,一对多
引用计数 无,无需计数 有,核心依赖引用计数
性能开销 几乎无开销,和裸指针一致 有轻微开销(原子操作维护计数)
拷贝赋值 禁止拷贝、禁止赋值 允许拷贝、允许赋值
内存管理 析构时直接释放内存 引用计数为0时释放内存
内存泄漏 几乎不会出现 存在「循环引用」导致的内存泄漏
适用场景 优先使用,所有独占资源的场景 多对象共享同一份资源的场景
核心优势 轻量、高效、安全 灵活,支持资源共享

✅ 黄金原则:能用unique_ptr的地方,绝对不用shared_ptr


三、std::shared_ptr --- 共享式智能指针

✅ 核心定位:共享所有权的智能指针 ,解决unique_ptr「不能共享资源」的痛点;

✅ 核心实现:基于引用计数 ,多个shared_ptr可以指向同一块动态内存,内存的释放时机由「最后一个指针」决定;

✅ 核心特点:支持拷贝、支持赋值,是C++中最灵活的智能指针,但有轻微性能开销。

1. std::shared_ptr 的定义与基本语法

✅ 基础语法(和unique_ptr一致,头文件还是<memory>
cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 方式1:创建空的shared_ptr
    shared_ptr<int> p1;
    
    // 方式2:直接new创建
    shared_ptr<int> p2(new int(100));
    
    // 方式3:推荐写法:make_shared (重中之重,补充大纲)
    shared_ptr<int> p3 = make_shared<int>(200);
    return 0;
}

✅ 补充核心考点:为什么优先用make_shared

  1. 性能更高:make_shared会一次性分配「对象内存+引用计数内存」,而new的方式需要分配两次,效率低;
  2. 更安全:避免内存泄漏,比如在创建智能指针时抛出异常,make_shared能保证内存被正确管理;
  3. 代码更简洁:无需写new,可读性更好。
✅ 核心特性:引用计数机制(大纲重点+可视化理解)

这是shared_ptr的核心,必须彻底理解!我们用代码+文字+可视化,让小白一看就懂:

cpp 复制代码
shared_ptr<int> p1 = make_shared<int>(100); // 引用计数 = 1
shared_ptr<int> p2 = p1; // 拷贝,引用计数 +=1 → 2
shared_ptr<int> p3 = p2; // 赋值,引用计数 +=1 → 3

cout << "p1的引用计数:" << p1.use_count() << endl; // 输出3
cout << "p2的引用计数:" << p2.use_count() << endl; // 输出3
cout << "p3的引用计数:" << p3.use_count() << endl; // 输出3

✅ 可视化引用计数变化:

复制代码
内存地址(0x123) → 值=100 | 引用计数=3
 ↑    ↑    ↑
p1   p2   p3

✅ 引用计数的核心规则(再强调):

  1. 新创建一个shared_ptr指向内存 → 计数=1;
  2. 拷贝/赋值一个shared_ptr → 计数+1;
  3. 一个shared_ptr销毁/置空/重置 → 计数-1;
  4. 计数=0 → 自动释放内存,销毁对象。
✅ 何时释放内存

只有当最后一个指向该内存的shared_ptr 被销毁/置空时,引用计数变为0,内存才会被释放:

cpp 复制代码
shared_ptr<int> p1 = make_shared<int>(100); // 计数=1
{
    shared_ptr<int> p2 = p1; // 计数=2
} // p2超出作用域,计数-1 → 1
// p1还在,内存不会释放
p1.reset(); // p1置空,计数-1 →0,内存释放!

2. std::shared_ptr 的生命周期管理

✅ 核心成员方法

shared_ptr的成员方法和unique_ptr大部分一致,但多了几个和「引用计数」相关的方法,都是高频使用的:

  1. use_count() :返回当前的引用计数,调试时非常有用;
  2. unique() :判断当前shared_ptr是否是「独占」该内存(计数=1时返回true);
  3. reset() :重置指针,计数-1,释放内存(计数=0时);
  4. get() :获取封装的裸指针,慎用,禁忌和unique_ptr一致;
  5. operator bool() :判断指针是否为空,等价于p != nullptr

代码案例:

cpp 复制代码
shared_ptr<int> p = make_shared<int>(100);
cout << "计数:" << p.use_count() << endl; // 1
cout << "是否独占:" << p.unique() << endl; // true
p.reset(new int(200)); // 计数变为1,指向新内存
cout << *p << endl; // 200
✅ 最大痛点:循环引用问题

这是shared_ptr致命缺陷,也是面试必考的顶级考点,小白必须理解「什么是循环引用」「为什么会内存泄漏」「怎么解决」。

✔ 什么是循环引用?

两个(或多个)shared_ptr对象,互相指向对方,形成一个「闭环」,导致它们的引用计数永远无法变为0,内存永远不会被释放,造成内存泄漏

✔ 代码演示:循环引用导致内存泄漏(必看)
cpp 复制代码
// 定义两个类,互相持有对方的shared_ptr
class B; // 前置声明
class A {
public:
    shared_ptr<B> b_ptr;
    ~A() { cout << "A被销毁了" << endl; }
};
class B {
public:
    shared_ptr<A> a_ptr;
    ~B() { cout << "B被销毁了" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b_ptr = b; // A持有B的指针,b的计数=2
    b->a_ptr = a; // B持有A的指针,a的计数=2
} // 函数结束,a和b销毁,计数各减1 → 都是1,永远不为0!
// 结果:A和B的析构函数都不会执行,内存泄漏!

✅ 问题本质:闭环形成后,两个对象的引用计数永远是1,智能指针无法释放内存,这是shared_ptr自身无法解决的问题,必须依赖std::weak_ptr

3. std::shared_ptr 的应用场景

shared_ptr的适用场景非常明确:当一块内存需要被多个对象/多个地方共享使用时 ,就用它,其他场景优先用unique_ptr

  1. 多对象共享同一份资源:比如多个线程共享一个数据源、多个类对象共享一个配置文件指针;
  2. 容器中存储需要共享的对象:比如vector<shared_ptr<Person>>,多个容器元素共享同一个Person对象;
  3. 函数参数传递:需要传递指针,且希望调用方和被调用方都能共享该资源;
  4. 类的成员变量:类中需要持有其他类的指针,且希望多个对象共享该指针。

4. std::shared_ptrstd::unique_ptr 的比较

和本章第一节的表格完全一致,这里只强调黄金原则

✅ 开发优先级:unique_ptr > shared_ptr

✅ 选择原则:能独占,就不共享;共享是万不得已的选择。


四、std::weak_ptr --- 弱引用智能指针

✅ 核心定位:shared_ptr的「辅助工具」 ,没有独立的使用价值,必须和shared_ptr配合使用;

✅ 核心作用:解决shared_ptr的循环引用问题 ,这是它唯一的核心价值;

✅ 核心特点:弱引用,不拥有内存所有权,不修改引用计数,不会导致内存泄漏。

1. std::weak_ptr 的定义与基本语法

✅ 核心认知(小白必须记住)
  1. weak_ptr弱引用 :它可以「观察」shared_ptr指向的内存,但不拥有该内存的所有权
  2. weak_ptr 不会修改引用计数:无论有多少个weak_ptr指向该内存,shared_ptr的引用计数都不会变化;
  3. weak_ptr 不能直接使用内存:它没有operator*operator->,不能解引用,必须先转换为shared_ptr才能使用;
  4. weak_ptr 可以默认构造,但不能直接用new构造,只能通过shared_ptr赋值构造。
✅ 基本语法
cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp = make_shared<int>(100);
    weak_ptr<int> wp = sp; // 用shared_ptr构造weak_ptr,sp的计数不变!
    
    cout << "sp的计数:" << sp.use_count() << endl; // 1
    cout << "wp的计数:" << wp.use_count() << endl; // 1 (只是读取,不修改)
    return 0;
}
✅ 核心:如何解决shared_ptr的循环引用问题

解决思路:把循环引用中的一方,从shared_ptr改为weak_ptr,打破闭环,让引用计数可以正常减到0。

修复之前的循环引用案例,只需要改一行代码:

cpp 复制代码
class B;
class A {
public:
    shared_ptr<B> b_ptr;
    ~A() { cout << "A被销毁了" << endl; }
};
class B {
public:
    weak_ptr<A> a_ptr; // 关键修改:把shared_ptr改为weak_ptr
    ~B() { cout << "B被销毁了" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b_ptr = b; // b的计数=2
    b->a_ptr = a; // a的计数=1 !!wp不会增加计数
} // 函数结束,a销毁→计数0,A被释放;b销毁→计数0,B被释放
// 结果:A和B的析构函数都执行,内存正常释放,无泄漏!

✅ 修复原理:B中的a_ptrweak_ptr,不会增加a的引用计数,闭环被打破,计数可以正常减到0,内存被释放。

2. std::weak_ptr 的生命周期管理

✅ 核心成员方法

weak_ptr的成员方法不多,但每一个都是核心,必须全部掌握,都是高频使用的:

lock() 方法 ------ 最核心的方法
  • 功能:尝试将weak_ptr转换为shared_ptr,返回一个shared_ptr对象;
  • 关键规则:如果原shared_ptr指向的内存有效(未释放) ,则返回的shared_ptr有效,计数+1;如果内存已释放 ,则返回空的shared_ptr
  • 作用:这是weak_ptr唯一能访问内存的方式,保证访问时内存一定有效,不会出现野指针。

代码案例:

cpp 复制代码
shared_ptr<int> sp = make_shared<int>(100);
weak_ptr<int> wp = sp;

// 转换为shared_ptr,访问内存
shared_ptr<int> sp2 = wp.lock();
if(sp2) { // 判断是否转换成功
    cout << *sp2 << endl; // 100
}

// 释放原内存
sp.reset();
shared_ptr<int> sp3 = wp.lock();
if(!sp3) {
    cout << "内存已释放,转换失败" << endl;
}
expired() 方法
  • 功能:判断weak_ptr观察的内存是否已过期(已释放)
  • 返回值:true → 内存已释放,false → 内存有效;
  • 等价于:wp.use_count() == 0,但效率更高。
use_count() 方法
  • 功能:读取当前shared_ptr的引用计数,不修改计数
reset() 方法
  • 功能:重置weak_ptr,让它不再观察任何内存。

3. std::weak_ptr 的应用场景

weak_ptr的应用场景非常固定,都是围绕「解决循环引用」和「安全观察共享资源」展开,开发中常用的场景有:

  1. 解决shared_ptr的循环引用:这是最核心、最常用的场景,没有之一;
  2. 缓存场景:比如缓存一个对象的指针,用weak_ptr观察,当对象被释放时,缓存自动失效,不会访问到无效内存;
  3. 监听器模式:比如事件监听器,用weak_ptr持有监听对象,避免监听器和被监听对象形成循环引用;
  4. 树形结构:树的父节点用shared_ptr持有子节点,子节点用weak_ptr持有父节点,打破循环引用。

✅ 总结:weak_ptr是「安全的观察者」,它只看不用,不会影响资源的生命周期。


相关推荐
fqbqrr2 小时前
2601C++,模块基础
c++
海南java第二人2 小时前
SpringBoot启动流程深度解析:从入口到容器就绪的完整机制
java·开发语言
星火开发设计2 小时前
C++ queue 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识·队列
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 394 题:字符串解码
数据结构·c++·结构与算法
DICOM医学影像2 小时前
2. go语言从零实现以太坊客户端-查询区块链账户余额
开发语言·golang·区块链·以太坊·web3.0·hardhat
Data_agent2 小时前
Python 编程实战:函数与模块化编程及内置模块探索
开发语言·python
new_zhou2 小时前
vs2019+qt工程中生成dump文件及调试
开发语言·qt·visual studio·dump调试
栈与堆3 小时前
LeetCode 19 - 删除链表的倒数第N个节点
java·开发语言·数据结构·python·算法·leetcode·链表
一路向北·重庆分伦3 小时前
03-01:MQ常见问题梳理
java·开发语言