现代 C++ 编程实战
吴咏炜
前 Intel 资深软件架构师
34196 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 51 讲
加餐 (1讲)
现代 C++ 编程实战
15
15
1.0x
00:00/00:00
登录|注册

01 | 堆、栈、RAII:C++里该如何管理资源?

缺少哪些功能
智能指针的功能
使用析构函数进行资源释放
对象不能或不应该存储在栈上的情况
对象存储在栈上
通过栈和析构函数对资源进行管理
Resource Acquisition Is Initialization
编译器会自动调用析构函数,包括在发生异常时
对于非 POD 类型,编译器会插入构造和析构函数的调用
函数占用的栈空间
不存在内存碎片
简单的内存分配和释放
后进先出的结构
函数调用过程中产生的本地变量和调用数据的区域
堆内存分配不符合 C++ 惯用法
漏掉 delete 导致内存泄漏
可能导致内存碎片化
需要手工释放
可能触发垃圾收集操作
考虑程序当前未分配的内存
使用 newdelete 进行内存分配和释放
需要手工释放内存,否则会导致内存泄漏
动态分配内存的区域
shape_wrapper 和智能指针的比较
RAII 的示例
RAII 的应用
RAII 的概念
栈展开
栈上的内存分配
栈帧
栈的特点
栈的概念
内存管理的问题
内存释放
内存分配过程
堆的操作
堆的概念
思考题
RAII
C++ 内存管理

该思维导图由 AI 生成,仅供参考

你好,我是吴咏炜。
今天我们就正式开启了 C++ 的学习之旅,作为第一讲,我想先带你把地基打牢。我们来学习一下内存管理的基本概念,大致的学习路径是:先讲堆和栈,然后讨论 C++ 的特色功能 RAII。掌握这些概念,是能够熟练运用 C++ 的基础。

基本概念

,英文是 heap,在内存管理的语境下,指的是动态分配内存的区域。这个堆跟数据结构里的堆不是一回事。这里的内存,被分配之后需要手工释放,否则,就会造成内存泄漏。
C++ 标准里一个相关概念是自由存储区,英文是 free store,特指使用 newdelete 来分配和释放内存的区域。一般而言,这是堆的一个子集:
newdelete 操作的区域是 free store
mallocfree 操作的区域是 heap
newdelete 通常底层使用 mallocfree 来实现,所以 free store 也是 heap。鉴于对其区分的实际意义并不大,在本专栏里,除非另有特殊说明,我会只使用堆这一术语。
,英文是 stack,在内存管理的语境下,指的是函数调用过程中产生的本地变量和调用数据的区域。这个栈和数据结构里的栈高度相似,都满足“后进先出”(last-in-first-out 或 LIFO)。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

C++内存管理是编程中的重要概念,本文介绍了堆、栈和RAII的相关知识。堆是动态分配内存的区域,需要手动释放以避免内存泄漏。栈是函数调用过程中产生的本地变量和调用数据的区域,遵循“后进先出”的原则。RAII是C++特有的资源管理方式,依托栈和析构函数对所有资源进行管理,避免了像Java那样的垃圾收集方法。文章还介绍了堆内存分配的操作和释放过程,以及常见的内存泄漏问题。总的来说,本文为读者提供了C++内存管理的基本概念和相关技术特点。 栈是C++里最“自然”的内存使用方式,使用基于栈和析构函数的RAII,可以有效地对包括堆内存在内的系统资源进行统一管理。文章还强调了C++的值语义和引用语义的区别,以及对象切片(object slicing)的问题。此外,通过示例代码和解释,读者可以了解栈的增长方向、栈帧的概念以及栈展开(stack unwinding)的重要性。 另外,文章还介绍了如何确保在使用返回值为指针或引用的工厂方法时不会发生内存泄漏,通过析构函数和栈展开行为的实现方式。最后,文章提到了RAII的基本用法,包括关闭文件、释放同步锁和释放其他重要的系统资源。通过`shape_wrapper`的示例,读者可以了解RAII的基本实现方式。 总的来说,本文通过深入浅出的方式,帮助读者了解C++内存管理的基本概念和相关技术特点,以及如何使用RAII进行资源管理。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《现代 C++ 编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(114)

  • 最新
  • 精选
  • bo
    老师您好!工程的时候,具体怎么考虑在栈上分配还是在堆上分配,更合理些?

    作者回复: 凡生命周期超出当前函数的,一般需要用堆(或者使用对象移动传递)。反之,生命周期在当前函数内的,就该用栈。

    2019-11-26
    8
    47
  • Milittle
    说实话,这个专栏对于我这个经常使用C++来做项目的人来讲,我认为不适合初学者,上车需要有过C++开发经验的。一般的小伙伴可能会有压力哒,但是如果想学,克服心里畏惧,从这个专栏出发可以迅速的深入。很好的专栏。

    作者回复: 谢谢。这个专栏是要求之前学过、用过C++的。没学过的不合适。

    2019-11-26
    3
    44
  • NEVER SETTLE
    学习笔记: 1、概念 堆(heap):在内存管理中,指的是动态分配内存的区域。当被分配之后需要手工释放,否则,就会造成内存泄漏。 C++ 标准里一个相关概念是自由存储区(free store),特指使用 new 和 delete 来分配和释放内存的区域。 这是堆的一个子集:new 和 delete 操作的区域是 free store,而 malloc 和 free 操作的区域是 heap 。 但 new 和 delete 通常底层使用 malloc 和 free 来实现,所以 free store 也是 heap。 栈(stack):在内存管理中,指的是函数调用过程中产生的本地变量和调用数据的区域。 RAII(Resource Acquisition Is Initialization):C++ 所特有的资源管理方式。 RAII 依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理。 对 RAII 的使用,使得 C++ 不需要垃圾收集方法,也能有效地对内存进行管理。 2、堆 C++程序需要牵涉到两个的内存管理器的操作: 1). 让内存管理器分配一个某个大小的内存块 分配内存要考虑程序当前已经有多少未分配的内存。 内存不足时要从操作系统申请新的内存。 内存充足时,要从可用的内存里取出一块合适大小的内存,并将其标记为已用,然后将其返回给要求内存的代码。 2). 让内存管理器释放一个之前分配的内存块 释放内存不只是简单地把内存标记为未使用。 对于连续未使用的内存块,通常内存管理器需要将其合并成一块,以便可以满足后续的较大内存分配要求。 目前的编程模式都要求申请的内存块是连续的。 从堆上申请的内存需要手工释放,但在此过程中,内存可能有碎片化的情况。 一般情况下不需要开发人员介入。因为内存分配和释放的管理,是内存管理器的任务。 开发人员只需要正确地使用 new 和 delete,即每个 new 出来的对象都应该用 delete 来释放。 3、栈 大部分计算机体系架构中,栈的增长方向是低地址,因而上方意味着低地址。 任何一个函数,根据架构的约定,只能使用进入函数时栈指针向上部分的栈空间。 当函数调用另外一个函数时,会把参数也压入栈里,然后把下一行汇编指令的地址压入栈,并跳转到新的函数。 新的函数进入后,首先做一些必须的保存工作,然后会调整栈指针,分配出本地变量所需的空间,随后执行函数中的代码。 在执行完毕之后,根据调用者压入栈的地址,返回到调用者未执行的代码中继续执行。 本地变量所需的内存就在栈上,跟函数执行所需的其他数据在一起。 当函数执行完成之后,这些内存也就自然而然释放掉了。 栈上的内存分配,是移动一下栈指针。 栈上的内存释放,是函数执行结束时移动一下栈指针。 由于后进先出的执行过程,不可能出现内存碎片。 每个函数占用的栈空间有个特定的术语,叫做栈帧(stack frame)。 GCC 和 Clang 的命令行参数中提到 frame 的,如 -fomit-frame-pointer,一般就是指栈帧。 如果本地变量是简单类型,C++ 里称之为 POD 类型(Plain Old Data)。 对于有构造和析构函数的非 POD 类型,栈上的内存分配也同样有效。 只不过 C++ 编译器会在生成代码的合适位置,插入对构造和析构函数的调用。 编译器会自动调用析构函数,包括在函数执行发生异常的情况。 在发生异常时对析构函数的调用,还有一个专门的术语,叫栈展开(stack unwinding)。 在 C++ 里,所有的变量缺省都是值语义。 引用一个堆上的对象需要使用 * 和 & 。 对于像智能指针这样的类型,使用 ptr->call() 和 ptr.get(),语法上都是对的,并且 -> 和 . 有着不同的语法作用。 这种值语义和引用语义的区别,是 C++ 的特点,也是它的复杂性的一个来源。

    作者回复: 认真记笔记非常好。 不过,建议笔记还是记关键字和要点,解释文字不用多。否则篇幅跟原文接近就意义不大了。

    2019-11-26
    3
    32
  • 史鹏飞
    老师在shape_wrapper类下边的foo函数调用完后,会把shape析构掉,但如何析构circle呢?

    作者回复: 这就是面向对象里的基本用法了。在面向对象的继承体系了,shape需要有一个虚析构函数。这样如果有一个shape*实际指向circle,在delete这个指针时,调用的是circle的析构函数(当然析构过程中,最后也会再调用shape的析构函数)。 下面的代码可以展示这个过程: #include <stdio.h> class shape { public: virtual ~shape() { puts("~shape"); } }; class circle : public shape { public: ~circle() { puts("~circle"); } }; int main() { shape* ptr = new circle(); delete ptr; } 结果是: ~circle ~shape

    2019-11-29
    7
    28
  • Geek_3f3bcb
    看的有点爽

    作者回复: 哈,你是第一个用这个形容词的。😁

    2019-12-01
    21
  • LiKui
    内存泄漏的原因之二: 1. 异常或分支导致delete未得到执行 2.分配和释放不在一个函数里导致的遗漏delete

    作者回复: 是的。

    2019-12-19
    2
    19
  • super-ck
    您好,有一点不是很清楚,在n为42时为何不是构造函数-throw-析构函数这个顺序,根据上下文,为42时,按一般逻辑应该进判断执行才对

    作者回复: 是这个顺序。但异常抛出后,如果有相应的catch的话,析构完了才会执行到catch。

    2020-01-23
    15
  • 泰伦卢
    话说一般delete.后需要把这个变量置成nullptr吗,我有时候这样写,不知道有没有必要

    作者回复: 如果这个变量下面还有用到的地方,这是个好习惯。不过,这个习惯主要还是从C来的。现代C++不推荐一般代码里再使用裸指针和new/delete的。

    2019-11-26
    8
    15
  • 莫珣
    C++对象在销毁的时候会自动调用析构函数,所谓RAII机制其实就是在对象构造的时候初始化它所需要的资源,在析构的时候自动释放它持有的资源。

    作者回复: 是这样。不过RAII这个名字很差劲,看名字完全看不到最关键的点——析构。

    2020-04-03
    12
  • 楚小奕
    这个专栏配合 《modern effect c++》效果很好

    作者回复: Meyers的书对提升C++能力到下一个台阶是非常重要的。我也从中学了很多。

    2019-11-29
    12
收起评论
显示
设置
留言
99+
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部