C++如何使用智能指针与容器结合管理内存

在C++中,应优先使用智能指针管理容器中的动态对象,以避免内存泄漏和悬空指针。std::unique_ptr适用于独占所有权场景,性能高且无引用计数,适合std::vector等线性容器存储多态对象;而std::shared_ptr用于共享所有权,通过引用计数管理生命周期,适用于std::map等需共享资源的场景,但需警惕循环引用。混合使用原始指针与智能指针会导致双重释放、悬空指针和所有权混乱等风险,应避免。通过定制删除器,智能指针可管理文件句柄、C风格数组等非内存资源,实现RAII的通用资源管理。

c++如何使用智能指针与容器结合管理内存

在C++中,将智能指针与容器结合使用是管理动态内存、避免内存泄漏和确保资源安全的关键实践。它通过RAII(资源获取即初始化)原则,让对象在离开作用域时自动释放所持有的资源,极大地简化了内存管理,并提高了代码的健壮性。简单来说,就是把那些需要手动

new

出来的对象,用

std::unique_ptr

std::shared_ptr

包装起来,再把这些智能指针放入容器,让智能指针替你操心内存的生老病死。

解决方案

说实话,每次看到代码里大量裸指针在容器里跳来跳去,我心里都会咯噔一下。这不光是代码美观的问题,更是潜在的灾难。C++11之后,智能指针的引入简直是福音。

核心思路是:当你的容器需要存储动态分配的对象时,不要直接存储原始指针,而是存储智能指针。这解决了几个老大难的问题:

内存泄漏:容器销毁时,智能指针会自动释放其管理的内存。异常安全:即使在函数执行过程中抛出异常,智能指针也能保证内存被正确释放。所有权语义清晰

std::unique_ptr

明确表示独占所有权,

std::shared_ptr

则表示共享所有权,这让代码意图一目了然。

使用

std::unique_ptr

立即学习“C++免费学习笔记(深入)”;

当你确定容器中的每个元素都拥有其所管理对象的唯一所有权时,

std::unique_ptr

是首选。它的开销非常小,因为它不涉及引用计数。它最适合用在

std::vector

std::list

std::deque

这种线性容器中,尤其是当你需要存储多态对象时。

#include #include #include class Base {public:    virtual void greet() const { std::cout << "Hello from Base!" << std::endl; }    virtual ~Base() { std::cout << "Base destructor called." << std::endl; }};class Derived : public Base {public:    void greet() const override { std::cout << "Hello from Derived!" << std::endl; }    ~Derived() { std::cout << "Derived destructor called." << std::endl; }};// 示例:使用 unique_ptr 存储多态对象std::vector<std::unique_ptr> objects;objects.push_back(std::make_unique()); // 推荐使用 make_uniqueobjects.push_back(std::make_unique());for (const auto& obj_ptr : objects) {    obj_ptr->greet();}// 当 objects 离开作用域时,所有 Base 和 Derived 对象都会被正确销毁

使用

std::shared_ptr

如果你需要多个智能指针共同拥有同一个对象,并且在所有拥有者都消失后才释放对象,那么

std::shared_ptr

是你的选择。它通过引用计数来管理对象的生命周期,当引用计数降为零时,对象才会被销毁。这在

std::map

std::set

这种需要共享对象引用的场景中特别有用,或者在构建复杂的图结构时。

#include #include #include #include class Resource {public:    std::string name;    Resource(std::string n) : name(std::move(n)) {        std::cout << "Resource " << name << " created." << std::endl;    }    ~Resource() {        std::cout << "Resource " << name << " destroyed." << std::endl;    }};// 示例:使用 shared_ptr 存储共享资源std::map<std::string, std::shared_ptr> shared_resources;// 创建一个共享资源auto r1 = std::make_shared("DatabaseConnection");shared_resources["db_conn_1"] = r1;shared_resources["db_conn_2"] = r1; // 两个键共享同一个 Resource 对象std::cout << "Reference count for r1: " << r1.use_count() << std::endl;// 另一个 shared_ptr 也可以指向它std::shared_ptr another_ref = shared_resources["db_conn_1"];std::cout << "Reference count for r1 after another_ref: " << r1.use_count() << std::endl;// 当所有 shared_ptr 都离开作用域或被重置时,Resource 才会被销毁

记住,

std::make_unique

std::make_shared

是创建智能指针的最佳实践,它们不仅能避免重复的类型书写,还能提供异常安全保证和性能优化。

在C++容器中,何时选择

std::unique_ptr

而非

std::shared_ptr

这其实是个老生常谈的问题,但总有人会踩坑。我的经验是,能用

unique_ptr

就尽量用

unique_ptr

,除非你真的需要共享所有权。

选择

std::unique_ptr

的场景:

独占所有权:这是最核心的理由。如果一个对象在容器中被创建,并且它的生命周期完全由这个容器中的智能指针来管理,没有其他地方会“拥有”它,那么

unique_ptr

是完美的。比如,一个

std::vector<std::unique_ptr>

,每个

MyObject

实例都只属于

vector

中的一个位置。性能考量

unique_ptr

的开销非常小,因为它不涉及引用计数。这意味着更快的创建、销毁和赋值操作。在性能敏感的场景下,或者容器中对象数量非常庞大时,这点差异可能会累积起来。

shared_ptr

需要维护一个引用计数器,这会带来额外的内存开销和原子操作的性能成本。移动语义

unique_ptr

是可移动但不可复制的。这意味着你可以高效地将对象的所有权从一个

unique_ptr

转移到另一个

unique_ptr

,或者从容器中取出

unique_ptr

并转移所有权。这对于很多现代C++编程模式来说非常自然。避免循环引用

shared_ptr

最让人头疼的问题之一就是循环引用,这会导致内存泄漏(两个或多个对象相互持有

shared_ptr

,导致引用计数永远不为零)。

unique_ptr

根本没有引用计数,所以不存在这个问题。

什么时候你可能不得不考虑

std::shared_ptr

当你确实需要多个智能指针共同管理同一个对象的生命周期时。例如,你有一个图形场景,多个节点可能引用同一个纹理或模型数据;或者在一个缓存系统中,多个用户可能同时访问同一个数据块。在这种情况下,

shared_ptr

是必要的,但也要警惕

std::weak_ptr

来打破潜在的循环引用。

我个人觉得,很多人一开始会滥用

shared_ptr

,因为它听起来“更安全”,似乎什么都能管。但实际上,它带来了额外的复杂性和开销。所以,我的建议是:从

unique_ptr

开始考虑,只有当

unique_ptr

无法满足你的所有权需求时,才转向

shared_ptr

将原始指针与智能指针混合使用会带来哪些风险?

这简直是C++内存管理中的“雷区”,一不小心就会踩爆。混合使用原始指针和智能指针,尤其是当它们指向同一个动态分配的对象时,会引入一系列难以调试的严重问题。

双重释放(Double Free):这是最常见的风险。想象一下,你用

new

分配了一个对象,然后用一个

unique_ptr

管理它。接着,你又把这个对象的原始指针传给了一个函数,或者存入了一个容器,并且在某个地方对这个原始指针调用了

delete

。当

unique_ptr

离开作用域时,它会再次尝试

delete

同一个内存地址。这会导致未定义行为,通常表现为程序崩溃。

MyObject* raw_ptr = new MyObject();std::unique_ptr smart_ptr(raw_ptr); // smart_ptr 现在管理 raw_ptr// 某个地方,不小心又 delete 了delete raw_ptr; // 第一次释放// smart_ptr 离开作用域时会再次 delete,导致双重释放!

悬空指针(Dangling Pointer):当智能指针管理的对象被释放后,如果还有原始指针指向这块内存,那么这些原始指针就变成了悬空指针。对悬空指针的任何解引用操作都是未定义行为,可能导致数据损坏或崩溃。

std::unique_ptr smart_ptr = std::make_unique();MyObject* raw_ptr = smart_ptr.get(); // 获取原始指针smart_ptr.reset(); // 对象被释放了,raw_ptr 成了悬空指针raw_ptr->doSomething(); // 糟糕!访问已释放的内存

所有权语义混乱:智能指针的核心价值在于明确所有权。

unique_ptr

是独占,

shared_ptr

是共享。一旦引入原始指针,这种清晰的所有权语义就被打破了。谁负责释放内存?谁是最终的拥有者?代码变得模糊不清,维护者需要花费大量精力去推断,极易出错。

异常安全问题:在异常发生时,智能指针能够保证资源的正确释放。但如果你的代码逻辑依赖于手动管理原始指针,一旦异常抛出,那些本应被

delete

的原始指针可能就永远不会被释放,导致内存泄漏。

我的建议是:

一旦对象被智能指针管理,尽量只通过智能指针来访问它。 如果实在需要传递原始指针(比如给一些遗留C API),请确保那个API不会尝试

delete

这个指针,并且它的生命周期不会超过智能指针所管理对象的生命周期。不要从原始指针构造多个

shared_ptr

比如

std::shared_ptr p1(new T); std::shared_ptr p2(p1.get());

这会创建两个独立的

shared_ptr

控制块,导致对象被双重释放。正确的做法是

std::shared_ptr p2 = p1;

坚持“智能指针优先”的原则。 除非有非常明确的理由,否则不要在容器中存储原始指针,也不要轻易地将智能指针管理的原始指针暴露出去。

如何为

std::unique_ptr

std::shared_ptr

定制删除器以处理特殊资源?

智能指针的强大之处不仅在于管理堆内存,还在于它可以通过定制删除器(Deleter)来管理几乎任何需要“清理”的资源,比如文件句柄、网络套接字、互斥锁等等。这让智能指针成为了一个通用的RAII工具

std::unique_ptr

的定制删除器:

unique_ptr

的删除器是其模板参数的一部分。这意味着如果你使用不同的删除器,那么它们的类型也是不同的。删除器可以是一个函数对象(lambda、仿函数)或者一个函数指针。

#include #include  // For FILE* and fclose#include #include // 1. 使用函数对象(Lambda表达式)作为删除器auto file_closer = [](FILE* f) {    if (f) {        std::cout << "Closing file with lambda deleter." << std::endl;        fclose(f);    }};using FilePtr = std::unique_ptr;// 2. 使用结构体(仿函数)作为删除器struct SocketCloser {    void operator()(int* sock_fd) const { // 注意这里,通常传递原始指针类型        if (sock_fd && *sock_fd != -1) {            std::cout << "Closing socket with functor deleter: " << *sock_fd << std::endl;            // 假设这是一个真实的套接字关闭函数            // close(*sock_fd);            delete sock_fd; // 释放原始指针本身        }    }};using SocketPtr = std::unique_ptr; // 假设 int 是文件描述符void demo_unique_ptr_custom_deleter() {    // 示例1: 管理文件句柄    FILE* f = fopen("test.txt", "w");    if (f) {        FilePtr managed_file(f, file_closer);        fprintf(f, "Hello from unique_ptr!n");        // managed_file 离开作用域时,file_closer 会被调用    } else {        std::cerr << "Failed to open file." << std::endl;    }    // 示例2: 管理一个假想的套接字描述符    int* sock_fd = new int(123); // 假设这是从某个 API 获取的    SocketPtr managed_socket(sock_fd, SocketCloser{});    // managed_socket 离开作用域时,SocketCloser() 会被调用}

可以看到,

unique_ptr

的删除器类型是其类型签名的一部分,这在模板编程时可能需要注意。

std::shared_ptr

的定制删除器:

shared_ptr

的定制删除器是通过其构造函数传入的,它不需要作为模板参数。这意味着所有使用相同类型对象的

shared_ptr

,即使有不同的删除器,它们的类型也是相同的。

#include #include #include // 1. 使用函数指针作为删除器void custom_array_deleter(int* p) {    std::cout << "Custom array deleter called. Deleting int array." << std::endl;    delete[] p;}// 2. 使用 Lambda 表达式作为删除器void demo_shared_ptr_custom_deleter() {    // 示例1: 管理C风格动态数组    std::shared_ptr arr_ptr(new int[10], custom_array_deleter);    arr_ptr.get()[0] = 100;    std::cout << "Array element: " << arr_ptr.get()[0] << std::endl;    // arr_ptr 离开作用域时,custom_array_deleter 会被调用    // 示例2: 管理一个通过 C API 分配的内存    // 假设有一个 C 函数 `void* allocate_buffer(size_t size)`    // 和一个 `void free_buffer(void* ptr)`    auto c_buffer_deleter = [](void* p) {        std::cout << "Custom C buffer deleter called." << std::endl;        // free_buffer(p); // 假设调用 C API 的释放函数        std::free(p); // 这里用 std::free 模拟    };    void* raw_buffer = std::malloc(128); // 模拟 C API 分配    std::shared_ptr managed_buffer(raw_buffer, c_buffer_deleter);    // managed_buffer 离开作用域时,c_buffer_deleter 会被调用}

什么时候需要定制删除器?

管理非堆内存资源:比如文件句柄(

FILE*

),数据库连接,网络套接字,

malloc

分配的内存(需要

free

而不是

delete

)。C风格数组

new T[N]

分配的内存需要用

delete[]

释放,而

std::unique_ptr

默认只调用

delete T

。所以,对于

std::unique_ptr

,它会自动使用

delete[]

,但如果你写的是

std::unique_ptr

去管理

new T[N]

,就需要定制删除器了。

std::shared_ptr

则总是需要定制删除器来处理C风格数组。资源池管理:如果你从一个资源池中获取对象,释放时需要将其归还到池中,而不是真正销毁。定制删除器可以实现这种“回收”逻辑。

定制删除器是一个非常强大的特性,它让智能指针的应用场景远超简单的内存管理。它真正体现了RAII的精髓,将资源的生命周期管理抽象化,让你的代码更干净、更安全。

以上就是C++如何使用智能指针与容器结合管理内存的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1474444.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
九天算力平台任务:本地电脑关机后,计算任务还会继续运行吗?
上一篇 2026年5月10日 11:01:40
HTML语义化:单列数据展示的最佳实践与替代方案
下一篇 2026年5月10日 11:01:46

相关推荐

  • JS如何实现懒加载组件?React.lazy

    在javascript中实现react组件懒加载的核心方法是使用react.lazy和suspense。react.lazy通过动态import()将组件拆分为独立代码块,suspense通过fallback属性定义加载时的占位内容,从而实现按需加载,显著提升应用初始加载性能。该方案解决了大型单页应…

    2026年5月10日
    100
  • 如何明确指定Go语言函数多返回值类型

    在Go语言中,函数可以返回多个值,这是一种强大的特性。然而,在处理多返回值时,有时会遇到代码可读性问题,尤其是在不清楚函数返回值类型的情况下。例如: func randomNumber() (int, error) { return 4, nil}func main() { nr, err := r…

    2026年5月10日
    000
  • 怎样使用匿名联合体 特殊内存访问场景应用实例

    匿名联合体是一种无名联合体,其成员直接提升到外层作用域,允许以不同视图访问同一内存区域,常用于硬件寄存器操作和内存布局精确控制,提升代码可读性与维护性。 匿名联合体,在我看来,它更像是一种语言层面的“透视镜”,允许我们以不同的视角去观察和操作同一块内存区域。它没有自己的变量名,而是将其成员直接提升到…

    2026年5月10日
    000
  • HTML容器怎么创建_HTML的div和span容器使用区别

    div是块级元素,独占一行,适合构建大块区域;span是行内元素,不打断文本流,适合修饰局部文本。 在HTML中,我们创建容器主要依赖 和 这两个标签,它们就像是网页布局的积木。核心的区别在于它们默认的显示行为: 是块级元素,天生就喜欢独占一行,适合构建页面结构的大块区域;而 是行内元素,更像文本的…

    2026年5月10日
    100
  • 如何用Python实现一个简单的爬虫?

    答案:使用Python实现简单爬虫最直接的方式是结合requests和BeautifulSoup库。首先通过requests发送HTTP请求获取网页HTML内容,并设置headers、超时和编码;然后利用BeautifulSoup解析HTML,通过CSS选择器提取目标数据,如文章标题和链接;为避免被…

    2026年5月10日
    100
  • JavaScript 中处理页面重载时的瞬时错误

    在 JavaScript 应用中,页面重载是一个常见的操作,但瞬时网络错误可能会中断重载过程,导致页面加载失败。为了提高应用的健壮性,我们需要采取一些措施来处理这种情况。 使用 navigator.onLine 检测网络状态 navigator.onLine 属性可以用来检测浏览器当前的网络连接状态…

    用户投稿 2026年5月10日
    000
  • JS脚本的基本结构是什么

    javascript脚本的基本结构由语句、注释、变量声明、数据类型、函数、控制流以及对象和数组构成,其执行过程涉及浏览器解析html时暂停并加载脚本,通过js引擎进行解析、编译和执行,并借助事件循环处理异步操作,编写健壮代码的最佳实践包括优先使用const和let、保持代码风格一致、合理处理错误、遵…

    2026年5月10日
    000
  • Vue.js 中 MSAL loginRedirect 的正确使用与重定向处理

    本文深入探讨了在 vue.js 单页应用中集成 msal.js 并使用 `loginredirect` 方法时常见的挑战,如 `getallaccounts` 返回空和缓存配置不生效等问题。核心内容在于强调正确处理 msal 重定向回调的重要性,并指导开发者如何通过 `handleredirectp…

    2026年5月10日
    000
  • Python与IPMI重启:确保文件数据持久化的最佳实践

    本文探讨了在linux环境下,python脚本写入文件后立即通过ipmi工具进行系统重启时,文件内容可能丢失的问题。该问题源于操作系统文件系统缓存未及时刷新至永久存储。教程将详细解释数据丢失的原因,并提供使用`sync`命令确保数据持久化的有效解决方案,帮助开发者避免类似的数据完整性问题。 Pyth…

    2026年5月10日
    000
  • 使用Flexbox实现图像的2×2网格布局:掌握flex-basis的关键

    使用Flexbox实现图像的2×2网格布局:掌握flex-basis的关键使用Flexbox实现图像的2×2网格布局:掌握flex-basis的关键使用Flexbox实现图像的2×2网格布局:掌握flex-basis的关键使用Flexbox实现图像的2×2网格布局:掌握flex-basis的关键

    本文详细介绍了如何利用css flexbox实现图像元素的2×2网格布局。文章重点讲解display: flex、flex-wrap以及核心属性flex-basis在控制子元素尺寸和换行行为中的作用。通过具体代码示例,展示如何精确调整图像排列,确保在不同屏幕尺寸下保持清晰、响应式的视觉效果…

    2026年5月10日 用户投稿
    100
  • HTML如何设置上标和下标?sup和sub标签的作用是什么?

    答案:HTML中使用和标签分别创建上标和下标,适用于数学公式、化学式、脚注等场景,可通过CSS调整字体大小、颜色及vertical-align对齐方式以优化显示效果,主流浏览器支持良好,必要时可用CSS微调确保兼容性。 HTML中,你可以用 标签设置上标,用 标签设置下标。 用于显示指数、脚注等, …

    2026年5月10日
    000
  • clion的安装与配置教程

    CLion是一款由JetBrains开发的C/C++ IDE。安装步骤包括:下载并安装CLion、安装CMake、选择工具链。配置步骤包括:导入项目、编译和运行、调试、代码格式化、添加插件。CLion支持多种功能,包括CMake构建、工具链选择、代码格式化、调试和插件扩展。 CLion 安装与配置教…

    2026年5月10日
    000
  • c语言中单双引号的区别

    C 语言中,单引号定义字符常量,双引号定义字符串常量。单引号还能定义预处理器宏,其范围和优先级与双引号不同。虽然两者均可定义字符串,但建议优先使用双引号,因为它支持转义字符。 C 语言中单双引号的区别 明确回答: C 语言中,单引号 (‘) 和双引号 (“) 用于定义字符常量…

    2026年5月10日
    000
  • 什么是JavaScript中的严格模式_它如何帮助避免常见错误?

    严格模式通过启用限制性规则提升代码质量与可维护性,需在脚本顶部或函数首行添加”use strict”;激活,能捕获未声明赋值、重复参数等错误,并使this指向更明确。 JavaScript中的严格模式(Strict Mode)是一种让代码在更严格的条件下运行的机制,它通过启用…

    2026年5月10日
    000
  • HTML语义化:单列数据展示的最佳实践与替代方案

    HTML语义化:单列数据展示的最佳实践与替代方案HTML语义化:单列数据展示的最佳实践与替代方案HTML语义化:单列数据展示的最佳实践与替代方案HTML语义化:单列数据展示的最佳实践与替代方案

    本文探讨了将两列表格数据转换为单列、交替标题/内容格式时可能遇到的语义化和可访问性问题。它详细解释了html ` ` 元素作用域的局限性,并提出了多种符合语义化标准的替代方案,包括使用定义列表(“)、语义化标题(“ 标签)结合段落(` `),以及在特定场景下谨慎使用嵌套表格,…

    2026年5月10日 用户投稿
    000
  • 九天算力平台任务:本地电脑关机后,计算任务还会继续运行吗?

    九天算力平台:本地电脑关闭后任务运行状态详解 使用九天算力平台进行AI训练时,许多用户关心一个问题:本地电脑关机后,平台上的计算任务能否继续运行? 部分用户反馈,关闭VS Code后,任务似乎停止,需要重新启动,这与预期中的远程服务器持续运行不符。 虽然平台后台显示计算时间仍在继续(用户截图所示),…

    2026年5月10日
    200
  • html如何建立副标题_为HTML文档添加副标题标签【标签】

    推荐使用与标签组合:主标题用,副标题用带class=”subtitle”的,语义清晰且不破坏大纲;已废弃但部分浏览器支持;ARIA可增强可访问性;CSS伪元素适合固定文本场景。 如果您希望在HTML文档中为标题添加副标题,以提供更详细的说明或补充信息,则需要使用语义化的方式组…

    2026年5月10日
    000
  • 什么是模块化HTML文件?如何查看HTML格式内容?

    什么是模块化HTML文件?如何查看HTML格式内容?什么是模块化HTML文件?如何查看HTML格式内容?什么是模块化HTML文件?如何查看HTML格式内容?什么是模块化HTML文件?如何查看HTML格式内容?

    现代前端开发倾向于模块化html,是因为它能有效解决大型项目中代码重复、维护困难和团队协作低效的问题,通过将页面拆分为独立、可复用的组件,实现高复用性、易维护性和高效协作,尤其在单页应用和微前端架构中不可或缺;1. 模块化使ui元素如导航栏、表单等可抽象为独立组件,一处修改全局生效;2. 支持组件为…

    2026年5月10日 用户投稿
    000
  • C++技术中的大数据处理:如何优化C++代码以提升大数据处理性能?

    通过优化 c++++ 代码,可以提升大数据处理性能。优化技术包括:使用智能指针管理内存。优化数据结构,如使用哈希表和 b 树。利用并行编程。减少拷贝开销。缓存数据。 C++ 技术中的大数据处理:优化代码以提升性能 引言 在当今大数据时代,高效处理海量数据集至关重要。C++ 以其卓越的性能和灵活性而备…

    2026年5月10日
    000
  • C++标记模式 运行时类型识别替代

    标记模式是一种基于类型标签在编译期实现函数分发的技术,通过定义标签类型(如tag_derived_a)并结合虚函数返回对应标签,利用if constexpr在编译期判断类型并调用相应逻辑,避免了RTTI开销,适用于嵌入式或性能敏感场景,但需手动扩展标签且灵活性低于dynamic_cast。 在C++…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信