CppCon 2024 学习: C++ Under the Hood: (Internal Class Mechanisms) (续)
bp=>startup()bp=>bp+12b+8bp-4bp-..bp-Nsp=>Locals???stackparam argc,for main
1⃣ 总体布局
SVG 分为三列:
- Stack Frame [Base Pointer]
左侧显示基指针 BP,用于表示函数栈帧的起点。 - Stack [Stack Pointer]
中间显示实际栈内容(参数、局部变量、返回地址),栈从高地址向低地址增长。 - Instruction Pointer / Code History
右侧显示程序执行指令和调用历史,标注 IP(指令指针)当前位置。
2⃣ 栈帧(BP)解析
BP 用于定位当前函数的栈帧:
bp =>
- BP 保存调用者的 BP(prev bp)
- 栈帧布局可以表示为:
BPcurrent→prev BP \text{BP}_\text{current} \rightarrow \text{prev BP} BPcurrent→prev BP
函数栈帧构造流程(x86 cdecl):
push bp
mov bp, sp
sub sp, sizeof(locals)
数学公式表示局部变量和参数偏移:
- 局部变量 iii:
addr(locali)=BP−4(i+1) \text{addr(local}_i) = BP - 4(i+1) addr(locali)=BP−4(i+1) - 参数 iii:
addr(parami)=BP+4(i+2) \text{addr(param}_i) = BP + 4(i+2) addr(parami)=BP+4(i+2)
其中 4 表示 32 位整型或指针大小。
3⃣ 栈内容(SP)
中间列显示栈内容(从高地址到低地址):
param argc → main 的第一个参数
param *argv → main 的第二个参数
return addr → main 返回到 startup
0 → argv[argc] = 0
local var 1..N → main 的局部变量
??? → 未知或填充空间
栈指针 SP 变化规律:
push操作:
SP←SP−sizeof(data) SP \leftarrow SP - \text{sizeof(data)} SP←SP−sizeof(data)pop操作:
SP←SP+sizeof(data) SP \leftarrow SP + \text{sizeof(data)} SP←SP+sizeof(data)
4⃣ 指令序列 / IP
右列显示 mainCRTStartup() 调用 main() 的指令:
push param
push param
call main(...)
ip =>
add sp, sizeof(params)
exit(ax)
解释:
push param→ 压入参数call main(...)→ 压入返回地址并跳转- main 栈帧构造:
push bp
mov bp, sp
sub sp, sizeof(locals)
- 函数返回后:
add sp, sizeof(params) ← 清理参数
mov sp, bp ← 恢复栈顶
pop bp ← 恢复基指针
ret ← 返回调用者
返回地址数学表示:
return address=IPafter call \text{return address} = IP_\text{after call} return address=IPafter call
5⃣ main 内部栈帧示意
C 代码示意:
int main(int argc, char** argv) {
int i1, i2, i3;
...
return Func1(i1, ..., 3);
}
- 参数和局部变量在栈上的偏移:
argc=[BP+8]argv=[BP+12]argv[argc]=0local vari=BP−4(i+1) \begin{aligned} \text{argc} &= [BP + 8] \\ \text{argv} &= [BP + 12] \\ \text{argv[argc]} &= 0 \\ \text{local var}_i &= BP - 4(i+1) \end{aligned} argcargvargv[argc]local vari=[BP+8]=[BP+12]=0=BP−4(i+1) - 调用 Func1():
push param_i
call Func1
add sp, sizeof(params)
数学公式描述 SP 变化:
SPafter push=SPbefore push−4 SP_\text{after push} = SP_\text{before push} - 4 SPafter push=SPbefore push−4
6⃣ 栈帧偏移总结
进入 main() 前,栈布局(高地址 → 低地址):
SP+0=return address (startup)SP+4=argcSP+8=argvSP+12=argv[argc]=0 \begin{matrix} SP + 0 & = & \text{return address (startup)} \\ SP + 4 & = & argc \\ SP + 8 & = & argv \\ SP + 12 & = & argv[argc] = 0 \end{matrix} SP+0SP+4SP+8SP+12====return address (startup)argcargvargv[argc]=0
- main 设置 BP:
BPmain=SPstartup BP_\text{main} = SP_\text{startup} BPmain=SPstartup - main 内局部变量偏移:
local∗i=BP∗main−4(i+1) \text{local}*i = BP*\text{main} - 4(i+1) local∗i=BP∗main−4(i+1)
7⃣ 栈和指令关系
- 红色
ip =>→ 当前执行指令位置 - 红色
param→ 栈上参数 - 绿色
0→ argv[argc] = 0 (prev bp)→ 调用者的 BP,指向前一栈帧
函数调用和返回数学化:
call Func1⇒SP减少,return⇒SP恢复 \text{call Func1} \Rightarrow SP \text{减少},\quad \text{return} \Rightarrow SP \text{恢复} call Func1⇒SP减少,return⇒SP恢复
总结
这个 SVG 详细展示了:
- startup() → main() → Func1() 栈帧构造
- 参数、局部变量、返回地址在栈上的偏移
- SP / BP / IP 的动态变化规律
- x86 cdecl 栈操作流程
好的,我们来详细分析你提供的 SVG 图,它描述了一个 C/C++ 程序从 mainCRTStartup 到 main() 调用及函数调用栈 的结构,同时展示了 栈帧、栈指针、基指针和指令指针的变化。我会结合解释,并在必要时用数学公式表示栈帧和偏移。
1. 栈帧概念
在 x86/x64 调用约定下,每次函数调用都会在栈上生成一个 栈帧 (Stack Frame),包含:
- 参数 (parameters)
- 返回地址 (return address)
- 保存的基指针 (saved BP / prev bp)
- 局部变量 (locals)
通常结构如下:
栈帧=参数⏟∗从高地址往低地址压栈+返回地址⏟∗由 call 指令压入+保存的 BP⏟∗prev bp+局部变量⏟∗sp-偏移 \text{栈帧} = \underbrace{\text{参数}}*{\text{从高地址往低地址压栈}} + \underbrace{\text{返回地址}}*{\text{由 call 指令压入}} + \underbrace{\text{保存的 BP}}*{\text{prev bp}} + \underbrace{\text{局部变量}}*{\text{sp-偏移}} 栈帧= 参数∗从高地址往低地址压栈+ 返回地址∗由 call 指令压入+ 保存的 BP∗prev bp+ 局部变量∗sp-偏移
在你的 SVG 中:
- 左侧标注
Stack Frame[Base Pointer] - 中间标注
Stack[Stack Pointer] - 右侧标注
Instruction Pointer/Code History
2. mainCRTStartup 到 main 调用过程
栈帧布局 (图中中间区域)
从图中可以看到 mainCRTStartup(...) 的操作:
- 保存 BP
将前一个函数的基指针压栈,并用 BP 保存当前 SP 位置。push bp mov bp, sp - 分配局部变量
将 SP 向下移动,为局部变量开辟空间。sub sp, sizeof(locals) - 压入参数
准备调用push param1 push param2 ...main函数。 - 调用
main
此时 CPU 会将返回地址压入栈中,IP 指向call main(...)main函数。 - 函数返回后的恢复
add sp, sizeof(params) mov sp, bp pop bp ret- 清理参数空间
- 恢复 SP 到函数入口时的 BP
- 弹出保存的 BP
- 返回调用点
栈帧示意(根据 SVG)
假设 SP 向下增长,地址从高到低排列:
高地址
----------------------
paramN (Func1 参数)
...
param1
返回地址 (return addr)
保存 BP (prev bp)
局部变量 N
...
局部变量 1
低地址
图中红色标注:
(prev bp)→ 保存的 BPparam argc, *argv→main()的参数local var 1, ..., local var N→ 局部变量
SP 和 BP 的关系:- 调用前:
sp = bp - 分配局部变量后:
sp = bp - sizeof(locals) - 调用参数压栈后:
sp = bp - sizeof(locals) - sizeof(params)
用公式表示:
spcall=bp−locals_size−params_size sp_\text{call} = bp - \text{locals\_size} - \text{params\_size} spcall=bp−locals_size−params_size
3. 参数和局部变量访问
栈帧提供了 BP 作为访问基准:
- 参数访问:
bp + offset - 局部变量访问:
bp - offset
图中的例子: bp + 8, bp + 12→ 参数bp - 4, bp - N→ 局部变量
公式表示:
参数 i:address=bp+4⋅(i+1)(假设每个参数 4 字节) \text{参数 } i: \text{address} = bp + 4 \cdot (i+1) \quad (\text{假设每个参数 4 字节}) 参数 i:address=bp+4⋅(i+1)(假设每个参数 4 字节)
局部变量 j:address=bp−4⋅j \text{局部变量 } j: \text{address} = bp - 4 \cdot j 局部变量 j:address=bp−4⋅j
4. 指令指针和代码历史
右侧的 Instruction Pointer/Code History 区域显示了:
ip =>当前指向的指令- 红色标注
push param/call Func1→ 表示当前执行的指令 - 栈操作按顺序记录,便于回溯函数调用历史
这就是 调试器中栈跟踪 (stack trace) 的逻辑。
5. 总结
SVG 展示了一个典型的 C/C++ 函数调用和栈帧布局:
- SP 指向栈顶,BP 保存基地址
- 栈向下增长,局部变量在 BP 下方,参数在 BP 上方
- 调用前 BP 和 SP 保存前一函数栈帧
- 调用函数时,参数压栈,返回地址压栈
- 函数返回时恢复 SP 和 BP,执行 ret 返回调用点
访问公式总结:
- 参数:addrparam=bp+kaddr_\text{param} = bp + kaddrparam=bp+k
- 局部变量:addrlocal=bp−kaddr_\text{local} = bp - kaddrlocal=bp−k
- 调用前 SP:spbefore=bpsp_\text{before} = bpspbefore=bp
- 调用后 SP:spafter=bp−locals_size−params_sizesp_\text{after} = bp - \text{locals\_size} - \text{params\_size}spafter=bp−locals_size−params_size
这个结构非常适合理解 函数调用、栈帧布局、BP/SP/IP 关系,也是调试和逆向分析的基础。
1. 栈和栈帧的整体概念
在程序运行时,每个函数调用都会在栈(Stack)上分配一个 栈帧(Stack Frame),用来存储函数的:
- 参数(Parameters)
- 局部变量(Local Variables)
- 返回地址(Return Address)
- 上一个栈帧的基指针(Previous Base Pointer, BP)
栈帧从上向下生长(在 x86 架构下),即 栈顶指针 SP(Stack Pointer)向低地址方向移动。 - 栈顶指针 spspsp 指向当前栈的顶部
- 基指针 bpbpbp 指向当前函数的栈帧基址
一个函数调用的典型过程如下:
- 保存上一个栈帧的 BP:
push bp - 设置新的 BP:
mov bp, sp - 分配局部变量空间:
sub sp, sizeof(locals) - 函数体执行
- 恢复 SP 和 BP:
mov sp, bp; pop bp - 返回:
ret
在图中,这对应红色箭头标记的bp =>和sp =>。
2. 栈帧结构详解
图中以矩形块表示栈帧,每个矩形块对应一个函数的栈帧,里面标注了:
- 函数参数(param1, param2, …)
- 局部变量(local var 1, …, local var N)
- 返回地址(return addr)
假设我们有一个函数调用链:
main() → Func1(...) → Func2(...)
对应栈帧布局如下(高地址在上,低地址在下):
Stack Top (高地址)[Func2 的参数][Func2 的局部变量][Func2 返回地址][Func1 的参数][Func1 的局部变量][Func1 返回地址][main 的参数][main 的局部变量][main 返回地址]Stack Bottom (低地址) \text{Stack Top (高地址)} \\ \text{[Func2 的参数]} \\ \text{[Func2 的局部变量]} \\ \text{[Func2 返回地址]} \\ \text{[Func1 的参数]} \\ \text{[Func1 的局部变量]} \\ \text{[Func1 返回地址]} \\ \text{[main 的参数]} \\ \text{[main 的局部变量]} \\ \text{[main 返回地址]} \\ \text{Stack Bottom (低地址)} Stack Top (高地址)[Func2 的参数][Func2 的局部变量][Func2 返回地址][Func1 的参数][Func1 的局部变量][Func1 返回地址][main 的参数][main 的局部变量][main 返回地址]Stack Bottom (低地址)
在图里:
param argc, argv对应main的参数local var 1,...,N对应main的局部变量return addr (in startup)对应调用main的返回地址(这里是startup())
BP 与 SP 的作用
- bpbpbp(Base Pointer):固定指向当前栈帧底部
- spspsp(Stack Pointer):指向当前栈顶,可动态变化
每次函数调用时,新的 bpbpbp 会保存旧 bpbpbp 的值,从而形成链表式结构:
bpcurrent→bpprev→… bp_{\text{current}} \to bp_{\text{prev}} \to \dots bpcurrent→bpprev→…
这就是所谓的 帧指针链(Frame Pointer Chain)。
3. 程序调用流程
根据图中右侧的代码历史:
mainCRTStartup:这是 C 运行时的入口,负责初始化 C 运行环境,并最终调用main()。main():程序的主函数,参数为argc和argv。Func1(i1,...,i3):主函数调用的用户函数Func2(...):Func1内部调用的另一个函数
函数调用在栈上的操作:
- push 参数:将函数参数压入栈中
sp:=sp−sizeof(params) sp := sp - \text{sizeof(params)} sp:=sp−sizeof(params) - call 函数:将返回地址压入栈,并跳转到函数入口
push return_addr; ip:=func_addr \text{push return\_addr}; \ ip := func\_addr push return_addr; ip:=func_addr - 在函数内分配局部变量:
sp:=sp−sizeof(locals) sp := sp - \text{sizeof(locals)} sp:=sp−sizeof(locals)
图中的红色箭头标记了 当前 IP(Instruction Pointer)的位置,说明 CPU 正在执行哪个函数。
4. 局部变量和参数的存储关系
以 Func1 为例:
bp+8 → 第1个参数 param1
bp+12 → 第2个参数 param2
...
bp-4 → 第1个局部变量
bp-8 → 第2个局部变量
...
- 参数存储在 BP 的正偏移
- 局部变量存储在 BP 的负偏移
这是 x86 常见的 栈帧布局公式:
argi=[bp+2(i+1)](i 从 0 开始) \text{arg}_i = [bp + 2(i+1)] \quad (\text{i 从 0 开始}) argi=[bp+2(i+1)](i 从 0 开始)
localj=[bp−j×size_of(var)] \text{local}_j = [bp - j \times \text{size\_of(var)}] localj=[bp−j×size_of(var)]
图中用矩形块标出每个局部变量和参数位置。
5. 可视化理解
SVG 图分为三大部分:
- 左侧 BP 栈帧:标记每个函数的 BP、局部变量、参数
- 中间 Stack:SP 指向的实际栈内容
- 右侧 Instruction Pointer/Code History:程序执行历史,显示 push、call、ret 等汇编操作
- 红色箭头表示当前执行的指令(IP)
- 绿色或灰色箭头表示栈指针变化
这个可视化把 抽象的栈操作 变成了 直观的矩形和箭头,可以清晰地看到函数调用链和内存布局。
6. 栈帧数学公式总结
- 栈帧大小:
StackFrameSize=sizeof(locals)+sizeof(params)+sizeof(return_addr)+sizeof(prev_bp) \text{StackFrameSize} = \text{sizeof(locals)} + \text{sizeof(params)} + \text{sizeof(return\_addr)} + \text{sizeof(prev\_bp)} StackFrameSize=sizeof(locals)+sizeof(params)+sizeof(return_addr)+sizeof(prev_bp) - 栈顶位置:
spnew=bp−sizeof(locals) sp_{\text{new}} = bp - \text{sizeof(locals)} spnew=bp−sizeof(locals) - 局部变量访问:
locali=[bp−offseti] local_i = [bp - offset_i] locali=[bp−offseti] - 参数访问:
parami=[bp+offseti] param_i = [bp + offset_i] parami=[bp+offseti]
1⃣ 顶部文字说明
- Stack Frame [Base Pointer] → 描述栈帧起点(BP)。
- Stack [Stack Pointer] → 描述栈当前指针(SP)。
- Instruction Pointer / Code History → 描述指令执行历史或调用链。
2⃣ 栈帧可视化
- 栈向下增长(通常内存地址从高到低)。
- 栈帧中包含:
- 函数参数(
param argc, param argv, param1 for Func1/Func2...)。 - 返回地址(Return Address)。
- 保存的前一个 BP(
(prev bp))。 - 局部变量(
local var 1..N of main/Func1/Func2)。
- 函数参数(
- 每个函数都有自己的栈帧:
- main() → 参数
argc/argv,局部变量i1,i2,i3,... - Func1(…) → 参数
p1,p2,p3,局部变量i1,i2,i3 - Func2(…) → 参数
p1,p2,p3,局部变量i1,i2,i3
- main() → 参数
- 红色文本表示函数参数。
- 黑色文本表示局部变量。
- 绿色文本标记栈地址或初始值(如
0)。
3⃣ 栈帧箭头
bp =>表示当前函数栈帧的基指针。- 每个函数栈帧通过
(prev bp)连接上一个函数的栈帧。
4⃣ 右侧“代码历史”
- 描述栈操作对应的汇编指令:
这些就是典型的函数进入和退出过程:push bp mov bp, sp sub sp, sizeof(locals) mov ax,... mov sp, bp pop bp ret- 保存旧 BP。
- 设置新的 BP。
- 分配局部变量空间。
- 执行函数体。
- 恢复 SP 和 BP,返回调用函数。
5⃣ 参数与局部变量关系
- 栈中 参数靠高地址(bp+X),局部变量靠低地址(bp-4…bp-N)。
- 返回地址位于 参数和局部变量之间。
- 栈帧布局和函数调用顺序紧密相关:
main() ├─ param argc/argv ├─ return addr (in startup) ├─ local vars └─ calls Func1() ├─ param p1,p2,p3 ├─ return addr (in main) └─ local vars
总结
这个 SVG 图是一个典型的 函数调用栈示意图:
- 左边显示栈帧结构(BP/SP、参数、局部变量、返回地址)。
- 右边显示对应的 C++ 函数以及汇编指令操作历史。
- 可以用来理解:
- 栈帧布局
- 函数调用顺序
- 参数传递与局部变量分配
- 调用与返回的汇编过程
1⃣ Func2 的栈帧结构回顾
从图中可见,Func2 栈帧大致如下:
[高地址] <- SP/BP 向下增长
+-------------------+
| 参数 N ... | <- 调用者传入的参数
+-------------------+
| 返回地址 | <- Func1 的下一条指令
+-------------------+
| 上一个 BP | <- Func1 的 BP(prev bp)
+-------------------+
| 局部变量 1..N | <- Func2 内部局部变量
+-------------------+
[低地址] <- SP
- 局部变量:存放函数内部临时变量。
- 保存的 BP:用来恢复调用者的栈帧基址。
- 返回地址:CPU 用来返回调用者的下一条指令。
2⃣ 函数返回的汇编流程
典型 x86 汇编返回流程(假设使用帧指针 BP):
mov sp, bp ; 将 SP 恢复到函数开始时的 BP(去掉局部变量)
pop bp ; 恢复调用者的 BP
ret ; 弹出返回地址到 IP,跳转回调用者
步骤解释:
- 恢复 SP:
sp = bp→ 释放局部变量空间,栈指针回到保存 BP 的位置。
- 恢复 BP:
pop bp→ 栈顶现在是保存的前一个 BP(Func1 的 BP),恢复调用者的栈帧链。
- 跳转返回:
ret→ 弹出栈顶的返回地址到指令指针 IP,跳回调用Func2的下一条指令。
3⃣ 栈帧示意
从你 SVG 中的标记来看:
Func1 栈帧
+-----------------+
| Func1 locals |
| ... |
| prev BP |
| return addr | <- Func1 调用 Func2 之后的下一条指令
+-----------------+
Func2 栈帧
| Func2 locals |
| ... |
| prev BP | <- 保存 Func1 的 BP
| return addr | <- Func2 返回地址
| Func2 params |
+-----------------+
返回步骤对应:
- 栈顶 SP 指向
Func2局部变量。 mov sp,bp→ SP 指向保存的前一个 BP。pop bp→ 恢复 Func1 的 BP。ret→ 弹出返回地址跳回 Func1 的下一条指令。
4⃣ 返回到 Func1
返回后,栈帧恢复成调用前的状态:
Func1 栈帧
+-----------------+
| Func1 locals |
| ... |
| prev BP |
| return addr | <- main 或上一层调用
+-----------------+
- SP 指向 Func1 的局部变量区。
- BP 指向 Func1 的栈帧基址。
- CPU IP 指向 Func1 调用
Func2的下一条指令。
总结关键点: Func2返回时只修改 SP 和 BP,然后通过ret跳回调用者。- 局部变量会被自动“丢弃”,栈空间被释放。
- 调用链通过保存的 BP 串起来,便于调试和堆栈回溯。
1⃣ 继承(Inheritance)与聚合(Aggregation)
概念:
- 继承(Inheritance):子类拥有父类的所有成员,可以重写父类虚函数。底层通常通过 内存连续布局 + vtable 指针 实现多态。
- 聚合(Aggregation):类中包含另一个类作为成员,成员对象独立存在,不参与多态机制。
内存布局:
假设有单继承:
class Base { int a; virtual void f(); };
class Derived : public Base { int b; };
内存布局通常:
[Derived Object]
+----------------+
| vptr -> vtable |
| a | <- Base 成员
| b | <- Derived 成员
+----------------+
vptr(v-table pointer)指向虚表,管理虚函数调用。- 多重继承时,可能有多个 vptr,按继承顺序排列。
2⃣ v-table 指针放置
- 构造函数(Constructor):
- 打开花括号
{时,立即 设置 vptr 指向当前类的虚表。 - 原因:虚函数可能在构造函数内部调用,需要正确的动态绑定。
- 打开花括号
- 析构函数(Destructor):
- 打开花括号
{时,也设置 vptr,确保析构过程遵循 Inside-Out / Outside-In 原则。- Inside-Out 构造:从基类到子类。
- Outside-In 析构:从子类到基类。
- 打开花括号
3⃣ 指针 vs 成员指针
| 类型 | 说明 |
|---|---|
int* p |
普通指针,指向一个 int |
int Foo::* mp |
成员指针,指向类 Foo 的成员 int |
int(*f)(int) |
普通函数指针,指向函数 int(int) |
int(Foo::*mf)(int) |
成员函数指针,指向类 Foo 的成员函数 |
- 成员指针 不能直接解引用,必须结合对象实例:
Foo obj; obj.*mp = 42; // 对象访问成员指针 (obj.*mf)(5); // 对象调用成员函数指针
4⃣ nullptr 的魔法
- C++11 引入
nullptr,是 类型安全的空指针,不等同于整数 0。 - 用法:
int* p = nullptr; // 空指针 Foo* f = nullptr; // 空对象指针
5⃣ 栈帧(Stack Frames)与基指针(BP)
栈帧结构(单函数):
高地址
+----------------+
| 参数 | <- 调用者传入参数
+----------------+
| 返回地址 | <- CPU IP 返回位置
+----------------+
| 上一个 BP | <- 保存调用者 BP
+----------------+
| 局部变量 | <- 当前函数局部
+----------------+
低地址
返回机制公式化:
- 保存前 BP:
BPold=BP BP_{old} = BP BPold=BP - 设置新 BP:
BP=SP BP = SP BP=SP - 局部变量空间:
SP=SP−locals_size SP = SP - \text{locals\_size} SP=SP−locals_size - 返回时:
SP=BP SP = BP SP=BP
BP=pop() BP = \text{pop()} BP=pop()
IP=pop() return address IP = \text{pop() return address} IP=pop() return address
调试与栈遍历
- 可以通过 BP 链遍历函数调用栈:
BP0→BP1→BP2→...→0 BP_0 \rightarrow BP_1 \rightarrow BP_2 \rightarrow ... \rightarrow 0 BP0→BP1→BP2→...→0 - 每个 BP 保存调用者的基指针,配合返回地址可以回溯整个调用链。
6⃣ 其他要点
- First shall be last:
- 构造函数先构造基类,后构造子类;析构函数逆序。
- Inside-Out / Outside-In:
- 构造:Base → Derived
- 析构:Derived → Base
总结:
- 继承 vs 聚合:继承内存连续,聚合对象独立。
- vptr 安置:Ctor/Dtor 开头设置,保证虚函数调用正确。
- 指针类型差异:普通指针 vs 成员指针、函数指针。
- nullptr:类型安全空指针。
- 栈帧与 BP:函数调用、返回机制、栈遍历。
- 构造/析构顺序:Inside-Out / Outside-In,保证对象完整性。
C++ Under the Hood –Constructor/Destructor Order
#include <iostream>
using std::cout;
struct Foo
{
Foo() { cout << "Foo::Foo() this=" << this << "\n"; }
~Foo() { cout << "~Foo::Foo() this=" << this << "\n"; }
};
int main()
{
cout << "new Foo[5];\n";
Foo* pFoo = new Foo[5];
cout << "Foo foo[5];\n";
Foo foo[5];
cout << "delete[] pFoo;\n";
delete[] pFoo;
cout << "//end of scope main()\n";
}
输出
new Foo[5];
Foo::Foo() this=0x1e37f2c8
Foo::Foo() this=0x1e37f2c9
Foo::Foo() this=0x1e37f2ca
Foo::Foo() this=0x1e37f2cb
Foo::Foo() this=0x1e37f2cc
Foo foo[5];
Foo::Foo() this=0x7fff7cdcdc63
Foo::Foo() this=0x7fff7cdcdc64
Foo::Foo() this=0x7fff7cdcdc65
Foo::Foo() this=0x7fff7cdcdc66
Foo::Foo() this=0x7fff7cdcdc67
delete[] pFoo;
~Foo::Foo() this=0x1e37f2cc
~Foo::Foo() this=0x1e37f2cb
~Foo::Foo() this=0x1e37f2ca
~Foo::Foo() this=0x1e37f2c9
~Foo::Foo() this=0x1e37f2c8
//end of scope main()
~Foo::Foo() this=0x7fff7cdcdc67
~Foo::Foo() this=0x7fff7cdcdc66
~Foo::Foo() this=0x7fff7cdcdc65
~Foo::Foo() this=0x7fff7cdcdc64
~Foo::Foo() this=0x7fff7cdcdc63
好的,我们来详细解析这个 C++ 例子中的 构造函数和析构函数调用顺序,用结合公式和底层原理进行理解。
1⃣ 代码行为回顾
代码中有两种对象创建方式:
- 动态数组:
Foo* pFoo = new Foo[5];
- 使用
new[]动态分配内存并调用构造函数。
- 自动数组(栈上):
Foo foo[5];
- 栈上分配,作用域结束时自动析构。
2⃣ 构造函数调用顺序
观察输出:
Foo::Foo() this=0x1e37f2c8
Foo::Foo() this=0x1e37f2c9
Foo::Foo() this=0x1e37f2ca
Foo::Foo() this=0x1e37f2cb
Foo::Foo() this=0x1e37f2cc
- 动态数组
new Foo[5]构造顺序:
Foo[0],Foo[1],Foo[2],Foo[3],Foo[4] Foo[0], Foo[1], Foo[2], Foo[3], Foo[4] Foo[0],Foo[1],Foo[2],Foo[3],Foo[4] - 栈上数组
Foo foo[5]构造顺序:
foo[0],foo[1],foo[2],foo[3],foo[4] foo[0], foo[1], foo[2], foo[3], foo[4] foo[0],foo[1],foo[2],foo[3],foo[4]
结论:数组构造总是从低地址到高地址依次调用构造函数。
3⃣ 析构函数调用顺序
观察输出:
~Foo::Foo() this=0x1e37f2cc
~Foo::Foo() this=0x1e37f2cb
~Foo::Foo() this=0x1e37f2ca
~Foo::Foo() this=0x1e37f2c9
~Foo::Foo() this=0x1e37f2c8
- 动态数组
delete[] pFoo析构顺序:
Foo[4],Foo[3],Foo[2],Foo[1],Foo[0] Foo[4], Foo[3], Foo[2], Foo[1], Foo[0] Foo[4],Foo[3],Foo[2],Foo[1],Foo[0] - 栈上数组析构(作用域结束):
foo[4],foo[3],foo[2],foo[1],foo[0] foo[4], foo[3], foo[2], foo[1], foo[0] foo[4],foo[3],foo[2],foo[1],foo[0]
结论:数组析构总是从高地址到低地址逆序调用析构函数,符合 Inside-Out / Outside-In 原则。
4⃣ 内存布局
以动态数组为例(假设每个 Foo 占 1 个单位地址):
低地址
0x1e37f2c8 Foo[0]
0x1e37f2c9 Foo[1]
0x1e37f2ca Foo[2]
0x1e37f2cb Foo[3]
0x1e37f2cc Foo[4]
高地址
- 构造顺序:0 → 4
- 析构顺序:4 → 0
栈上数组布局类似,只是地址在栈区域,低地址在底部,高地址在栈顶。
5⃣ 数学公式化理解
- 构造函数顺序:
for i=0 to N−1:construct(Foo[i]) \text{for } i=0 \text{ to } N-1: \quad \text{construct}(Foo[i]) for i=0 to N−1:construct(Foo[i]) - 析构函数顺序:
for i=N−1 down to 0:destruct(Foo[i]) \text{for } i=N-1 \text{ down to } 0: \quad \text{destruct}(Foo[i]) for i=N−1 down to 0:destruct(Foo[i])
其中 NNN 是数组长度。
- 动态数组内存分配与栈数组内存分配只是 分配位置不同:
- 动态数组:堆内存
- 栈数组:栈内存
6⃣ 总结理解
- 构造顺序:数组从低地址到高地址依次构造。
- 析构顺序:数组从高地址到低地址依次析构。
- new[]/delete[] 与栈数组:
- 本质相同:依次构造,逆序析构。
- 不同点:分配位置(堆 vs 栈)和生命周期(手动 delete[] vs 自动作用域)。
- Inside-Out / Outside-In 原则:
- 构造:外层(基类/低索引) → 内层(派生类/高索引)
- 析构:内层 → 外层
#include <iostream>
using std::cout;
// =========================== Foo 类 ===========================
// 一个简单的类,包含构造函数、析构函数和成员函数 In()
struct Foo {
Foo() { cout << "Foo::Foo()\tthis=" << this << "\n"; } // 构造函数,打印对象地址
~Foo() { cout << "~Foo::Foo()\tthis=" << this << "\n"; } // 析构函数,打印对象地址
void In() { cout << "Foo::In()\tthis=" << this << "\n"; } // 成员函数,打印调用对象地址
};
// =========================== 函数内 static 对象 ===========================
// Bar() 返回一个函数内 static Foo 对象的引用
Foo& Bar() {
static Foo foo; // 函数内 static 对象,第一次调用 Bar() 时初始化
// 生命周期贯穿整个程序,析构在程序结束
return foo;
}
// =========================== 全局 static 对象 ===========================
static Foo foo; // 静态全局对象,程序启动时初始化,程序结束时析构
// =========================== Qaz 类 ===========================
// 包含一个 inline static 成员 Foo
struct Qaz {
static inline Foo foo; // C++17 起支持 inline static 成员,类共享,生命周期贯穿整个程序
Qaz() {
cout << "Qaz::Qaz()\tthis=" << this << "\n";
foo.In(); // 构造函数中调用类共享的 inline static Foo
}
~Qaz() {
cout << "~Qaz::Qaz()\tthis=" << this << "\n";
foo.In(); // 析构函数中调用类共享的 inline static Foo
}
} qaz; // 全局 Qaz 对象,程序启动时初始化,结束时析构
// =========================== main ===========================
int main()
{
cout << "\nint main()\n";
// 调用 Bar() 返回的函数内 static 对象并调用 In()
cout << "Bar().In();\n";
Bar().In(); // 如果是第一次调用 Bar(),会先构造静态对象 foo
// 调用全局 static Foo 对象 foo 的成员函数 In()
cout << "foo.In();\n";
foo.In();
cout << "\n//end of scope main()\n\n";
}
输出
Foo::Foo() this=0x4041a8
Foo::Foo() this=0x4041a9
Qaz::Qaz() this=0x404198
Foo::In() this=0x4041a9
int main()
Bar().In();
Foo::Foo() this=0x404199
Foo::In() this=0x404199
foo.In();
Foo::In() this=0x4041a8
//end of scope main()
~Foo::Foo() this=0x404199
~Qaz::Qaz() this=0x404198
Foo::In() this=0x4041a9
~Foo::Foo() this=0x4041a9
~Foo::Foo() this=0x4041a8
1⃣ 代码涉及对象类型
代码里涉及几类对象:
- 全局静态对象
static Foo foo;
- 在 main() 之前初始化
- 析构在 main() 之后自动调用
- 静态局部对象(函数内)
Foo& Bar() {
static Foo foo;
return foo;
}
- 第一次调用
Bar()时初始化(懒汉式) - 程序结束时析构
- 类的静态成员对象
struct Qaz {
static inline Foo foo;
Qaz() { foo.In(); }
} qaz;
static inline Foo foo;属于 类静态成员- 构造在程序启动阶段(和全局对象类似)
- 析构在程序结束阶段
Qaz qaz;是 全局非静态对象,构造函数执行时调用静态成员函数foo.In()
2⃣ 构造顺序分析
观察输出:
Foo::Foo() this=0x4041a8 // static Foo foo;
Foo::Foo() this=0x4041a9 // Qaz::foo (静态成员)
Qaz::Qaz() this=0x404198 // qaz
Foo::In() this=0x4041a9 // Qaz::构造中调用 Qaz::foo.In()
- 全局静态对象
static Foo foo最先构造- 地址 0x4041a8
- 类静态成员对象
Qaz::foo构造- 地址 0x4041a9
- 全局非静态对象
Qaz qaz构造- 地址 0x404198
- 构造函数内调用
foo.In():- 输出地址 0x4041a9,确认是 静态成员 Foo
结论:
全局/静态对象→类静态成员对象→全局非静态对象构造体内部初始化 \text{全局/静态对象} \to \text{类静态成员对象} \to \text{全局非静态对象构造体内部初始化} 全局/静态对象→类静态成员对象→全局非静态对象构造体内部初始化
3⃣ main() 内部调用
int main()
Bar().In(); // 静态局部对象初始化
foo.In(); // 已构造全局对象调用
输出:
Foo::Foo() this=0x404199 // Bar() 中的静态局部对象首次初始化
Foo::In() this=0x404199
Foo::In() this=0x4041a8 // 全局对象 foo
- 静态局部对象
Bar()在第一次使用时初始化(懒汉式) - 全局对象
foo已在 main() 前构造
结论:
静态局部对象初始化:第一次使用时全局对象/静态成员对象初始化:程序开始阶段 \text{静态局部对象初始化:第一次使用时} \quad \text{全局对象/静态成员对象初始化:程序开始阶段} 静态局部对象初始化:第一次使用时全局对象/静态成员对象初始化:程序开始阶段
4⃣ 析构顺序分析
观察输出:
~Foo::Foo() this=0x404199 // Bar() 静态局部对象析构
~Qaz::Qaz() this=0x404198 // 全局对象 qaz 析构
Foo::In() this=0x4041a9 // Qaz::foo 静态成员析构时调用
~Foo::Foo() this=0x4041a9 // Qaz::foo 析构
~Foo::Foo() this=0x4041a8 // 全局对象 foo 析构
- 析构顺序(程序退出阶段):
静态局部对象→全局非静态对象→静态成员对象/全局静态对象 \text{静态局部对象} \to \text{全局非静态对象} \to \text{静态成员对象/全局静态对象} 静态局部对象→全局非静态对象→静态成员对象/全局静态对象 - 特点:构造顺序与析构顺序相反,符合 Inside-Out / Outside-In 原则。
5⃣ 内存地址总结
| 对象类型 | 地址 | 构造顺序 | 析构顺序 |
|---|---|---|---|
全局静态对象 foo |
0x4041a8 | 1 | 4 |
类静态成员 Qaz::foo |
0x4041a9 | 2 | 3 |
全局非静态对象 qaz |
0x404198 | 3 | 2 |
静态局部对象 Bar() |
0x404199 | 4 | 1 |
6⃣ 数学公式化理解
- 构造顺序(程序启动阶段 + 第一次调用):
Oglobal/static→Ostatic member→Oglobal non-static→Ostatic local (lazy) O_\text{global/static} \to O_\text{static member} \to O_\text{global non-static} \to O_\text{static local (lazy)} Oglobal/static→Ostatic member→Oglobal non-static→Ostatic local (lazy) - 析构顺序(程序退出阶段):
Ostatic local→Oglobal non-static→Ostatic member→Oglobal/static O_\text{static local} \to O_\text{global non-static} \to O_\text{static member} \to O_\text{global/static} Ostatic local→Oglobal non-static→Ostatic member→Oglobal/static - Inside-Out / Outside-In 原则:
构造:从依赖对象的外层 → 内层析构:从内层 → 外层 \text{构造:从依赖对象的外层 → 内层} \quad \text{析构:从内层 → 外层} 构造:从依赖对象的外层 → 内层析构:从内层 → 外层
#include <iostream>
using std::cout;
#include <memory>
using std::make_shared;
#include <map>
// =========================== 类定义 ===========================
// A 类:普通成员 map,每个对象有独立的 map
struct A {
auto& Get() { return map; } // 返回成员 map 的引用
private:
std::map<int,int> map; // 每个 A 对象独立拥有的 map
};
// B 类:inline static 成员 map,属于类共享
struct B {
static auto& Get() { return map; } // 返回类级别共享 map 的引用
private:
inline static std::map<int,int> map; // C++17 起支持 inline static 成员
// 该 map 在所有 B 对象中共享,生命周期贯穿整个程序
};
// C 类:inline static shared_ptr 指向 map
struct C {
static auto& Get() { return *map; } // 返回共享指针指向的 map 引用
private:
inline static auto map = std::make_shared<std::map<int,int>>();
// 静态 shared_ptr,只会初始化一次,生命周期贯穿整个程序
};
// D 类:函数内 static map,懒汉式初始化
struct D {
static auto& Get() {
static std::map<int,int> map; // 第一次调用 Get 时初始化
return map; // 所有调用返回同一个 map
}
};
// E 类:函数内 static shared_ptr,懒汉式初始化
struct E {
static auto& Get() {
static auto map = std::make_shared<std::map<int,int>>(); // 第一次调用初始化
return *map; // 返回 map 的引用
}
};
// =========================== 全局对象 ===========================
A a; // 普通全局对象,拥有自己的 map
B b; // 全局对象,但 map 是类共享的
C c; // 全局对象,map 是 inline static shared_ptr
D d; // 全局对象,map 通过函数内 static 创建
E e; // 全局对象,map 通过函数内 static shared_ptr 创建
// =========================== 智能指针对象 ===========================
auto pA = std::make_shared<A>(); // 堆上对象,map 独立
auto pB = std::make_shared<B>(); // 堆上对象,但 map 是类共享
auto pC = std::make_shared<C>(); // 堆上对象,但 map 是 static shared_ptr
auto pD = std::make_shared<D>(); // 堆上对象,但 map 是函数内 static
auto pE = std::make_shared<E>(); // 堆上对象,但 map 是函数内 static shared_ptr
// =========================== 输出函数 ===========================
void Out(auto& map)
{
// 遍历 map 并打印 key/value
for(auto& [key,value] : map)
std::cout << '{' << key << ',' << value << "} ";
}
// =========================== main ===========================
int main()
{
// 每个对象或指针对象向 map 中插入数据
a.Get()[1]=2; // A: 每个对象独立 map
b.Get()[2]=3; // B: 类共享 map
c.Get()[3]=4; // C: inline static shared_ptr map
d.Get()[4]=5; // D: 函数内 static map,第一次调用初始化
e.Get()[5]=6; // E: 函数内 static shared_ptr map,第一次调用初始化
pA->Get()[6]=7; // pA: 堆上对象独立 map
pB->Get()[7]=8; // pB: 类共享 map,会与 b.Get() 共享同一个 map
pC->Get()[8]=9; // pC: inline static shared_ptr map,所有对象共享
pD->Get()[9]=10; // pD: 函数内 static map,所有对象共享
pE->Get()[10]=11;// pE: 函数内 static shared_ptr map,所有对象共享
// 输出各对象的 map 内容
std::cout << "\na: "; Out(a.Get()); // 只包含 a 的独立数据
std::cout << "\nPa: "; Out(pA->Get()); // 只包含 pA 的独立数据
std::cout << "\nb: "; Out(b.Get()); // 类共享 map,包含 b 和 pB 的数据
std::cout << "\npB: "; Out(pB->Get()); // 与 b.Get() 相同
std::cout << "\nc: "; Out(c.Get()); // inline static shared_ptr map,c/pC 共享
std::cout << "\npC: "; Out(pC->Get()); // 与 c.Get() 相同
std::cout << "\nd: "; Out(d.Get()); // 函数内 static map,d/pD 共享
std::cout << "\npD: "; Out(pD->Get()); // 与 d.Get() 相同
std::cout << "\ne: "; Out(e.Get()); // 函数内 static shared_ptr map,e/pE 共享
std::cout << "\nPe: "; Out(pE->Get()); // 与 e.Get() 相同
}
输出
a: {1,2}
Pa: {6,7}
b: {2,3} {7,8}
pB: {2,3} {7,8}
c: {3,4} {8,9}
pC: {3,4} {8,9}
d: {4,5} {9,10}
pD: {4,5} {9,10}
e: {5,6} {10,11}
Pe: {5,6} {10,11}
好的,我们来详细解析这段代码,重点是对象成员 vs 静态成员 vs 静态局部对象 vs shared_ptr 的区别,以及它们对数据共享和生命周期的影响。
1⃣ 类和对象分类
1. A:普通成员变量
struct A {
auto& Get() { return map; }
private:
std::map<int,int> map;
};
- 每个
A实例有独立的map。 - 对象间不共享数据。
- pA 与 a 的
map互不干扰。
2. B:类静态成员变量
struct B {
static auto& Get() { return map; }
private:
inline static std::map<int,int> map;
};
inline static确保 C++17 起类内初始化。- 所有 B 的实例共享同一个 map。
- 修改
b.Get()或pB->Get()都会影响同一个 map。 - 输出中:
b: {2,3} {7,8}
pB: {2,3} {7,8}
验证了 共享性。
3. C:类静态 shared_ptr
struct C {
static auto& Get() { return *map; }
private:
inline static auto map = std::make_shared<std::map<int,int>>();
};
map是shared_ptr,指向同一个堆上的std::map。- 访问
*map得到共享 map。 - 共享性和
B类似。 - 输出中:
c: {3,4} {8,9}
pC: {3,4} {8,9}
4. D:函数内静态局部变量
struct D {
static auto& Get() {
static std::map<int,int> map;
return map;
}
};
map是 函数内静态局部变量,在第一次调用Get()时构造。- 所有 D 的实例共享同一个 map。
- 输出:
d: {4,5} {9,10}
pD: {4,5} {9,10}
5. E:函数内静态局部 shared_ptr
struct E {
static auto& Get() {
static auto map = std::make_shared<std::map<int,int>>();
return *map;
}
};
- 与
D类似,但map是shared_ptr,指向堆上的 map。 - 所有 E 的实例共享同一个 map。
- 输出:
e: {5,6} {10,11}
Pe: {5,6} {10,11}
2⃣ 对象和指针实例总结
| 类型 | 存储位置 | 数据共享 | 输出解释 |
|---|---|---|---|
A |
每个对象各自 | 不共享 | a 和 pA 的 map 独立 |
B |
类静态成员 | 共享 | b 和 pB 的 map 相同 |
C |
类静态 shared_ptr | 共享 | c 和 pC 的 map 相同 |
D |
函数内静态局部 | 共享 | d 和 pD 的 map 相同 |
E |
函数内静态 shared_ptr | 共享 | e 和 pE 的 map 相同 |
3⃣ 内存和生命周期理解
- A 的 map:
- 每个对象在栈或堆上独立分配。
- 生命周期随对象结束。
- B 的静态成员 map:
- 存储在全局静态区。
- 生命周期贯穿整个程序。
- C 的静态 shared_ptr:
- 指针在静态区。
- 实际 map 在堆上。
- 生命周期贯穿整个程序,销毁时释放堆内存。
- D 的静态局部 map:
- 存储在静态区。
- 第一次访问时初始化(懒汉式)。
- E 的静态局部 shared_ptr:
- 指针存储在静态区,指向堆内存 map。
- 第一次访问时初始化。
- 生命周期贯穿整个程序。
4⃣ 数学化公式理解
- 对象独立性:
mapA(a)≠mapA(pA) \text{map}_A^{(a)} \ne \text{map}_A^{(pA)} mapA(a)=mapA(pA) - 静态共享性:
mapB(b)=mapB(pB),mapC(c)=mapC(pC),mapD(d)=mapD(pD),mapE(e)=mapE(pE) \text{map}_B^{(b)} = \text{map}_B^{(pB)}, \quad \text{map}_C^{(c)} = \text{map}_C^{(pC)}, \quad \text{map}_D^{(d)} = \text{map}_D^{(pD)}, \quad \text{map}_E^{(e)} = \text{map}_E^{(pE)} mapB(b)=mapB(pB),mapC(c)=mapC(pC),mapD(d)=mapD(pD),mapE(e)=mapE(pE) - 访问与修改公式:
Get()[k]=v ⟹ 所有共享实例访问同一个 map[k]=v \text{Get()}[k] = v \implies \text{所有共享实例访问同一个 map}[k] = v Get()[k]=v⟹所有共享实例访问同一个 map[k]=v - 生命周期公式:
{A.map 生命周期 = 对象生命周期B.map, C.map, D.map, E.map 生命周期 = 程序全局 \begin{cases} \text{A.map 生命周期 = 对象生命周期} \\ \text{B.map, C.map, D.map, E.map 生命周期 = 程序全局} \\ \end{cases} {A.map 生命周期 = 对象生命周期B.map, C.map, D.map, E.map 生命周期 = 程序全局
总结
- 普通成员:每个对象独立。
- 类静态成员 & 静态局部对象:所有对象共享。
shared_ptr静态变量:共享 map,在堆上分配。- 输出验证:共享对象的不同实例修改 map 都会相互影响,独立对象不会。
#include <iostream>
#include <memory>
using std::cout;
using std::make_shared;
// 假设 Print 是一个简单类,用于打印构造和析构信息
struct Print {
Print(const char* name) { cout << "ctor: " << name << "\n"; }
~Print() { cout << "dtor: " << name << "\n"; }
const char* name;
};
// =========================== 全局对象 ===========================
// 全局 int 对象,初始化时会调用 print,属于 pre-main
int a = Print("global int a");
// 解释:程序启动时,main 之前就会执行这里的构造函数
// 全局 auto 对象,初始化时会调用 Print 构造函数,pre-main
auto A = Print("global auto A");
// 解释:同样是全局对象,main 之前执行构造
// 全局 shared_ptr 对象,指向堆上 Print 对象,pre-main
auto shared = std::make_shared<Print>("global shared make_shared<Print>");
// 解释:shared_ptr 自身是全局对象,内部堆对象在构造时分配
// 全局 C 对象,构造函数在 pre-main 执行
C c;
// 解释:全局对象 c 的构造函数在 main 前执行
// 调用全局对象 c 的成员函数 Get2(),返回函数内静态对象
auto d = c.Get2();
// 解释:如果 Get2() 内有静态局部对象,它会在第一次调用时初始化
// 这个对象生命周期贯穿整个程序,析构发生在 post-main
// =========================== main 函数 ===========================
int main() {
cout << "main()\n";
// main 开始,执行 main 内局部对象
// main 内局部 shared_ptr 对象,指向堆上的 Print 对象
auto shared = std::make_shared<Print>("main local shared make_shared<Print>");
// 构造发生在此行,析构在 main 结束时
// 调用 c 的成员函数 Get3(),返回函数内静态对象
auto e = c.Get3();
// 构造在第一次调用时发生,析构在 post-main
// 这种函数内静态对象实现了“懒汉式初始化”
cout << "end main()\n";
// main 内局部对象(如 main 内 shared_ptr)析构在此行结束后
}
// =========================== 运行时行为总结 ===========================
// 构造顺序(pre-main + main 内)示例:
// 1. global int a
// 2. global auto A
// 3. global shared make_shared<Print>
// 4. C c
// 5. c.Get2() 内部静态对象(第一次访问时初始化)
// 6. main 内局部 shared_ptr
// 7. c.Get3() 内部静态对象(第一次访问时初始化)
// 析构顺序(post-main + main 内局部对象)遵循 LIFO 原则:
// 1. main 内 shared_ptr
// 2. c.Get3() 静态对象
// 3. c.Get2() 静态对象
// 4. C c
// 5. global shared make_shared<Print>
// 6. global auto A
// 7. global int a
1⃣ 全局对象和静态对象的初始化
C++ 中的对象可以分为几类:
- 全局对象(Global Objects):
- 在所有函数外部定义。
- 程序启动时初始化(
pre-main),main 之前执行。 - 程序结束时析构(
post-main)。
- 静态成员对象(Static Member):
- 类内
inline static或函数内static。 - 类内 static 成员:
- 如果是 inline static(C++17 起),在程序启动时初始化。
- 函数内 static 局部变量:
- 第一次访问时初始化(懒汉式)。
- 类内
- 局部对象(Local Objects):
- 只在函数调用时构造。
- 离开作用域析构。
2⃣ 从你的输出分析对象构造顺序
构造顺序(Pre-main 和 main 内)
ctor: inline static C::p1
func: global int a
ctor: global auto A
ctor: global shared make_shared<Print>
func: C::Get2()
ctor: C::Get2()
dtor: C::Get2()
ctor: inline static Get1() C::p2
main()
ctor: main local shared make_shared<Print>
func: C::Get3()
ctor: C::Get3()
dtor: C::Get3()
ctor: inline static C::Get2() C::p3
分析:
inline static C::p1:- 类内 inline static 成员
p1在程序启动时初始化(pre-main)。
- 类内 inline static 成员
global int a:- 全局变量
a初始化时会调用print("globalint a")。
- 全局变量
global auto A:- 全局
auto对象构造。
- 全局
global shared make_shared<Print>:- 全局 shared_ptr 的构造。
C::Get2():- 调用函数,函数返回的局部静态对象
C::Get2()初始化。
- 调用函数,函数返回的局部静态对象
- 局部静态对象
C::p2:- 这里
inline static Get1() C::p2,首次调用 Get1() 时初始化。
- 这里
- main():
- 到达 main,执行局部对象构造。
- main 局部 shared_ptr:
ctor: main local shared make_shared<Print>。
- C::Get3():
- 调用函数,返回局部对象临时构造,随后析构。
- 函数内静态对象
C::p3:- 懒汉式初始化,第一次调用 Get2() 或 Get3() 时构造。
析构顺序(Post-main)
dtor: inline static C::Get2() C::p3
dtor: main local shared make_shared<Print>
dtor: inline static C::Get2() C::p3
dtor: inline static Get1() C::p2
dtor: inline static Get1() C::p2
dtor: global shared make_shared<Print>
dtor: global auto A
dtor: inline static C::p1
解析:
- 局部静态对象在 main 结束后析构:
- 函数内静态对象
C::p3。
- 函数内静态对象
- main 局部对象析构:
main local shared make_shared<Print>。
- 重复析构:
- 如果对象多次引用或 static inline,析构顺序可能重复出现(由 inline static 实现细节导致)。
- 全局对象析构:
global shared make_shared<Print>。global auto A。inline static C::p1。
- 析构顺序 = 构造顺序的逆序:
- 即 LIFO(Last In First Out)原则:
dtor order=reverse(ctor order) \text{dtor order} = \text{reverse(ctor order)} dtor order=reverse(ctor order)
- 即 LIFO(Last In First Out)原则:
3⃣ 总结规律
- Pre-main(程序启动时):
- 全局对象
- 类内 inline static 成员
- 静态局部变量(在第一次调用前不会初始化)
- main 内:
- 局部对象
- 函数调用构造的临时对象
- Post-main(程序结束时):
- main 内局部对象析构
- 静态局部对象析构
- 类内 inline static 成员析构
- 全局对象析构
- 析构顺序 = 构造顺序的逆序
4⃣ 数学化理解
设:
- Ctor(X)\text{Ctor}(X)Ctor(X) = X 的构造函数被调用
- Dtor(X)\text{Dtor}(X)Dtor(X) = X 的析构函数被调用
- 顺序列表:
Ctor order=[C::p1,a,A,shared,C::Get2(),C::p2,mainshared,C::Get3(),C::p3] \text{Ctor order} = [C::p1, a, A, shared, C::Get2(), C::p2, main_shared, C::Get3(), C::p3] Ctor order=[C::p1,a,A,shared,C::Get2(),C::p2,mainshared,C::Get3(),C::p3]
则析构顺序:
Dtor order=reverse(Ctor order)=[C::p3,C::Get3(),mainshared,C::p2,C::Get2(),shared,A,a,C::p1] \text{Dtor order} = \text{reverse(Ctor order)} = [C::p3, C::Get3(), main_shared, C::p2, C::Get2(), shared, A, a, C::p1] Dtor order=reverse(Ctor order)=[C::p3,C::Get3(),mainshared,C::p2,C::Get2(),shared,A,a,C::p1]
LIFO 原则成立。
总结
- 全局对象和类静态成员在 main 之前初始化。
- 函数内静态局部变量在第一次调用时初始化(懒汉式)。
- 析构顺序与构造顺序严格逆序(LIFO)。
- main 内局部对象的生命周期仅在 main 内。
- shared_ptr 不同于对象自身,指向堆内存,析构时释放堆对象。
更多推荐



所有评论(0)