C++动态内存管理与源码剖析
引言
在本篇文章中,我们主要剖析c++中的动态内存管理,包括malloc、new expression、operator new、array new和allocator内存分配方法以及对应的内存释放方式和他们之间的调用关系,另外也包括一些会引发的陷阱如内存泄漏。
动态内存管理函数及其调用关系
c++中的动态内存分配和释放方式有很多,主要包括:
- malloc与free
- new expression与delete expression
- array new 与array delete
- operator new和operator delete
- allocator中的allocate与deallocate
除此之外还有placement new,但需要注意placement new不是用来内存分配和释放的,而是在已分配的内存上构造对象。
他们之间的调用关系如下:

下面我们来具体看下每一种分配和释放方式的使用和原理。
malloc与free
void *p1 = malloc(32); //分配32字节的内存
free(p1);//释放指针p1指向的内存
malloc函数以字节数为参数,返回指向分配的内存的首地址的void指针;而free函数释放给定指针指向的内存。
operator new与operator delete
void *p6 = ::operator new(32); //分配32字节
::operator delete(p6);
PS:底层调用malloc和free。gnu的实现:
_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
void *p;
/* malloc (0) is unpredictable; avoid it. */
if (__builtin_expect (sz == 0, false))
sz = 1;
while ((p = malloc (sz)) == 0)
{
new_handler handler = std::get_new_handler ();
if (! handler)
_GLIBCXX_THROW_OR_ABORT(bad_alloc());
handler ();
}
return p;
}
_GLIBCXX_WEAK_DEFINITION void
operator delete(void* ptr) noexcept
{
std::free(ptr);
}
new expression与delete expression
首先来看下简单的使用:
int *p2 = new int;
delete p2;
string *p3 = new string("hello");
delete p3;
new expression完成两样工作:
申请并分配内存。
调用构造函数。
string *p3 = new string("hello");被编译器替换成下面的工作:
string *p3;
try{
void * tmp_p = operator new(sizeof(string));
p3 = static_cast<string *>(tmp_p);
//string 通过宏被替换为basic_string,string的实际实现是basic_string,这里不是重点。
p3 -> basic_string::basic_string("hello"); //编译器可以这么调用,但我们自己写代码时不能。即我们不能以这种方式通过指针显式调用构造函数。
}catch (std::bad_alloc){
//若分配失败,构造函数不执行
}
我们看到,原来new expression的内存申请和分配是通过调用operator new()来完成的。
delete expression也完成两样工作:
- 调用析构函数。
- 释放内存。
delete p3;被编译器替换成下面的工作:
p3 -> ~string();//通过指针直接调用析构函数。我们自己写代码时也可以这么做。
operator delete(p3);//释放内存
array new 与array delete
//Complex为自定义类,只需要知道Complex类中没有指针成员。
Complex *pca = new Complex[3];//3次构造函数
delete[] pca;//3次析构函数
string *psa = new string[3];//3次构造函数
delete[] psa;//3次析构函数
array new调用一次内存分配函数(底层源码实现中,其实是调用operator new,只是调用的时候计算好了大小。因此,有上下两个cookie。)和多次构造函数。正因为调用多次构造函数,因此只能调用无参构造函数。
Complex和string的很大不同之处在于,string有指针成员,布局如下图:


array delete调用多次析构函数,一次内存释放函数(底层源码实现中其实是调用一次operator delete)。
我们来看下,如果本应该使用array delete的地方使用了delete expression会发生什么:
Complex *pca = new Complex[3];//3次构造函数
delete pca;//1次析构函数
string *psa = new string[3];//3次析构函数
delete psa;//1次析构函数
对于Complex,我们使用了array new调用了3次构造函数,却没有使用array delete而使用了delete expression,因此只调用了一次析构函数。那么,会发生内存泄漏吗? 不会。因为Complex的析构函数是无关痛痒的(trivial),因为没有要释放的关联的内存(Complex对象自身所占内存之外没有隐式占用的内存)。
同样,对于string,我们使用了array new调用了3次构造函数,却没有使用array delete而使用了delete expression,因此只调用了一次析构函数。那么,会发生内存泄漏吗? 会。因为string的析构函数不是无关痛痒的(non-trivial),因为要释放关联的内存(我们知道string底层是通过char[]存储的,析构时会释放掉那些实际存储字符的内存)。
PS: 具体的内存布局例子(涉及到cookie、对齐填充padding等等)。
int *p = new int[10];
delete[]p;
//delete p 亦可。int无关痛痒。
VC6中的内存布局如下:

另:
Demo *p = new Demo[3];//Demo为析构函数non-trivial的自定义class
delete[] p;
//delete p; //错误
VC6中的内存布局(注意红框内的3):

allocate与deallocate
#ifdef __GNUC__ //GNUC环境下
void *p7 = allocator<int>().allocate(4); //非static函数,通过实例化匿名对象调用allocate,分配4个int的内存。
allocator<int>().deallocate((int *)p7, 4);
void *p8 = __gnu_cxx::__pool_alloc<int>().allocate(4);
__gnu_cxx::__pool_alloc<int>().deallocate((int *)p8, 4);
#endif
allocator为模板,实例化时需提供模板类型参数,上面的程序中模板类型参数为<int>,allocate的参数为4则allocate函数分配时就分配4个int的内存。释放内存时需要给出指向所要释放的内存位置的指针,以及要释放的内存大小,单位为模板类型参数类型的大小。
__pool_alloc也为模板,除底层调用malloc的时机不同外(__pool_alloc使用内存池降低cookie带来的overhead),使用和上面的allocator相同。
placement new
用法:
char *buf = new char[sizeof(Complex) * 3];
Complex *pc = new(buf) Complex(1, 2);
new(buf + 1) Complex(1, 3);
new(buf + 2) Complex(1, 3);
delete[] buf;
Complex *pc = new(buf) Complex(1, 2);被编译器替换成如下的工作:
Complex *pc;
try{
void *tmp = operator new(sizeof(Complex), buf);//该重载版本并不分配内存。buf指针已经指向内存。
pc = static_cast<Complex*>(tmp);
pc->Complex::Complex(1, 2);//构造函数
}catch(std::bad_alloc){
//若分配失败则不执行构造函数。实际上没有分配,因为之前已经分配完。
}
上面使用的GNU库重载版本的operator new()函数如下:
// Default placement versions of operator new.
_GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
可以看到确实没有分配内存。
重载内存管理函数
new expression、delete expression都不可重载。
operator new、operator delete可以重载:
- 重载global
operator new、operator delete,即::operator new(size_t)与::operator delete(void *)。(一般不会重载全局的该函数,因为影响太广) - 重载某个class的
operator new、operator delete
若某个类重载了operator new、operator delete,则用new expression实例化该类时,调用的是类的operator new、operator delete,否则,调用globaloperator new、operator delete。
array new、array delete也可以重载。同样分全局的和类所属的。
具体如何重载这些内存管理函数,以及如何使用重载的内存管理函数,将在下一篇文章中分析。
参考资料
[1] 《STL源码剖析》
[2] 《Effective C++》3/e
[3] 《C++ Primer》5/e
[4] 侯捷老师的课程
[5] gcc开源库:https://github.com/gcc-mirror/gcc
C++动态内存管理与源码剖析的更多相关文章
- 内存管理初始化源码4:add_active_range
我们在阅读源码时,函数功能可以分为两类:1. bootmem.c 2. page_alloc.c. 1. bootmem.c是关于bootmem allocator的,上篇文章已经简述过. 2. pa ...
- 内存管理初始化源码1:setup_arch
源码声明:基于Linux kernel 3.08 1. 在kernel/arch/mips/kernel/head.S中会做一些特定硬件相关的初始化,然后会调用内核启动函数:start_kernel: ...
- 内存管理初始化源码3:bootmem
start_kernel ——> setup_arch ——> arch_mem_init ——> bootmem_init ——> init_bootmem_node: 此时 ...
- 内存管理初始化源码2:setup_arch
PFN相关宏说明: /* kernel/include/linux/pfn.h */ PFN : Page Frame Number(物理页帧) /* * PFN_ALIGN:返回地址x所在那一页帧的 ...
- 内存管理初始化源码5:free_area_init_nodes
start_kernel ——> setup_arch ——> arch_mem_init ——> |——> bootmem_init |——> device_tree ...
- Spring源码剖析依赖注入实现
Spring源码剖析——依赖注入实现原理 2016年08月06日 09:35:00 阅读数:31760 标签: spring源码bean依赖注入 更多 个人分类: Java 版权声明:本文为博主原 ...
- (原创滴~)STL源码剖析读书总结1——GP和内存管理
读完侯捷先生的<STL源码剖析>,感觉真如他本人所说的"庖丁解牛,恢恢乎游刃有余",STL底层的实现一览无余,给人一种自己的C++水平又提升了一个level的幻觉,呵呵 ...
- linux0.11内核源码剖析:第一篇 内存管理、memory.c【转】
转自:http://www.cnblogs.com/v-July-v/archive/2011/01/06/1983695.html linux0.11内核源码剖析第一篇:memory.c July ...
- 菜鸟nginx源码剖析数据结构篇(九) 内存池ngx_pool_t[转]
菜鸟nginx源码剖析数据结构篇(九) 内存池ngx_pool_t Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.csdn. ...
随机推荐
- Spring Boot下的一种导入Excel文件的代码框架
1.前言 Spring Boot下如果只是导入一个简单的Excel文件,是容易的.网上类似的文章不少,有的针对具体的实体类,代码可重用性不高:有的利用反射机制或自定义注解,开发了Excel导入工具 ...
- html中各种标签和属性(最基础的基本都有)
1.标题标签: h1~h6 ctrl+1~6 2.段落标签: p ctrl+shift+p 3.换行标签: br shift+回车 4.水平线: hr 5.加粗标签: stro ...
- 『无为则无心』Python基础 — 14、Python流程控制语句(while循环语句)
目录 1.什么是循环结构 2.while循环 (1)while循环的语法 (2)while循环的应用 3.while语句的死循环 4.break和continue 5.while循环嵌套 (1)应用场 ...
- vue中$nextTick详细讲解保证你一看就明白
1.功能描述 今天我们要实现这个一个小功能: 页面渲染完成后展示一个div元素: 当点击这个div元素后: div元素消失: 出现一个input元素:并且input元素聚焦 想必大家我觉得简单,我们一 ...
- Qt之先用了再说系列-信号与槽
QT之信号与槽 简介:信号与槽可是Qt最大成功点,也是整个Qt基本核心机制,如果不会信号与槽,将无法领略Qt之美: 1.信号与槽函数原型: QObject::connect(const QObject ...
- Docker搭建mysql:5.7版本数据库
搭建MySQL: 1.启动测试mysql,拷贝容器内配置文件到宿主机 mkdr -P /server/docker/mysql/{data,conf} docker run -e MYSQL_ROOT ...
- centos 关闭SELINUX并重启系统
关闭SELINUX [root@bogon ~]# vim /etc/sysconfig/selinux ... SELINUX=disabled ... 执行过程: 重启系统 [root@bogo ...
- Java:Java的重写与重载区分
最明显的区别为:重写只存在于子类与父类中,重载存在于一个类中. 具体区别如下: 一.重写(override) override是重写(覆盖)了一个方法,以实现不同的功能.一般是用于子类在继承父类时,重 ...
- XCTF reverse maze
一.查壳 二.拖入ida64,静态调试,找到主函数F5反编译 二.1 思路分析(逆向是真的费时间,每个函数都要分析过去): 1.发现每个if最终都会进入LABEL-15 点进去,看看这个函数是干啥的. ...
- HGAME2020 reverse maze
1.查壳,发现是64位的linux文件,由于虚拟机没装linux,只能静态调了 2.拖入ida分析,找到主函数 2.1.思路分析 1.先将对应的ASCII的转换成字符,因为迷宫题一般都是用wsad,或 ...