01 | 堆、栈、RAII:C++里该如何管理资源?
01 | 堆、栈、RAII:C++里该如何管理资源?
讲述:吴咏炜
时长16:03大小11.01M
基本概念
堆
栈
RAII
内容小结
课后思考
参考资料
赞 49
提建议
精选留言(111)
- 泰伦卢2019-11-26没有引用计数,没有拷贝和移动,没有线程安全,没有自定义delete函数,另外想请教老师一些问题. 1. 全局静态和局部静态的变量是存储在哪个区域?看很多书是静态存储区,但静态存储区又是什么区?堆? 2. thread local的变量存储在哪个区?因为线程是动态创建的,理解这个变量内存也应该动态分配的,线程结束内存自动释放?难道也是堆? 3. 类的大小是怎么定的呢?一般都是看类的成员变量占用字节数再根据是否虚类看是否加4字节,但是类里面有很多成员函数,这些成员函数不占空间吗,如果有静态成员变量或者静态成员函数呢? 谢谢老师!展开
作者回复: 其他都对,不过,自定义delete似乎目前没这个必要? 1. 好问题。静态存储区既不是堆也不是栈,而是……静态的。意思是,它们是在程序编译、链接时完全确定下来的,具有固定的存储位置(暂不考虑某些系统的地址扰乱机制)。堆和栈上的变量则都是动态的,地址无法确定。 2. thread_local和静态存储区类似,只不过不是整个程序统一一块,而是每个线程单独一块。用法上还是当成全局/静态变量来用,但不共享也就不需要同步了。 3. 非静态数据成员加上动态类型所需的空间。注意后者不一定是4,而一般是指针的大小,在64位系统上是8字节。还有,要考虑字节对齐的影响。静态数据成员和成员函数都不占个别对象的空间。
共 22 条评论93 - Milittle2019-11-26说实话,这个专栏对于我这个经常使用C++来做项目的人来讲,我认为不适合初学者,上车需要有过C++开发经验的。一般的小伙伴可能会有压力哒,但是如果想学,克服心里畏惧,从这个专栏出发可以迅速的深入。很好的专栏。
作者回复: 谢谢。这个专栏是要求之前学过、用过C++的。没学过的不合适。
共 3 条评论42 - bo2019-11-26老师您好!工程的时候,具体怎么考虑在栈上分配还是在堆上分配,更合理些?
作者回复: 凡生命周期超出当前函数的,一般需要用堆(或者使用对象移动传递)。反之,生命周期在当前函数内的,就该用栈。
共 8 条评论41 - NEVER SETTLE2019-11-26学习笔记: 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++ 的特点,也是它的复杂性的一个来源。展开
作者回复: 认真记笔记非常好。 不过,建议笔记还是记关键字和要点,解释文字不用多。否则篇幅跟原文接近就意义不大了。
共 3 条评论30 - 史鹏飞2019-11-29老师在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
共 6 条评论28 - Geek_3f3bcb2019-12-01看的有点爽
作者回复: 哈,你是第一个用这个形容词的。😁
20 - LiKui2019-12-19内存泄漏的原因之二: 1. 异常或分支导致delete未得到执行 2.分配和释放不在一个函数里导致的遗漏delete
作者回复: 是的。
共 2 条评论17 - 泰伦卢2019-11-26话说一般delete.后需要把这个变量置成nullptr吗,我有时候这样写,不知道有没有必要
作者回复: 如果这个变量下面还有用到的地方,这是个好习惯。不过,这个习惯主要还是从C来的。现代C++不推荐一般代码里再使用裸指针和new/delete的。
共 8 条评论14 - super-ck2020-01-23您好,有一点不是很清楚,在n为42时为何不是构造函数-throw-析构函数这个顺序,根据上下文,为42时,按一般逻辑应该进判断执行才对
作者回复: 是这个顺序。但异常抛出后,如果有相应的catch的话,析构完了才会执行到catch。
11 - 楚小奕2019-11-29这个专栏配合 《modern effect c++》效果很好
作者回复: Meyers的书对提升C++能力到下一个台阶是非常重要的。我也从中学了很多。
9 - 张珂2019-12-26老师您好,我说一下我对内存切片那里的理解,不知道对不对: 返回的是个基类指针shape*,但其实指向的是个继承类circle对象。那么在用户程序里,就算用户记得delete这个指针shape*,也会造成circle部分永久残留在内存,从而造成内存泄漏,我理解的对吗?
作者回复: 不是。返回shape*是没有问题的。返回shape才会造成对象切片。 可以通过delete一个shape*来删除一个circle对象的。这个是正常的面向对象行为。要求是shape有虚析构函数。
共 2 条评论8 - 莫珣2020-04-03C++对象在销毁的时候会自动调用析构函数,所谓RAII机制其实就是在对象构造的时候初始化它所需要的资源,在析构的时候自动释放它持有的资源。
作者回复: 是这样。不过RAII这个名字很差劲,看名字完全看不到最关键的点——析构。
8 - 吴军旗^_^2019-11-25老师可推荐一下教程吗? 从php转过来的,感觉有点难。
作者回复: 如果刚开始学的话,这个专栏可能会有点挑战。可以先看一下 C++ 之父的 A Tour of C++,国内出版叫《C++语言导学》(谢谢小猪钱钱同学告知)。 另外,《C++ Primer》名声很响,但 848 页初学有点厚了。注意不是《C++ Primer Plus》,这本跟前者完全无关,不推荐。
共 3 条评论7 - Gerry2019-11-27栈通常说是向下增长,从高地址到低地址。文中表述是向上增长感觉欠妥。
作者回复: 因你这句话,我特地又去查了一下,目前看到的图,开口永远是上方。中英文资料都是如此。 这个词的来源实际上可能是堆盘子。显然,你只能从上面取放盘子……
共 5 条评论6 - yuchen2019-11-26怕评论中您看不到,在此再问一下,麻烦您啦~ 上个问题回顾: 对于图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是被调用者。展开
作者回复: 这里主要牵涉到“栈帧”是如何定义的。虽然“参数属于调用者而非被调用者,一般也是由调用者来释放”概念上没有错,但我当时对“栈帧”的定义想当然了。我后来又查了一下定义(用词要以大家接受的用法为准),发现参数和局部变量应该算作一个栈帧里。也就是说,你们这儿的质疑是有道理的。所以,目前我已经把图修改了,这样应该就都没有疑问了。
共 3 条评论6 - Home2020-06-15老师好,关于值语义和引用语义可以分别举几个例子么?
作者回复: 所有的指针、引用变量(以 &、* 结尾的)都算引用语义,其他的就是值语义了。
4 - 陈嘉伟2020-04-16请教一个问题,既然delete空指针是合法的,那多次delete同一个指针为什么会报错呢?
作者回复: 空值是个单一数值,编译器会产生检查的代码,简单得很。编译器可没有简单方法来帮你检查你删除的指针是否是之前已经删除的。 反过来,删除指针后立即清空可以部分防止这种问题,虽然这种用法在C++里的必要性要低一点。我们推荐**不**使用裸指针,尽量用高层抽象来帮我们自动解决这些问题。
共 3 条评论4 - xm20182019-11-29关于演示栈展开的那段程序,如果main函数里面不try catch的话,第二个foo(42) obj的析构函数不会被调用,程序非法退出。这种情况算不算泄漏?
作者回复: 一般不这么看。异常安全性对系统有很多约定,违反了约定,通常 terminate 会被调用。这种情况下就是不做清理工作的。 在Windows上,你甚至可以用 catch(...) 捕获指针越界访问(需要 /EHa 编译参数),但前提条件一样是你需要去 catch。 从另一个角度,程序崩溃时,大部分资源都会被操作系统回收,不会对系统造成问题。我们说泄漏,关注的主要是程序(长时间)运行过程中应该释放而没有释放掉的东西,如内存、文件句柄、锁等等。
4 - Interesting2020-04-28C++是只有使用new关键字出来的对象才分配到堆上吗? Obj obj(); Obj obj* = new Obj(); 只有后者是在堆上吗? 堆栈的却别就是 栈的销毁是随着局部变量失效和函数调用完自动销毁 而 堆是需要申请和手动销毁吗? 抱歉从别的语言转过来的可能表述不是很准确展开
作者回复: 是的。 另外,对象内部可能额外申请堆内存,并在对象析构时自动释放。容器对象基本都会这么做。从实际的角度,真正的应用代码里完全可以没有new和delete(底层库代码一般仍然会需要)。
3 - Anita2019-12-10在 C++ 里,所有的变量缺省都是值语义——如果不使用 * 和 & 的话,变量不会像 Java 或 Python 一样引用一个堆上的对象。对于像智能指针这样的类型,你写 ptr->call() 和 ptr.get(),语法上都是对的,并且 -> 和 . 有着不同的语法作用。而在大部分其他语言里,访问成员只用 .,但在作用上实际等价于 C++ 的 ->。这种值语义和引用语义的区别,是 C++ 的特点,也是它的复杂性的一个来源。要用好 C++,就需要理解它的值语义的特点。 这段有些不理解,老师能再解释一下吗?谢谢展开
作者回复: 这么说吧,Java里的一个对象变量相当于C++的指针变量,. 相当于 C++ 的 ->。其他就再多读两遍体会一下吧。
3