1. 问题的引入
new和malloc有什么不同?这是一个很经典的问题。
他们共同点是,都是用进行动态内存分配的,但实现的方式不同。
其中最根本的不同是,new是一个C++的操作符/关键字,而malloc是C/C++中一个函数。基于这个原因,它们又有以下的不同
1. 参数,malloc需要指定申请内存的大小,new不需要,它会自己计算类中成员变量所占内存的大小
2. 返回值,new返回类对象类型的指针,malloc返回void*
3. 如果内存分配失败,new抛出 std::bad_alloc 异常(c++11),malloc则会返回NULL
4. new/delete操作符是可以被重载的
5. 内存分配方式,malloc是从堆上分配内存的,new申请的内存则是自由存储区。其实这个自由存储区是C++标准里一个逻辑概念,实现上也是基于堆的。
6. new 除了申请内存之外,还会调用类的构造函数。
2. 知道这个问题,有什么用呢?
我目前只想到下面这种场景
和其他语言不同,C++为了兼容C,在设计上做出了很多让步。虽然new和malloc有上述这么多的不同,但它们在使用上,很容易让人犯错,尤其是新手。比如,new出来的对象,free掉。malloc出来的内存,delete掉。关键是这些编译都是可以通过的,基础类型比如int、double、char还好,但是如果是类类型比如string等,那将会有大麻烦,内存污染之后,调试代码可没那么方便了。
那么怎么去避免呢?
结合以往的经验来看,主要有两种方式。一是使用智能指针,管理类对象的生命周期;二是利用cppcheck等静态代码分析工具,辅助审阅代码。剩下的就是考程序员的自我修养了,但这往往是不靠谱的,人总会有疏忽的地方。
3. 进一步思考,new和malloc是怎么申请内存的呢?
3.1 先说简单的malloc
- 当申请的内存小于128K时,会调用brk在堆上分配。brk是将数据段(.data)的最高地址指针_edata往高地址推,这个指针只有一个隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;brk(addr)是一个系统调用,对应内核函数是sys_brk(addr),它会直接修改堆的大小。
- 大于128K时,会调用mmap()在映射区分配,映射区简单理解就是栈区和堆区中间的一块区域。
其实malloc之所这样做,目的是为了减少内存碎片,因为brk分配的内存需要等到高地址内存释放以后才释放。
3.2 new都做了那些工作呢?
- 配置内存
- ::operator new
- 大于128B 一级空间配置器,调用malloc、realloc等库函数操作
- 小于128B 二级空间配置器,使用内存池技术。它有16个空闲链表。分别管理8,16,24,...120,128(8的倍数)的数据块。每次配置一大块内存,并配置16个空闲链表(free-list).如果有相同的内存需求,则直接从free_list中取。如果有小额区块被释放则被会受到free_list中。如果自由链表中找不到或者块数不够,则向内存池进行申请,一般一次申请20块。
- 调用构造函数 ::construct()
4. 为什么申请内存会这么复杂呢?
其实无论是操作系统还是编程语言,它们面向的场景是非常广泛的。所以它们要尽可能的在兼容各种场景下,兼顾程序的运行效率。尤其是内存碎片不能太多,能尽量少的调用系统函数完成内存申请工作。所以才会有128KB,128B这种不同阈值下的处理方案。那么其实从业务系统的角度来讲,我们也可以通过定制自己的内存分配函数来提高业务系统的运行效率。另一个常用的方案就是,使用内存池技术,预先分配一部分内存,这部分内存,用户自己管理而不是操作系统。这样降低了频繁申请内存和释放内存给系统带来的资源消耗。而预先配分的这部分内存,业务系统往往是可以接收这部分资源开销的。当然,也可以通过设定内存池高低水位,来动态伸缩。
5. C/C++中常见的内存错误有哪些呢?
因为C++中没有GC机制,所以内存的管理是程序员必须要掌握的知识。我总结了下工作中遇到一些内存问题,如下
1. 前文中提到的new/delete 、malloc/free、new[]/delete[] 不匹配,导致的内存污染或者内存泄漏
2. 野指针,delete/free之后,没有置NULL,再次使用
3. 申请内存之后,没有判断是否成功,直接使用
4. 内存越界,数组下标越界
5. memset 参数错误 污染内存
6. 函数返回指向局部指针变量,离开作用域,变量失效了,再使用就可能发生异常。注意,编译时会有警告。
7. 内存分配成功,没有初始化
8. 类成员中含有类对象,但使用memset等函数进行内存操作
9. map erase 删除操作会使当前指向被删除元素的迭代器失效。要注意这样删除map.erase(it_pos++);, 这样map.erase(it_pos);再继续访文迭代器是错误的。
推荐两个辅助工具
静态代码分析工具 cppcheck
内存泄漏检测工具 valgrind
参考:
[1] https://en.cppreference.com/w/cpp/keyword/new
[2] https://blog.youkuaiyun.com/dreamiond/article/details/75201473
[3] https://blog.youkuaiyun.com/gfgdsg/article/details/42709943
[4] 《深入理解LINUX内核》