最近在学习侯捷老师的《C++内存管理》课程,对C++底层的内存管理机制有了更深的一些理解,这里记录一下主要的学习内容和相关问题。

C++程序里使用memory的多种方式

C++控制内存方式
如上图所示,C++有多种直接或者间接控制内存的方式,相关的总结如下:

这里主要区分的是 newoperator new

  • new是C++的表达式(expressions),也就是C++在编译的时候会将表达式展开为多行代码语句,因为是表达式,所以new不可以重载
  • operator new是C++的函数,所以该函数是可以被重载的。

下图是C++的new expression的内部逻辑

可以看到针对new expression,编译器实际在背后做了以下几件事:

  • 首先通过operator new申请一块内存,内存大小根据complex类来定。
  • 然后将申请的内存指针强制转换为目标类型指针,这里的是Complex*类型。
  • 最后调用目标类型complex的构造函数构造对象。这里注意的是:通过pc->Complex::Complext(1,2)这种方式调用构造函数,只有编译器可以做,用户是不能这么做的

与new表达式对应的delete表达式的内部顺序则相反:首先调用Complex的析构函数,然后再调用operator delete释放内存。

Array New 和Array Delete


上图是array new的内存分配的逻辑,当使用array new分配一块内存,在这个内存的头部会记录一些cookie数据,包含数组的数量等,然后才是所有的元素的内存区。对于array new分配的内存,需要用array delete来释放。否则会造成内存泄漏

Q: 为什么array new分配的内存使用delete会发生内存泄漏?原因是什么?什么情况下不会发生泄漏?
A: delete和delete[]的区别在于delete[]会依次的调用每一个元素的析构函数,然后再释放内存,而delete则是只会调用第一个元素的析构函数,再释放内存。需要注意的是,delete和delete[]释放内存的阶段都是一样的,会将所有的元素的内存都释放掉,这部分都是交给底层的operator delete实现的,内存头部的cookie会记录整个内存的大小,因此可以全部释放。上文说的造成内存泄漏指的是调用析构函数阶段会造成内存泄漏。因为delete只会调用第一个元素的析构函数,其余的元素的析构函数是不会被调用的,如果这些元素在析构函数里会释放自身持有的内存,那么就会造成内存泄漏,如果析构函数里不需要释放内存,那么就不会有内存泄漏问题。
因此使用array new一个int的数组,然后调用delete来释放的话,是不会有内存泄漏的。

C++应用程序分配内存的途径


以上是C++应用程序的内存分配的途径,正常情况下,使用new表达式调用的是全局的::operator new函数,前面提到,operator new是函数表达式,是可以重载的,如果我们在类内部重载了operator new的话,那么new这个类的对象的时候就会调用重载的operator new而不是全局的。一下是一个重载的示例:

需要注意的点是,最好不要重载全局的operator new,因为这个影响太广了,所以直接使用默认的operator就好。

operator new

上文提到new表达式实际是调用operator new来分配内存的,operator底层又是调用的malloc来实现的,如下右图所示,在operator new内部会循环调用malloc来分配内存,如果分配失败,则会尝试调用_callnewh来处理,这里的_callnewh是一个new_handler函数,专门用于处理内存分配失败的情况,良好设计的new_handler只有两个选择:释放部分memory用来返回使用,或者调用abort()或exit().

std::allocator

这里以VC6 malloc来看下malloc分配内存的内部实现

从上图可以看到,使用malloc来分配用户所需要的内存时,在所需内存之外还申请了用于存储cookie,debug和padding信息的内存,cookie一般是记录分配的内存块大小等信息,后续使用free释放的时候就通过cookie的信息来获取大小。debug则是开启debug调试的时候需要的信息,以及一般内存的大小需要是16字节的倍数,因此会有padding用于对齐的内存开销。

从上图可以看到,VC6的allocate函数只是对malloc函数的二次封装,并没有采用特殊的操作。

std::alloc


在课程中侯捷老师针对std::alloc有非常精彩的一个讲解,以及源码的剖析,这里简单减少下,之后写一篇专门介绍这个std::alloc的实现。

std::alloc使用的是一个16个元素的数组来管理内存的链表。不同的数组元素表示不同的内存大小区块,如上图中3号区块管理的是大小为32byte的链表。每个链表用于申请和释放对应大小的内存。这里通过链表分配的内存大小是有上限的,最大的块大小为128Bytes,如果用户需要的内存大于128bytes,那么std::malloc则将会调用malloc用于分配空间。

allocator分配内存主要的目的是为了加快分配的效率,以及减少内存碎片,类似内存池的设计会一次性分配一大块的内存用于管理,因此可以有效的减少每次分配的cookie和对齐等内存浪费的情况。

参考资料