跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 智能指针原理与核心实现解析

手动管理堆内存易导致泄露或异常未释放。通过析构函数自动回收资源是 RAII 的核心思想。从基础封装出发,逐步推导 auto_ptr、unique_ptr、shared_ptr 及 weak_ptr 的实现逻辑,涵盖引用计数、循环引用解决、删除器定制及线程安全机制,帮助深入理解 C++ 智能指针底层原理。

并发大师发布于 2026/3/21更新于 2026/7/3032 浏览
C++ 智能指针原理与核心实现解析

手动管理堆内存的隐患

在 C++ 中,如果我们直接使用 new 分配堆内存,就必须确保在合适的时机调用 delete 释放。一旦疏忽,就会导致内存泄露。这在大型项目中是严重的隐患:若是低频调用的模块,可能数周甚至数月后服务器才崩溃;若是高频路径,几天内就可能引发故障,带来巨大的经济损失。

即便开发者有很强的 delete 意识,在复杂场景下依然容易出错。例如在抛出异常时,如果中途分配了多个对象却未释放就发生异常,必须确保异常处理逻辑能正确清理资源。若异常传播到更外层,清理逻辑会变得极其繁琐。因此,将内存管理权交给系统自动处理是更优的选择。

RAII 与析构函数

RAII(资源获取即初始化)的核心思想是利用对象的生命周期来管理资源。当对象超出作用域时,其析构函数会自动执行。我们可以封装一个类来持有指针,利用析构函数自动释放内存。

#include <iostream>
using namespace std;

template<class T>
class Autoptr {
    using Ptr = T*;
    using Ref = T&;
public:
    Autoptr(T* ptr) : _ptr(ptr) {}
    ~Autoptr() {
        if (_ptr) {
            cout << "指针内存释放" << endl;
            delete _ptr;
        }
    }
private:
    Ptr _ptr;
};

int main() {
    {
        Autoptr<int> ptr(new int);
    }
    cout << "作用域结束" << endl;
    return 0;
}

效果不错。为了更像原生指针使用,我们还可以重载操作符,使其支持解引用和成员访问。

template<class T>
class Autoptr {
    using Ptr = T*;
    using Ref = T&;
:
    (T* ptr = ) : _ptr(ptr) {}
    ~() {
         (_ptr) {
            cout <<  << endl;
             _ptr;
        }
    }
    Ref *()  {
         (!_ptr)  ();
         *_ptr;
    }
    Ptr ->()   {
         _ptr;
    }
    {  _ptr != ; }
:
    Ptr _ptr = ;
};
public
Autoptr
nullptr
Autoptr
if
"指针内存释放"
delete
operator
const
if
throw
runtime_error
"空指针"
return
operator
const
noexcept
return
explicit operator bool() const
return
nullptr
private
nullptr

auto_ptr 的局限性

如果在当前作用域外还需要使用该指针怎么办?普通的封装无法让指针'逃出'作用域,因为析构时会销毁底层资源。我们需要一种机制让其他对象继承对底层指针的管理权。

标准库早期的 auto_ptr 采用了这种所有权转移的方式:拷贝或赋值时,原对象会放弃所有权并置空。

template<class T>
class AutoPtr {
    using Ptr = T*;
    using Ref = T&;
public:
    explicit AutoPtr(T* ptr = nullptr) : _ptr(ptr) {}
    ~AutoPtr() noexcept {
        if (_ptr) {
            cout << "指针内存释放" << endl;
            delete _ptr;
        }
    }
    // 拷贝构造:转移所有权
    AutoPtr(AutoPtr& x) {
        _ptr = x._ptr;
        x._ptr = nullptr;
    }
    // 赋值运算:转移所有权
    AutoPtr& operator=(AutoPtr& x) noexcept {
        if (&x != this) {
            _ptr = x._ptr;
            x._ptr = nullptr;
        }
        return *this;
    }
    Ref operator*() const {
        if (!_ptr) throw runtime_error("空指针");
        return *_ptr;
    }
    Ptr operator->() const noexcept { return _ptr; }
    explicit operator bool() const noexcept { return _ptr != nullptr; }
private:
    Ptr _ptr = nullptr;
};

测试发现,赋值后原指针确实变空了。但这种剥夺式赋值在实际开发中极易导致悬空指针问题,因此在 C++11 中被弃用,C++17 彻底移除。

unique_ptr:独占所有权

很多时候,我们只希望某个堆内存被一个对象管控。unique_ptr 应运而生,它禁止拷贝,仅支持移动语义。

template<class T>
class UniquePtr {
    using Ptr = T*;
    using Ref = T&;
public:
    explicit UniquePtr(T* ptr = nullptr) : _ptr(ptr) {}
    ~UniquePtr() noexcept {
        if (_ptr) {
            cout << "指针内存释放" << endl;
            delete _ptr;
        }
    }
    // 禁止拷贝
    UniquePtr(const UniquePtr&) noexcept = delete;
    UniquePtr& operator=(const UniquePtr&) noexcept = delete;
    // 支持移动
    UniquePtr(UniquePtr&& x) noexcept {
        if (this != &x) swap(x);
    }
    UniquePtr& operator=(UniquePtr&& x) noexcept {
        if (this != &x) swap(x);
        return *this;
    }
    Ref operator*() const {
        if (!_ptr) throw runtime_error("空指针");
        return *_ptr;
    }
    Ptr operator->() const noexcept { return _ptr; }
    Ptr get() const noexcept { return _ptr; }
    Ptr release() noexcept {
        Ptr ret = _ptr;
        _ptr = nullptr;
        return ret;
    }
    void reset(Ptr ptr = nullptr) noexcept {
        if (_ptr) delete _ptr;
        _ptr = ptr;
    }
    void swap(UniquePtr& x) noexcept {
        std::swap(_ptr, x._ptr);
    }
private:
    Ptr _ptr = nullptr;
};

shared_ptr:共享所有权

auto_ptr 的转移方式并不符合所有需求,同一块地址往往需要被多个指针共同维护。shared_ptr 通过引用计数来实现这一点:拷贝时不转移指针,而是增加计数;析构时减少计数,归零则释放内存。

template<class T>
class SharedPtr {
    using Ptr = T*;
    using Ref = T&;
public:
    explicit SharedPtr(T* ptr = nullptr)
        : _ptr(ptr), _pcount(ptr ? new size_t(1) : nullptr) {}
    
    ~SharedPtr() noexcept {
        if (_ptr && !(*--_pcount)) {
            cout << "指针内存释放" << endl;
            delete _ptr;
            delete _pcount;
        }
    }
    
    SharedPtr(const SharedPtr& x) noexcept {
        _ptr = x._ptr;
        _pcount = x._pcount;
        if (_ptr) ++*_pcount;
    }
    
    SharedPtr& operator=(const SharedPtr& x) noexcept {
        if (&x != this) {
            SharedPtr temp(x);
            swap(temp);
        }
        return *this;
    }
    
    Ref operator*() const {
        if (!_ptr) throw runtime_error("空指针");
        return *_ptr;
    }
    Ptr operator->() const noexcept { return _ptr; }
    Ptr get() const noexcept { return _ptr; }
    size_t use_count() const noexcept {
        if (!_ptr) return 0;
        return *_pcount;
    }
private:
    void swap(SharedPtr& x) noexcept {
        std::swap(_ptr, x._ptr);
        std::swap(_pcount, x._pcount);
    }
    Ptr _ptr = nullptr;
    size_t* _pcount = nullptr;
};

多线程安全

如果多个线程同时访问同一个 shared_ptr,引用计数的增减必须是原子操作。我们可以引入 <atomic> 来保证线程安全。

#include <atomic>
// ... 修改 _pcount 类型为 atomic<size_t>* ...
explicit SharedPtr(T* ptr = nullptr)
    : _ptr(ptr), _pcount(ptr ? new atomic<size_t>(1) : nullptr) {}

~SharedPtr() noexcept {
    if (_ptr && !(_pcount && --*_pcount == 0)) {
        cout << "指针内存释放" << endl;
        delete _ptr;
        delete _pcount;
    }
}
// ... 其余部分类似 ...

weak_ptr:打破循环引用

在使用 shared_ptr 构建双向链表等结构时,容易出现循环引用,导致引用计数永远无法归零,造成内存泄露。

template<class T>
struct ListNode {
    ListNode(const T& val) : _val(val) {}
    SharedPtr<ListNode<T>> _l, _r;
    T _val;
};

int main() {
    SharedPtr<ListNode<int>> ln1(new ListNode(1)), ln2(new ListNode(1));
    ln1->_r = ln2;
    ln2->_l = ln1;
    return 0;
}

这种情况下,ln1 和 ln2 互相持有对方的强引用,即使离开作用域,计数也不会降为 0。解决之道是使用 weak_ptr,它只观察而不增加引用计数。

template<class T>
class WeakPtr {
    using Ptr = T*;
    using Ref = T&;
public:
    explicit WeakPtr(const SharedPtr<T>& sp = SharedPtr<T>()) : _ptr(sp.get()) {}
    
    WeakPtr(const WeakPtr& x) noexcept {
        _ptr = x._ptr;
    }
    
    Ref operator*() const {
        if (!_ptr) throw runtime_error("空指针");
        return *_ptr;
    }
    Ptr operator->() const noexcept { return _ptr; }
    Ptr get() const noexcept { return _ptr; }
private:
    Ptr _ptr = nullptr;
};

通过将其中一个成员改为 WeakPtr,即可打破循环,使内存正常回收。

删除器定制

上述实现主要针对单个对象。如果使用 new[] 分配数组,直接 delete 会导致未定义行为。我们需要自定义删除器。

// 仿函数方式
struct Del {
    void operator()(int* p) {
        cout << "仿函数删除器" << endl;
        delete[] p;
    }
};
std::shared_ptr<int> s(new int, Del());

// Lambda 表达式方式
auto del_ = [](int* p) {
    cout << "Lambda 删除器" << endl;
    delete[] p;
};
std::shared_ptr<int> ls(new int, del_);

对于 unique_ptr,由于 lambda 没有默认构造函数,需要显式指定模板参数类型。

底层控制块

工程级的 shared_ptr 和 weak_ptr 并非简单的指针复制。它们通常共享一个控制块(Control Block),其中包含强引用计数、弱引用计数以及原始指针。weak_ptr 持有的是指向控制块的指针,而非原始数据指针。这样即使原始数据被释放,控制块依然存在,直到弱引用也全部消失。这避免了 weak_ptr 访问已释放内存的风险。

具体实现涉及复杂的内存同步机制,此处不再展开代码细节,但理解这一设计有助于掌握智能指针的本质。

目录

  1. 手动管理堆内存的隐患
  2. RAII 与析构函数
  3. auto_ptr 的局限性
  4. unique_ptr:独占所有权
  5. shared_ptr:共享所有权
  6. 多线程安全
  7. weak_ptr:打破循环引用
  8. 删除器定制
  9. 底层控制块
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Golang 后端性能优化手册:高级优化技巧
  • MC.JS WEBMC1.8 与传统 JavaScript 开发效率对比
  • Rokid JSAR 基于 Web 技术栈的 AR 开发环境搭建与 3D 时钟实战
  • Chromium 144 Windows 编译指南:Git 安装与配置
  • OpenClaw 多飞书机器人配置指南
  • iTerm2 Snazzy 主题安装与个性化色彩配置指南
  • 国产FPGA厂家安路开发工具TD使用手册详细版
  • 冷启动数据与多阶段训练在 DeepSeek 中的作用
  • Java 堆外内存释放核心技术:从 Unsafe 到 ByteBuffer 的完整回收链解析
  • openclaw-termux:在 Android 上部署 OpenClaw AI Gateway
  • 2026 年主流 AI 大模型实测排名与选型指南
  • 基于 SpringBoot 的乡镇居民诊疗信息系统设计与实现
  • 基于 Neo4j 和 py2neo 的知识图谱搭建指南
  • Claude Code 安装配置与使用指南
  • Swagger MCP 实战:基于 Spring Boot 与 Spring AI 将 OpenAPI 转为工具
  • 基于 cpolar 内网穿透实现 Open-Lovable 远程访问方案
  • TypeScript 与 JavaScript 对比:主流框架为何转向 TypeScript
  • LeetCode 滑动窗口算法入门
  • Python 爬虫架构:基于 Selenium 与 ScrapyRT 的高并发动态网页抓取
  • SkyWalking 与 Spring Cloud Alibaba 全链路追踪实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online