C++ 程序启动过程、内存分配和释放原理
推荐阅读《程序员的自我修养》
CRT启动代码
CRT即C-Run-Time
main
函数并不是真正的入口点,在windows平台下,找到Visual Studio安装目录:VS2019\VC\Tools\MSVC\14.29.30037\crt\src\vcruntime
首先是exe_main.cpp
,英文注释已经说得很清楚了
1 | // |
然后在exe_common.inl
最底下323行
1 | // This is the common main implementation to which all of the CRT main functions |
在这个函数的上面235行就是__scrt_common_main_seh
的定义
1 | static __declspec(noinline) int __cdecl __scrt_common_main_seh() |
invoke_main
的定义在76行
1 | static int __cdecl invoke_main() |
就是它调用了main
函数
main函数启动之前做了什么
这部分大都已经过时了,了解大致思想就好
启动过程可以简化为
1 | mainCRTStartup // windows下的入口 |
其中_heap_init
会为crt创建专用的内存,_ioinit
会创建32个8字节的ioinfo
结构体,共256字节,用于初始化io,还会打开三个重要文件:stdin
、stdout
和stderr
关于保存打开的文件:使用一个FILE
数组_iob
保存,这个数组使用的是二级索引,最大可以索引2048。在一个FILE
结构体里有int _file
,这其中就保存了两级索引。stdin
、stdout
和stderr
分别就是这个数组的前三个元素。
_environ
是指针,指向指针数组,保存了一系列环境变量字符串,最后一个数组元素NULL
代表结束。
main函数参数个数为什么可以改变
在vcstartup_internal.h
中找到main
函数声明
1 | extern "C" int __CRTDECL main( |
main
函数声明为extern "C"
,因此会按照C风格进行编译,编译后不会带有参数类型名来区分重载(这也是为什么main
函数只能写一个)main
函数的Calling convention采用__cdecl
,参数从右向左压入栈中,栈的清空由主调函数完成,所以被调函数可以使用变长参数。而且生成汇编代码里函数名以下划线开头,并没有指明参数个数,所以可以正常调用。相比之下,__stdcall
的栈由被调函数处理,汇编函数名有@参数字节数
限制。
因此不论main
函数定义中使用了多少形参,形参类型是什么,最后编译成的名字都类似于_main
,可以被找到定义并调用。只不过形参定义为规定的形式才可以main
函数里正常使用压栈的参数。
表达式delete[]如何为每个数组元素调用dtor
在使用new [n]
开辟空间时,会额外分配一块空间,比如4字节,用来记录n的值,也就是数组的长度,而且dtor是逆序调用的,也就是从数组的最后一个元素开始调用
测试后发现,在VS2019上对数组用delete
删除会出现断言失败:_CrtIsValidHeapPointer
,说明那块计数空间也需要delete[]
来删除。但是当对象所属类型测试is_trivially_destructible_v
是true
时,就不会出错。猜想是new[]
采用了优化策略,没有设置额外的计数值,从而delete
也可以正确释放内存
delete数组一定会导致内存泄露吗
不一定,delete
调用的是operator delete
,其中调用了free
,会直接释放整个数组的内存空间,如果数组元素没有指针,或者析构函数本就是trivial
(不重要)的,那就不会有内存泄露问题。
free如何知道要释放的内存大小
首先要知道malloc
实际分配的内存情况:请求的内存大小+调试信息(比如内存用途、大小、文件名、行号,共32+4字节)+4*2的cookie+填充(补齐16字节的倍数)
这两个cookie就是实际分配内存的上下界限,其中记录了实际的内存大小,因为内存大小为16的倍数,所以最后一个16进制位一定是0,于是可以设置为1来标记内存已经被分配出去。于是在调用free
时,如果不是调试模式,没有调试信息,只要把指针上移4字节就可以找到上cookie,从而得知当初分配的内存大小。
关于把n对齐到16的倍数
1 | (n + (16-1)) & ~(16-1) |
free回收内存过程
回收内存时会对相邻的内存块进行合并
内存合并:当回收一块内存时查看内存块上面和下面是否有已经回收的,已回收(cookie最后一位为0)则合并(并更新cookie),因为每次都会与上下合并,所以只需要看自己的上下即可,而不用再走太远
查看上下内存的状态:当前指针指向自己内存开头
- 查看下面的:指针上移,查看cookie,得知自己内存大小,然后就可以下移到下面的内存的上cookie
- 查看上面的:指针上移再上移到上面一块的下cookie,所以如果没有下cookie就不知道上面内存的状态
当然也可以只有上cookie,只需要在一个cookie里同时保存本区块和上一个区块的长度即可(这时一个cookie占8字节)
最开始有几块内存不合并,它们的cookie全为1,从而作为特殊的分隔符