面试八股:C++ 智能指针

目录

对比

一,引入背景

二,std::unique_ptr

2.1 核心

2.2 场景

2.3 实现

2.4 性能与安全

2.5 建议

三,std::shared_ptr

3.1 核心

3.2 场景

3.3 实现

3.4 性能和安全

3.5 建议

四,std::weak_ptr

4.1 特性

4.2 场景

4.3 实现

4.4 性能

4.5 建议

五,最佳实践

六,智能指针--实现


内存管理库 - cppreference.cn - C++参考手册

👆包含 3 种智能指针的用法,场景,实现

  • C++ 开发中,资源管理是一个核心挑战
  • 手动管理内存,文件句柄等资源,容易导致(内存泄漏 / 悬垂指针 / 未定义行为)
  • 为了解决这些问题,C++ 标准库引入了智能指针 Smart Pointers
  • 智能指针三件套:std::unique_ptr, std::shared_ptr, std::weak_ptr

对比

特性unique_ptrshared_ptrweak_ptr
所有权独占共享无所有权(观察者)
资源管理方式移动语义转移所有权引用计数管理生命周期依赖 shared_ptr
开销裸指针开销(低)控制块+原子计数(中)共享控制块(中)
场景独占资源管理(文件句柄)资源共享(多线程,复杂对象)打破循环引用
线程安全无(需外部同步)引用计数线程安全(数据需保护)依赖 shared_ptr
实现封装裸指针控制块存储计数,计数为 0 时释放共享控制块,lock() 获取有效指针
创建方式make_uniquemake_shared从 shared_ptr 构造
  • unique_ptr 适合独占资源的高效管理
  • shared_ptr 用于共享资源的复杂场景
  • weak_ptr 辅助 shared_ptr

一,引入背景

C++11 引入的智能指针,是基于 RAII(资源获取即初始化)原则,将资源生命周期与对象生命周期绑定

智能指针在构造时获取资源,在析构时自动释放,不用手动干预

C++ 提供了三种智能指针

  1. std::unique_ptr:独占资源,单一所有权
  2. std::shared_ptr:共享资源,多方引用
  3. std::weak_ptr:观察资源,辅助管理

RAII 的本质

RAII 主张资源要在对象创建时获取,在对象销毁时释放

这种机制确保,即使发生异常或提前返回,资源也能被正确清理

智能指针通过 封装裸指针析构 的逻辑,实现 RAII 自动化

二,std::unique_ptr

独占资源的首选工具

2.1 核心

std::unique_ptr 是 C++11 引入的智能指针,采用独占所有权模型

一个资源只能被一个 unique_ptr 拥有无法复制,只能通过移动语义转移所有权

这种设计避免了资源被多方同时管理的风险,确保了清晰的所有权归属

  • 独占模式:资源归单一指针所有,避免冲突
  • 移动支持:通过 std::move 转移所有权,转移后原指针失效
  • 高效实现:无额外内存或计算开销,接近裸指针性能

2.2 场景

std::unique_ptr 是智能指针的默认选择,适用以下场景:

  • 独占模式:管理文件句柄,数据库连接,确保单一控制
  • 临时分配:管理动态对象,避免手动释放
  • 所有权转移:工厂函数中返回资源,明确调用者接管责任

2.3 实现

#include <memory>
#include <iostream>

class Device {
public:
    Device() { std::cout << "Device initialized\n"; }
    ~Device() { std::cout << "Device shutdown\n"; }
};

int main() {
    // 使用 std::make_unique 创建
    auto devPtr1 = std::make_unique<Device>();

    // 移动所有权到 devPtr2
    auto devPtr2 = std::move(devPtr1);
    if (!devPtr1) {
    std::cout << "devPtr1 is empty after move\n";
    }
    
    // devPtr2 超出作用域,资源自动释放
    return 0;
}
Device initialized
devPtr1 is empty after move
Device shutdown

std::make_unique 创建了一个 unique_ptr 管理 Device 对象

所有权从 devPtr1 移动到 devPtr2 后,devPtr1 变为 nullptr

当 devPtr2 超出作用域时,Device 对象被自动销毁

2.4 性能与安全

  • 性能:std::_unique_ptr 运行时开销极低,不需要维护引用计数或额外的数据结构,仅封装了裸指针和析构
  • 安全:std::unique_ptr 不提供内置线程安全机制,多线程访问需要外部的同步保护

2.5 建议

  1. 优先选择:管理动态资源首选 unique_ptr,简单高效
  2. 工厂函数:使用 std::make_unique 创建,避免直接 new,提高异常安全性
  3. 接口设计:在 API 种使用 unique_ptr 明确所有权转移语义

三,std::shared_ptr

共享资源的协作者

3.1 核心

std::shared_ptr 采用共享所有权模型

通过引用计数管理资源生命周期,允许多个 shared_ptr 同时引用同一资源

引用计数记录当前有多少指针引用资源,只有计数为 0,资源才会被释放

  • 共享模式:支持多方引用,资源生命周期由引用计数控制
  • 复制支持:可以复制 shared_ptr,每次复制增加计数
  • 动态管理:资源在最后一个引用消失时自动释放

3.2 场景

  • 多线程共享:资源在多个线程间传递,引用计数确保安全释放
  • 复杂关系:对象间存在多重引用,无法明确单一所有者
  • 动态模块:插件或动态加载系统需要共享核心资源

3.3 实现

#include <memory>
#include <iostream>

class SharedAsset {
public:
    SharedAsset() { std::cout << "SharedAsset created\n"; }
    ~SharedAsset() { std::cout << "SharedAsset destroyed\n"; }
};

int main() {
    // 使用 std::make_shared 创建
    auto asset1 = std::make_shared<SharedAsset>();
    std::cout << "Count after asset1: " << asset1.use_count() << "\n";
    
    // 复制到 asset2,增加计数
    auto asset2 = asset1;
    std::cout << "Count after asset2: " << asset1.use_count() << "\n";
    
    // 释放 asset1,计数减少
    asset1.reset();
    std::cout << "Count after asset1 reset: " << asset2.use_count() << "\n";
    
    // asset2 超出作用域,计数为 0,资源释放
    return 0;
}
SharedAsset created
Count after asset1: 1
Count after asset2: 2
Count after asset1 reset: 1
SharedAsset destroyed

3.4 性能和安全

  • 性能:std::shared_ptr 的开销高于 unique_ptr,因为要维护控制块(16 字节)和原子计数操作
  • 线程安全:引用计数操作是线程安全的,但资源本身需额外保护

3.5 建议

  • 谨慎使用:仅在共享资源不可避免时,使用 shared_ptr,否则优先 unique_ptr
  • 工厂函数:使用 std::make_shared 创建,提高内存分配效率
  • 避免循环:警惕循环引用,必要时引入 weak_ptr 解决

四,std::weak_ptr

资源观察的辅助利器

4.1 特性

std::weak_ptr 采用观察者模型,不拥有资源所有权,只观察 shared_ptr 管理的资源

它不影响引用计数,所以不会阻止资源释放

  • 无所有权:不参与资源管理,仅提供访问入口
  • 动态检查:通过 lock() 获取关联 shared_ptr,检查资源是否有效
  • 辅助功能:主要用于解决 shared_ptr 循环引用的问题

4.2 场景

  • 循环引用解决:对象互相引用时,使用 weak_ptr 避免资源泄漏
  • 资源缓存:观察资源状态,资源释放后自动失效
  • 非侵入访问:需要在不影响资源生命周期的情况下,访问资源

4.3 实现

#include <memory>
#include <iostream>

class CachedItem {
public:
    CachedItem() { std::cout << "CachedItem created\n"; }
    ~CachedItem() { std::cout << "CachedItem destroyed\n"; }
};

int main() {
    // 创建 shared_ptr
    auto item = std::make_shared<CachedItem>();
    std::weak_ptr<CachedItem> observer = item; // 观察 item
    
    // use_count() 返回引用计数的数值:多少个 shared_ptr 对象引用同一资源
    std::cout << "Reference count: " << item.use_count() << "\n";
    
    // 尝试获取一个 shared_ptr 对象,检查资源是否有效
    // 通过 lock() 访问资源
    if (auto locked = observer.lock()) {
        std::cout << "Item accessible via observer\n";
    }
    
    // 释放资源并置空:对于 share 引用计数减 1,对于 unique 直接释放
    item.reset();
    if (auto locked = observer.lock()) {
        std::cout << "This won't print\n";
    } else {
        std::cout << "Item released, observer invalid\n";
    }
    return 0;
}
CachedItem created
Reference count: 1
Item accessible via observer
CachedItem destroyed
Item released, observer invalid

上述例子中,std::weak_ptr 观察 shared_ptr 管理的资源,不影响计数

当 shared_ptr 释放资源后,weak_ptr 通过 lock() 检查返现资源已不可用

4.4 性能

  • 性能:std::weak_ptr 与关联的 shared_ptr 共享控制块,开销类似,但不参与计数管理
  • 线程安全:依赖 shared_ptr,访问需通过 lock() 检查有效性

4.5 建议

  1. 配合使用:必须与 shared_ptr 搭配,无法独立管理资源
  2. 有效性检查:调用 lock() 后立即检查返回的 shared_ptr 是否有效
  3. 特定场景:仅用于解决 循环引用 或观察资源

五,最佳实践

  • 工厂函数:使用 std::make_unique 和 std::make_shared 创建,避免直接 new
  • 避免裸指针:尽量不直接操作裸指针,把资源管理交给智能指针
  1. 语义清晰:API 设计中,通过智能指针类型表达所有权归属
  2. 避免混淆:不与裸指针混用,防止所有权混乱
  • 数据保护:多线程环境,智能指针不保证资源数据安全,需要额外同步
  • 共享范围:使用 shared_ptr 时,尽量缩小共享范围,减少开销
  1. 循环引用:shared_ptr 使用时,涉及循环引用,要用 weak_ptr
  2. 过度依赖:栈上对象不用智能指针,避免滥用
  3. 全局变量:避免全局的智能指针,构造细狗顺序可能引发问题

六,智能指针--实现

  • unique_ptr
    • 内部指针:封装裸指针,管理资源
    • 移动操作:移动语义转移所有权
    • 析构释放:释放资源
  • shared_ptr
    • 控制块:存储引用计数和删除器
    • 原子计数:确保线程安全更新计数
    • 资源释放:计数为 0 时释放资源
  • weak_ptr
    • 共享控制:与 shared_ptr 共享控制块
    • 不增计数:不影响资源生命周期
    • 动态检查:通过 lock() 检查资源状态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千帐灯无此声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值