当前位置: 首页 > news >正文

C++11 智能指针:从原理到实现

文章目录

  • 一、引言
  • 二、智能指针的核心原理:RAII
  • 三、智能指针的基本使用
    • 1. `std::unique_ptr`
    • 2. `std::shared_ptr`
    • 3. `std::weak_ptr`
  • 三、智能指针的实现原理
    • 1. `std::unique_ptr` 的实现
    • 2. `std::shared_ptr` 的实现
    • 3. `std::weak_ptr` 的实现
  • 四、`shared_ptr`的线程安全问题
    • 1. 引用计数的线程安全
    • 2. 指针本身的线程安全
    • 3. 解决方案

一、引言

在传统 C++ 中,动态内存管理一直是个头疼的问题。程序员需要手动使用 newdelete 来分配和释放内存,稍有不慎就会导致内存泄漏、悬空指针等问题。特别是在异常处理的场景中,资源释放的逻辑很容易被忽略。

C++11 引入的智能指针(Smart Pointer)通过 RAII(Resource Acquisition Is Initialization)技术,彻底改变了这一局面。它让内存管理变得自动化、安全化,大大降低了程序出错的概率。本文将深入探讨 C++11 中三种主要的智能指针:unique_ptrshared_ptrweak_ptr,并通过代码示例展示它们的实现原理。

二、智能指针的核心原理:RAII

RAII 是 C++ 中管理资源的一种重要编程范式,其核心思想是:资源在对象构造时获取,在对象析构时释放。智能指针正是基于这一思想,将动态内存的生命周期与对象的生命周期绑定在一起。

当我们使用智能指针管理内存时,只需要在构造时传入一个原始指针,智能指针会在其生命周期结束时自动释放该内存。即使在异常发生的情况下,栈展开(Stack Unwinding)过程也会确保智能指针的析构函数被调用,从而避免内存泄漏。

三、智能指针的基本使用

1. std::unique_ptr

unique_ptr 是一种独占式智能指针,确保同一时间只有一个智能指针指向该对象。它禁止拷贝,但支持移动语义。

创建与基本操作

#include <memory>
#include <iostream>void unique_ptr_demo() {// 创建 unique_ptrstd::unique_ptr<int> ptr1 = std::make_unique<int>(42);  // C++14 推荐写法std::unique_ptr<int> ptr2(new int(100));  // 传统写法// 使用解引用和箭头操作符std::cout << *ptr1 << std::endl;  // 输出: 42// 移动所有权std::unique_ptr<int> ptr3 = std::move(ptr1);  // ptr1 变为空if (!ptr1) {std::cout << "ptr1 is empty" << std::endl;}// 获取原始指针(谨慎使用)int* raw_ptr = ptr3.get();// 释放所有权int* released = ptr3.release();delete released;  // 需要手动释放// 重置指针ptr2.reset();  // 释放内存,ptr2 变为空
}

管理动态数组:

std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
for (int i = 0; i < 5; ++i) {arr[i] = i;
}

2. std::shared_ptr

shared_ptr 是一种共享式智能指针,使用引用计数来管理对象的生命周期。多个 shared_ptr 可以指向同一个对象,当最后一个 shared_ptr 被销毁时,对象才会被释放。

创建与引用计数:

void shared_ptr_demo() {// 创建 shared_ptrstd::shared_ptr<int> sp1 = std::make_shared<int>(10);  // 推荐写法std::shared_ptr<int> sp2(new int(20));  // 传统写法// 拷贝构造和赋值std::shared_ptr<int> sp3 = sp1;  // 引用计数+1sp2 = sp1;  // sp2 原来的对象被释放,引用计数+1// 获取引用计数std::cout << "Use count: " << sp1.use_count() << std::endl;  // 输出: 3// 自定义删除器(例如释放文件句柄)std::shared_ptr<FILE> file(fopen("test.txt", "r"), [](FILE* f) {if (f) fclose(f);});
}  // 所有 shared_ptr 离开作用域,对象被释放

3. std::weak_ptr

weak_ptr 是一种弱引用智能指针,不控制对象的生命周期,用于解决 shared_ptr 的循环引用问题。

基本使用:

void weak_ptr_demo() {std::shared_ptr<int> sp = std::make_shared<int>(100);std::weak_ptr<int> wp = sp;  // 不增加引用计数// 检查对象是否存在if (auto locked = wp.lock()) {  // 转换为 shared_ptrstd::cout << *locked << std::endl;  // 对象存在} else {std::cout << "Object expired" << std::endl;}// 查看引用计数std::cout << "Use count: " << wp.use_count() << std::endl;  // 输出: 1
}  // sp 离开作用域,对象被释放

解决循环引用:

class B;class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr;  // 使用 weak_ptr 避免循环引用~B() { std::cout << "B destroyed" << std::endl; }
};void test_cycle() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;  // 不会导致循环引用
}  // a 和 b 都能正确释放

三、智能指针的实现原理

1. std::unique_ptr 的实现

unique_ptr 的核心是禁止拷贝,只允许移动语义,并在析构时释放资源。
所以在实现 unique_ptr 时,必须禁用拷贝构造和拷贝赋值,并显式实现移动构造、移动赋值和资源管理,以确保独占资源的正确性和安全性。

    template<class T>class unique_ptr {public:unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}unique_ptr(unique_ptr&& other) : _ptr(other._ptr) {other._ptr = nullptr;}~unique_ptr() {delete _ptr;}unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;unique_ptr& operator=(unique_ptr&& other) {if (this != &other) {delete _ptr;_ptr = other._ptr;other._ptr = nullptr;}return *this;}T& operator*() const {return *_ptr;}T* operator->() const {return _ptr;}T* get() const {return _ptr;}T* release() {T* temp = _ptr;_ptr = nullptr;return temp;}void reset(T* ptr = nullptr) {if (_ptr) {delete _ptr;}_ptr = ptr;}private:T* _ptr;};

2. std::shared_ptr 的实现

shared_ptr 的核心是引用计数。

主要就是当执行拷贝构造、拷贝赋值的时候让计数器++,而当析构的时候让计数器--。如果计数器为 0 就释放掉资源。

    template<class T>class shared_ptr {public:shared_ptr(T* ptr): _ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _del(del){}~shared_ptr() {if (--(*_pcount) == 0) {_del(_ptr);delete _pcount;}}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp) {if (_ptr != sp._ptr) {if (--(*_pcount) == 0) {_del(_ptr);delete _pcount;}_pcount = sp._pcount;_ptr = sp._ptr;++(*_pcount);}return *this;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}int use_count() const {return *_pcount;}private:T* _ptr;int* _pcount;std::function<void(T*)> _del = [](T* ptr) { delete ptr; };};

3. std::weak_ptr 的实现

weak_ptr 的核心是弱引用机制,通过独立于 shared_ptr 的引用计数(弱计数)实现对资源的非拥有性访问,主要用于解决循环引用问题

下面的代码现在看不懂没关系,船到桥头自然直。

    template<class T>class weak_ptr {private:T* _ptr;std::atomic<int>* _pcount;std::atomic<int>* _wcount;public:weak_ptr() : _ptr(nullptr), _pcount(nullptr), _wcount(nullptr) {}weak_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount), _wcount(new std::atomic<int>(1)){}weak_ptr(const weak_ptr& wp): _ptr(wp._ptr), _pcount(wp._pcount), _wcount(wp._wcount){if (_wcount) {(*_wcount)++;}}~weak_ptr() {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}}weak_ptr& operator=(const shared_ptr<T>& sp) {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}_ptr = sp._ptr;_pcount = sp._pcount;_wcount = new std::atomic<int>(1);return *this;}weak_ptr& operator=(const weak_ptr& wp) {if (this != &wp) {if (_wcount && --(*_wcount) == 0) {if (_pcount && *_pcount == 0) {delete _wcount;}}_ptr = wp._ptr;_pcount = wp._pcount;_wcount = wp._wcount;if (_wcount) {(*_wcount)++;}}return *this;}shared_ptr<T> lock() const {if (expired()) {return shared_ptr<T>(nullptr);}return shared_ptr<T>(_ptr);}bool expired() const {return !_pcount || *_pcount == 0;}int use_count() const {return _pcount ? *_pcount : 0;}};

四、shared_ptr的线程安全问题

std::shared_ptr 的线程安全问题需要从 引用计数的原子性指针本身的访问语义 两方面分析。

1. 引用计数的线程安全

shared_ptr 的核心机制是通过 引用计数 管理资源生命周期,其引用计数的操作具有 原子性(由 std::atomic 保证)。具体规则如下:

  1. 原子性操作
    • 引用计数的递增 / 递减(如拷贝构造、赋值、析构)是 原子操作,多线程中无需额外同步。
    • 引用计数的查询(如 use_count())是 非原子操作,可能读取到中间状态(但不会导致程序崩溃,仅可能返回过时值)。
  2. 安全场景
    • 多个线程同时读取 shared_ptr 对象(如调用 use_count()、解引用 *ptr)是安全的,不会导致数据竞争。
    • 一个线程修改 shared_ptr(如赋值、析构),其他线程读取:由于引用计数操作是原子的,不会导致 shared_ptr 对象本身的不一致(但需注意管理对象的线程安全)。

2. 指针本身的线程安全

shared_ptr 管理的 目标对象(*_ptr)的访问 不保证线程安全,需开发者自行处理同步:

  • 数据竞争风险
    如果多个线程通过 shared_ptr 访问同一对象的 可变状态,且未加同步机制,会导致数据竞争:
std::shared_ptr<MyClass> ptr;// 线程 A
ptr->x = 10;  // 危险:未加锁,若线程 B 同时修改 x,发生数据竞争// 线程 B
int val = ptr->x;  // 危险:与线程 A 竞争

3. 解决方案

库里面的 std::shared_ptr 是保证了多个线程同时修改引用计数是原子的,不会导致数据竞争;引用计数的读取操作是安全的,但非原子,可能会读到中间状态。

我们上面实现的 shared_ptr_pcount 的类型是 int*,所以它的引用计数的读取不是线程安全的,可以将它改成 atomic<int>* 类型,用于保证引用计数的线程安全。当然,既然是多线程,那么使用互斥枷锁解决线程安全问题也可以(具体的等后面多线程部分再讲)。

		// 定义的时候:std::atomic<int>* _pcount;// 构造初始化的时候shared_ptr(T* ptr): _ptr(ptr), _pcount(new std::atomic<int>(1)){}

相关文章:

  • 为什么badmin reconfig以后始终不能提交任务
  • C#语音录制:使用NAudio库实现语音录制功能详解
  • 【CBAP50技术手册】#32 Organizational Modelling(组织建模):BA(业务分析师)的“变革导航图”
  • Ubuntu取消开机用户自动登录
  • Practice 2025.6.1—— 二叉树进阶面试题(2)
  • Python爬虫:AutoScraper 库详细使用大全(一个智能、自动、轻量级的网络爬虫)
  • GNSS终端授时之四:高精度的PTP授时
  • JDBC连不上mysql:Unable to load authentication plugin ‘caching_sha2_password‘.
  • 通俗易懂的 JS DOM 操作指南:从创建到挂载
  • uniapp uni-id 如果是正式项目,需自行实现发送邮件的相关功能
  • 【Java基础】Java基础语法到高级特性
  • WEBSTORM前端 —— 第3章:移动 Web —— 第5节:响应式网页
  • Python 训练营打卡 Day 41
  • 船舶二阶非线性响应方程的EKF与UKF参数辨识
  • 使用BERT/BiLSTM + CRF 模型进行NER进展记录~
  • PyTorch ——torchvision数据集使用
  • 缓存击穿、缓存雪崩、缓存穿透以及数据库缓存双写不一致问题
  • 落石石头检测数据集VOC+YOLO格式1185张1类别
  • 【MySQL】第13节|MySQL 中模糊查询的全面总结
  • Mixly1.0/2.0/3.0 (windows系统) 安装教程及使用常见问题解决
  • 现在给别人做网站还赚钱吗/产品如何做网络推广
  • 上海做网站企业/每天看七个广告赚40元的app
  • 网站开发公司哪家最专业/网站更换服务器对seo的影响
  • 商旅网站制作/百度客服电话人工服务
  • 哈尔滨疫情最新消息今天新增/免费关键词优化工具
  • 独立网店/北京seo软件