现代C++实战30讲
吴咏炜
前 Intel 资深软件架构师
立即订阅
3593 人已学习
课程目录
已更新 10 讲 / 共 30 讲
0/4登录后,你可以任选4讲全文学习。
课前必读 (2讲)
开篇词 | C++这么难,为什么我们还要用C++?
免费
课前必读 | 有关术语发音及环境要求
基础篇 (8讲)
01 | 堆、栈、RAII:C++里该如何管理资源?
02 | 自己动手,实现C++的智能指针
03 | 右值和移动究竟解决了什么问题?
04 | 容器汇编 I:比较简单的若干容器
05 | 容器汇编 II:需要函数对象的容器
06 | 异常:用还是不用,这是个问题
07 | 迭代器和好用的新for循环
08 | 易用性改进 I:自动类型推断和初始化
现代C++实战30讲
登录|注册

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

吴咏炜 2019-11-25
你好,我是吴咏炜。
今天我们就正式开启了 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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《现代C++实战30讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(54)

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

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

    2019-11-26
    2
    20
  • hello world
    没有引用计数,没有拷贝和移动,没有线程安全,没有自定义delete函数,另外想请教老师一些问题.
    1. 全局静态和局部静态的变量是存储在哪个区域?看很多书是静态存储区,但静态存储区又是什么区?堆?
    2. thread local的变量存储在哪个区?因为线程是动态创建的,理解这个变量内存也应该动态分配的,线程结束内存自动释放?难道也是堆?
    3. 类的大小是怎么定的呢?一般都是看类的成员变量占用字节数再根据是否虚类看是否加4字节,但是类里面有很多成员函数,这些成员函数不占空间吗,如果有静态成员变量或者静态成员函数呢?
    谢谢老师!

    作者回复: 其他都对,不过,自定义delete似乎目前没这个必要?

    1. 好问题。静态存储区既不是堆也不是栈,而是……静态的。意思是,它们是在程序编译、链接时完全确定下来的,具有固定的存储位置(暂不考虑某些系统的地址扰乱机制)。堆和栈上的变量则都是动态的,地址无法确定。

    2. thread_local和静态存储区类似,只不过不是整个程序统一一块,而是每个线程单独一块。用法上还是当成全局/静态变量来用,但不共享也就不需要同步了。

    3. 非静态数据成员加上动态类型所需的空间。注意后者不一定是4,而一般是指针的大小,在64位系统上是8字节。还有,要考虑字节对齐的影响。静态数据成员和成员函数都不占个别对象的空间。

    2019-11-26
    7
    16
  • 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
    1
    10
  • bo
    老师您好!工程的时候,具体怎么考虑在栈上分配还是在堆上分配,更合理些?

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

    2019-11-26
    2
    9
  • hello world
    话说一般delete.后需要把这个变量置成nullptr吗,我有时候这样写,不知道有没有必要

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

    2019-11-26
    4
    6
  • yuchen
    怕评论中您看不到,在此再问一下,麻烦您啦~
    上个问题回顾:
    对于图2d有疑惑,希望该图绘制中可以标明main函数占用的栈空间范围及其对应的栈帧,同理,对bar和foo也一样。如果将图2d从下到上每行编号为0,1,2,...,7,那么main、bar和foo对应的栈空间占用、栈帧分别是那几行呢?
    您的回答:嗯,问得有道理。我的颜色选取不够好,回头改一下。按一般的栈帧定义,只有 0 属于 main,1–4 属于 bar。5 以上属于 foo。

    首先,非常感谢您的回复~

    然而,看到有人这样问您:“参数42”和“a=43”分别是函数调用的参数和函数局部变量,应该属于同一个栈帧,为什么这里不同?
    您的回答是:同样,实际实现通常就是这个样子的。参数属于调用者而非被调用者,一般也是由调用者来释放——至少一般 x86 的实现是这个样子。

    那么和您这里回答我的就不一致的呢。您这里回答我1-4属于bar,因此,那个人问的问题(“参数42”和“a=43”应该属于同一个栈帧)这句就是对的。另外您说“参数属于调用者而非被调用者”,这里1-4既然属于bar了,那么参数42不就属于了被调用者bar了吗?我理解的是main是调用者,main调用了bar,则bar是被调用者。

    作者回复: 这里主要牵涉到“栈帧”是如何定义的。虽然“参数属于调用者而非被调用者,一般也是由调用者来释放”概念上没有错,但我当时对“栈帧”的定义想当然了。我后来又查了一下定义(用词要以大家接受的用法为准),发现参数和局部变量应该算作一个栈帧里。也就是说,你们这儿的质疑是有道理的。所以,目前我已经把图修改了,这样应该就都没有疑问了。

    2019-11-26
    4
  • 吴军旗^_^
    老师可推荐一下教程吗? 从php转过来的,感觉有点难。

    作者回复: 如果刚开始学的话,这个专栏可能会有点挑战。可以先看一下 C++ 之父的 A Tour of C++,国内出版叫《C++语言导学》(谢谢小猪钱钱同学告知)。

    另外,《C++ Primer》名声很响,但 848 页初学有点厚了。注意不是《C++ Primer Plus》,这本跟前者完全无关,不推荐。

    2019-11-25
    1
    3
  • Jerry银银
    老师反复提到,没有学过、用过c++的人不适合学;我的观点稍微有点不同:计算机基础知识扎实,熟悉Java和c,这门课还是蛮适合的。

    计算机基础知识深厚,深入理解堆和栈的区别,知道什么时候用堆内存,什么时候用栈内存,那么,剩下的就是语法了。

    作者回复: 嗯,有点道理。但需要学习能力很强,因为我假设你是懂C++的基本语法的。

    2019-12-07
    1
    2
  • 浑浑噩噩cium

    void foo(int n)
    {
      Obj obj;
      if (n == 42)
        throw "life, the universe and everything";
    }
    变量释放,析构函数调用是函数返回的时候调用的。
    我测试了下如果没有捕获异常的话第二个obj的析构函数没有被调用程序结束,说明:
    1.析构是在foo函数返回的时候调用的
    2.抛异常的函数没有返回,不会自动释放变量
    3.如果捕获了异常,就在catch里面释放异常函数foo里面的变量。

    作者回复: 「std::terminate() 为 C++ 运行时在异常处理因下列原因失败时调用:
    1) 抛出的异常未被捕捉(此情况下是否进行任何栈回溯是实现定义的)
    …」

    https://zh.cppreference.com/w/cpp/error/terminate

    2019-12-02
    1
  • Geek_3f3bcb
    看的有点爽

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

    2019-12-01
    1
  • 史鹏飞
    老师在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
    1
  • 楚小奕
    这个专栏配合 《modern effect c++》效果很好

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

    2019-11-29
    1
  • xm2018
    关于演示栈展开的那段程序,如果main函数里面不try catch的话,第二个foo(42) obj的析构函数不会被调用,程序非法退出。这种情况算不算泄漏?

    作者回复: 一般不这么看。异常安全性对系统有很多约定,违反了约定,通常 terminate 会被调用。这种情况下就是不做清理工作的。

    在Windows上,你甚至可以用 catch(...) 捕获指针越界访问(需要 /EHa 编译参数),但前提条件一样是你需要去 catch。

    从另一个角度,程序崩溃时,大部分资源都会被操作系统回收,不会对系统造成问题。我们说泄漏,关注的主要是程序(长时间)运行过程中应该释放而没有释放掉的东西,如内存、文件句柄、锁等等。

    2019-11-29
    1
  • Gerry
    栈通常说是向下增长,从高地址到低地址。文中表述是向上增长感觉欠妥。

    作者回复: 因你这句话,我特地又去查了一下,目前看到的图,开口永远是上方。中英文资料都是如此。

    这个词的来源实际上可能是堆盘子。显然,你只能从上面取放盘子……

    2019-11-27
    2
    1
  • The rustic
    虽然现在写go了,但是相信以后还会用到c++。c++,近两年确实受到很大的冲击。云原生很不容易有个envoy,但是由于c++的复杂性,导致项目没有那么活跃。

    作者回复: Go也有它的优点。写网络应用Go还是不错的。好的语言多了,也是C++用得少了些的原因。

    2019-11-25
    2
    1
  • 执假以为真
    会不会讲内存管理器呢,不是内核中的内存管理,是库级别的。好像Google自己做了一个
    2019-12-13
  • Sochooligan
    请课堂疑问:
    1. “值语义”这个概念是想说明,C++有指针,很多其他语言没有?
    2. ptr.get() 是什么语义?
    3. ptr->是什么语义?

    作者回复: 1. 恰恰相反,大部分语言里都有像指针的东西,只是不叫指针,不能随意转换和做加减法,也就不那么危险。很多语言都没有值语义——传递对象,而不是对象的指针/引用。

    2. 得到一个指针,当然是引用语义。

    3. 不是完整表达式,问题没有意义。

    2019-12-12
    2
  • Anita
    在 C++ 里,所有的变量缺省都是值语义——如果不使用 * 和 & 的话,变量不会像 Java 或 Python 一样引用一个堆上的对象。对于像智能指针这样的类型,你写 ptr->call() 和 ptr.get(),语法上都是对的,并且 -> 和 . 有着不同的语法作用。而在大部分其他语言里,访问成员只用 .,但在作用上实际等价于 C++ 的 ->。这种值语义和引用语义的区别,是 C++ 的特点,也是它的复杂性的一个来源。要用好 C++,就需要理解它的值语义的特点。

    这段有些不理解,老师能再解释一下吗?谢谢

    作者回复: 这么说吧,Java里的一个对象变量相当于C++的指针变量,. 相当于 C++ 的 ->。其他就再多读两遍体会一下吧。

    2019-12-10
  • panqing
    shape_wraper 缺一个 ref counter

    作者回复: 智能指针也可以不是引用计数的。🤓

    不过不管那种,下一讲都会讨论。

    2019-12-08
  • ChenLicong
    “你写 ptr->call() 和 ptr.get(),语法上都是对的,并且 -> 和 . 有着不同的语法作用”,这句话不太明白

    作者回复: 常用语言里只有C++区分这两种标记(就是文中的例子)。你不能写ptr.call()。一般其他语言里都这样写。

    2019-12-08
收起评论
54
返回
顶部