Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N sp=> Locals ??? stack param argc,for main return addr (in startup) param *argv,for main local var 1, of main ... local var N, of main int main(int argc, char** argv) { int 11,12, 13; ... return Func1(i1, ...,3); } (prev bp) 0 mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param push param ... call Func1(..) add sp.sizeof(params) mov sp,bp poр bp ret main() main() param

1⃣ 总体布局

SVG 分为三列:

  1. Stack Frame [Base Pointer]
    左侧显示基指针 BP,用于表示函数栈帧的起点。
  2. Stack [Stack Pointer]
    中间显示实际栈内容(参数、局部变量、返回地址),栈从高地址向低地址增长。
  3. Instruction Pointer / Code History
    右侧显示程序执行指令和调用历史,标注 IP(指令指针)当前位置。

2⃣ 栈帧(BP)解析

BP 用于定位当前函数的栈帧:

bp =>
  • BP 保存调用者的 BP(prev bp)
  • 栈帧布局可以表示为:
    BPcurrent→prev BP \text{BP}_\text{current} \rightarrow \text{prev BP} BPcurrentprev 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)=BP4(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)} SPSPsizeof(data)
  • pop 操作:
    SP←SP+sizeof(data) SP \leftarrow SP + \text{sizeof(data)} SPSP+sizeof(data)

4⃣ 指令序列 / IP

右列显示 mainCRTStartup() 调用 main() 的指令:

push param
push param
call main(...)
ip =>
add sp, sizeof(params)
exit(ax)

解释:

  1. push param → 压入参数
  2. call main(...) → 压入返回地址并跳转
  3. main 栈帧构造:
push bp
mov bp, sp
sub sp, sizeof(locals)
  1. 函数返回后:
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=BP4(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 push4

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) locali=BPmain4(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 Func1SP减少,returnSP恢复

总结

这个 SVG 详细展示了:

  1. startup() → main() → Func1() 栈帧构造
  2. 参数、局部变量、返回地址在栈上的偏移
  3. SP / BP / IP 的动态变化规律
  4. x86 cdecl 栈操作流程

Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N Locals sp=> ??? stack param1,for Func1 . . . param argc,for main return addr (in startup) param *argv,for main local var 1, of main ... local var N, of main int main(int argc, char** argv) { int 11,12, 13; ... return Func1( i1, ...,3 ); } (prev bp) paramN,for Func1 0 mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param ... push param call Func1(..) add sp.sizeof(params) mov sp,bp poр bp ret main() main() param params

好的,我们来详细分析你提供的 SVG 图,它描述了一个 C/C++ 程序从 mainCRTStartupmain() 调用及函数调用栈 的结构,同时展示了 栈帧、栈指针、基指针和指令指针的变化。我会结合解释,并在必要时用数学公式表示栈帧和偏移。

1. 栈帧概念

在 x86/x64 调用约定下,每次函数调用都会在栈上生成一个 栈帧 (Stack Frame),包含:

  1. 参数 (parameters)
  2. 返回地址 (return address)
  3. 保存的基指针 (saved BP / prev bp)
  4. 局部变量 (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 指令压入+ 保存的 BPprev bp+ 局部变量sp-偏移
    在你的 SVG 中:
  • 左侧标注 Stack Frame[Base Pointer]
  • 中间标注 Stack[Stack Pointer]
  • 右侧标注 Instruction Pointer/Code History

2. mainCRTStartupmain 调用过程

栈帧布局 (图中中间区域)

从图中可以看到 mainCRTStartup(...) 的操作:

  1. 保存 BP
    push bp
    mov bp, sp
    
    将前一个函数的基指针压栈,并用 BP 保存当前 SP 位置。
  2. 分配局部变量
    sub sp, sizeof(locals)
    
    将 SP 向下移动,为局部变量开辟空间。
  3. 压入参数
    push param1
    push param2
    ...
    
    准备调用 main 函数。
  4. 调用 main
    call main(...)
    
    此时 CPU 会将返回地址压入栈中,IP 指向 main 函数。
  5. 函数返回后的恢复
    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) → 保存的 BP
  • param argc, *argvmain() 的参数
  • 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=bplocals_sizeparams_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=bp4j

4. 指令指针和代码历史

右侧的 Instruction Pointer/Code History 区域显示了:

  • ip => 当前指向的指令
  • 红色标注 push param / call Func1 → 表示当前执行的指令
  • 栈操作按顺序记录,便于回溯函数调用历史
    这就是 调试器中栈跟踪 (stack trace) 的逻辑

5. 总结

SVG 展示了一个典型的 C/C++ 函数调用和栈帧布局

  1. SP 指向栈顶,BP 保存基地址
  2. 栈向下增长,局部变量在 BP 下方,参数在 BP 上方
  3. 调用前 BP 和 SP 保存前一函数栈帧
  4. 调用函数时,参数压栈,返回地址压栈
  5. 函数返回时恢复 SP 和 BP,执行 ret 返回调用点
    访问公式总结
  • 参数:addrparam=bp+kaddr_\text{param} = bp + kaddrparam=bp+k
  • 局部变量:addrlocal=bp−kaddr_\text{local} = bp - kaddrlocal=bpk
  • 调用前 SP:spbefore=bpsp_\text{before} = bpspbefore=bp
  • 调用后 SP:spafter=bp−locals_size−params_sizesp_\text{after} = bp - \text{locals\_size} - \text{params\_size}spafter=bplocals_sizeparams_size
    这个结构非常适合理解 函数调用、栈帧布局、BP/SP/IP 关系,也是调试和逆向分析的基础。

Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N Locals sp=> ??? stack return addr (in main) param1,for Func1 . . . param argc,for main return addr (in startup) int Func1(int p1,int p2, int p3) { int i1, i2, i3; return Func2(i1,..., i3); } param *argv,for main local var 1, of main ... local var N, of main int main(int argc, char** argv) { int 11,12, 13; ... return Func1 (i1, ...,3); } (prev bp) paramN,for Func1 0 mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param ... push param call Func1(..) main() Func1() main() push bp mov bp, sp sub sp, sizeot(locals) ... push param ... push param call Func2(...) add sp, sizeof(params) mov sp, bp pop bp ret param params

1. 栈和栈帧的整体概念

在程序运行时,每个函数调用都会在栈(Stack)上分配一个 栈帧(Stack Frame),用来存储函数的:

  • 参数(Parameters)
  • 局部变量(Local Variables)
  • 返回地址(Return Address)
  • 上一个栈帧的基指针(Previous Base Pointer, BP)
    栈帧从上向下生长(在 x86 架构下),即 栈顶指针 SP(Stack Pointer)向低地址方向移动
  • 栈顶指针 spspsp 指向当前栈的顶部
  • 基指针 bpbpbp 指向当前函数的栈帧基址
    一个函数调用的典型过程如下:
  1. 保存上一个栈帧的 BPpush bp
  2. 设置新的 BPmov bp, sp
  3. 分配局部变量空间sub sp, sizeof(locals)
  4. 函数体执行
  5. 恢复 SP 和 BPmov sp, bp; pop bp
  6. 返回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 bpcurrentbpprev
    这就是所谓的 帧指针链(Frame Pointer Chain)

3. 程序调用流程

根据图中右侧的代码历史:

  1. mainCRTStartup:这是 C 运行时的入口,负责初始化 C 运行环境,并最终调用 main()
  2. main():程序的主函数,参数为 argcargv
  3. Func1(i1,...,i3):主函数调用的用户函数
  4. Func2(...)Func1 内部调用的另一个函数
    函数调用在栈上的操作:
  • push 参数:将函数参数压入栈中
    sp:=sp−sizeof(params) sp := sp - \text{sizeof(params)} sp:=spsizeof(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:=spsizeof(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)]( 0 开始)
    localj=[bp−j×size_of(var)] \text{local}_j = [bp - j \times \text{size\_of(var)}] localj=[bpj×size_of(var)]
    图中用矩形块标出每个局部变量和参数位置。

5. 可视化理解

SVG 图分为三大部分:

  1. 左侧 BP 栈帧:标记每个函数的 BP、局部变量、参数
  2. 中间 Stack:SP 指向的实际栈内容
  3. 右侧 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=bpsizeof(locals)
  • 局部变量访问:
    locali=[bp−offseti] local_i = [bp - offset_i] locali=[bpoffseti]
  • 参数访问:
    parami=[bp+offseti] param_i = [bp + offset_i] parami=[bp+offseti]
Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N Locals bp=> sp=> bp+N bp+... bp+8 ??? stack return addr (in main) param1,for Func1 . . . param argc,for main return addr (in startup) int Func1(int p1,int p2, int p3) { int i1, i2, i3; return Func2(i1,..., i3); } param *argv,for main local var 1, of main ... local var N, of main int main(int argc, char** argv) { int 11,12, 13; ... return Func1(i1, ...,3); } (prev bp) (prev bp) paramN,for Func1 0 mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param ... push param call Func1(..) main() Func1() main() push bp mov bp, sp sub sp, sizeot(locals) ... push param ... push param call Func2(...) add sp, sizeof(params) mov sp, bp pop bp ret param params Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N Locals bp=> sp=> bp+N bp+... bp+8 ??? stack return addr (in main) param1,for Func1 . . . param argc,for main return addr (in startup) int Func1(int p1,int p2, int p3) { int i1, i2, i3; return Func2(i1,..., i3); } param *argv,for main local var 1, of main ... local var N, of main local var 1, of func1 ... local var N, of func1 int main(int argc, char** argv) { int 11,12, 13; ... return Func1(i1, ...,3); } (prev bp) (prev bp) paramN,for Func1 0 mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param ... push param call Func1(..) main() Func1() main() Func1() push bp mov bp, sp sub sp, sizeot(locals) ... push param ... push param call Func2(...) add sp, sizeof(params) mov sp, bp pop bp ret bp-4 bp-.. bp-N param params Locals Stack Frame[Base Pointer] Stack[Stack Pointer] Instruction Pointer/Code History bp=> startup() bp=> bp+12 b+8 bp-4 bp-.. bp-N Locals bp=> bp+N bp+... bp+8 ??? stack return addr (in main) return addr (in fun1) param1,for Func1 param1,for Func2 ... . . . param argc,for main return addr (in startup) int Func1(int p1,int p2, int p3) { int i1, i2, i3; return Func2(i1,..., i3); } int Func2(int p1,int p2,int p3) { int i1,i2, i3; ... return ...; } param *argv,for main local var 1, of main ... local var N, of main local var 1, of func1 local var 1, of func2 ... local var N, of func1 local var N, of func2 ... int main(int argc, char** argv) { int 11,12, 13; ... return Func1(i1, ...,3); } (prev bp) (prev bp) (prev bp) paramN,for Func1 paramN,for Func2 0 push bp mov bp, sp sub sp, sizeof(locals) mov ax,... mov sp,bp pop bp ret mainCRTStartup(...) ... push param push param call main(...) ip=> add sp, sizeof(params) ... exit(ax) startup() push bp mov bp,sp sub sp,sizeof(locals) ... push param ... push param call Func1(..) main() Func1() Func2() main() Func1() bp=> sp=> bp+N bp+... bp+8 Func1() push bp mov bp, sp sub sp, sizeot(locals) ... push param ... push param call Func2(...) bp-4 bp-.. bp-N bp-4 bp-.. bp-N param params Locals

1⃣ 顶部文字说明

  • Stack Frame [Base Pointer] → 描述栈帧起点(BP)。
  • Stack [Stack Pointer] → 描述栈当前指针(SP)。
  • Instruction Pointer / Code History → 描述指令执行历史或调用链。

2⃣ 栈帧可视化

  • 栈向下增长(通常内存地址从高到低)。
  • 栈帧中包含:
    1. 函数参数param argc, param argv, param1 for Func1/Func2...)。
    2. 返回地址(Return Address)。
    3. 保存的前一个 BP(prev bp))。
    4. 局部变量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
  • 红色文本表示函数参数。
  • 黑色文本表示局部变量。
  • 绿色文本标记栈地址或初始值(如 0)。

3⃣ 栈帧箭头

  • bp => 表示当前函数栈帧的基指针。
  • 每个函数栈帧通过 (prev bp) 连接上一个函数的栈帧。

4⃣ 右侧“代码历史”

  • 描述栈操作对应的汇编指令:
    push bp
    mov bp, sp
    sub sp, sizeof(locals)
    mov ax,...
    mov sp, bp
    pop bp
    ret
    
    这些就是典型的函数进入和退出过程:
    1. 保存旧 BP。
    2. 设置新的 BP。
    3. 分配局部变量空间。
    4. 执行函数体。
    5. 恢复 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,跳转回调用者

步骤解释:

  1. 恢复 SP
    • sp = bp → 释放局部变量空间,栈指针回到保存 BP 的位置。
  2. 恢复 BP
    • pop bp → 栈顶现在是保存的前一个 BP(Func1 的 BP),恢复调用者的栈帧链。
  3. 跳转返回
    • 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     |
+-----------------+

返回步骤对应:

  1. 栈顶 SP 指向 Func2 局部变量。
  2. mov sp,bp → SP 指向保存的前一个 BP。
  3. pop bp → 恢复 Func1 的 BP。
  4. 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
+----------------+
| 局部变量       | <- 当前函数局部
+----------------+
低地址

返回机制公式化:

  1. 保存前 BP:
    BPold=BP BP_{old} = BP BPold=BP
  2. 设置新 BP:
    BP=SP BP = SP BP=SP
  3. 局部变量空间:
    SP=SP−locals_size SP = SP - \text{locals\_size} SP=SPlocals_size
  4. 返回时:
    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 BP0BP1BP2...0
  • 每个 BP 保存调用者的基指针,配合返回地址可以回溯整个调用链。

6⃣ 其他要点

  • First shall be last
    • 构造函数先构造基类,后构造子类;析构函数逆序。
  • Inside-Out / Outside-In
    • 构造:Base → Derived
    • 析构:Derived → Base
      总结:
  1. 继承 vs 聚合:继承内存连续,聚合对象独立。
  2. vptr 安置:Ctor/Dtor 开头设置,保证虚函数调用正确。
  3. 指针类型差异:普通指针 vs 成员指针、函数指针。
  4. nullptr:类型安全空指针。
  5. 栈帧与 BP:函数调用、返回机制、栈遍历。
  6. 构造/析构顺序: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⃣ 代码行为回顾

代码中有两种对象创建方式:

  1. 动态数组
Foo* pFoo = new Foo[5];
  • 使用 new[] 动态分配内存并调用构造函数。
  1. 自动数组(栈上)
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 N1: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=N1 down to 0:destruct(Foo[i])

其中 NNN 是数组长度。

  • 动态数组内存分配与栈数组内存分配只是 分配位置不同
    • 动态数组:堆内存
    • 栈数组:栈内存

6⃣ 总结理解

  1. 构造顺序:数组从低地址到高地址依次构造。
  2. 析构顺序:数组从高地址到低地址依次析构。
  3. new[]/delete[] 与栈数组
    • 本质相同:依次构造,逆序析构。
    • 不同点:分配位置(堆 vs 栈)和生命周期(手动 delete[] vs 自动作用域)。
  4. 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⃣ 代码涉及对象类型

代码里涉及几类对象:

  1. 全局静态对象
static Foo foo;
  • main() 之前初始化
  • 析构在 main() 之后自动调用
  1. 静态局部对象(函数内)
Foo& Bar() {
    static Foo foo;
    return foo;
}
  • 第一次调用 Bar() 时初始化(懒汉式
  • 程序结束时析构
  1. 类的静态成员对象
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/staticOstatic memberOglobal non-staticOstatic 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 localOglobal non-staticOstatic memberOglobal/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
  • 对象间不共享数据
  • pAamap 互不干扰。

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>>();
};
  • mapshared_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 类似,但 mapshared_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⃣ 内存和生命周期理解

  1. A 的 map
    • 每个对象在栈或堆上独立分配。
    • 生命周期随对象结束。
  2. B 的静态成员 map
    • 存储在全局静态区。
    • 生命周期贯穿整个程序。
  3. C 的静态 shared_ptr
    • 指针在静态区。
    • 实际 map 在堆上。
    • 生命周期贯穿整个程序,销毁时释放堆内存。
  4. D 的静态局部 map
    • 存储在静态区。
    • 第一次访问时初始化(懒汉式)。
  5. 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 生命周期 = 程序全局

总结

  1. 普通成员:每个对象独立。
  2. 类静态成员 & 静态局部对象:所有对象共享。
  3. shared_ptr 静态变量:共享 map,在堆上分配。
  4. 输出验证:共享对象的不同实例修改 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++ 中的对象可以分为几类:

  1. 全局对象(Global Objects)
    • 在所有函数外部定义。
    • 程序启动时初始化(pre-main),main 之前执行。
    • 程序结束时析构(post-main)。
  2. 静态成员对象(Static Member)
    • 类内 inline static 或函数内 static
    • 类内 static 成员
      • 如果是 inline static(C++17 起),在程序启动时初始化。
    • 函数内 static 局部变量
      • 第一次访问时初始化(懒汉式)。
  3. 局部对象(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

分析:

  1. inline static C::p1
    • 类内 inline static 成员 p1 在程序启动时初始化(pre-main)。
  2. global int a
    • 全局变量 a 初始化时会调用 print("globalint a")
  3. global auto A
    • 全局 auto 对象构造。
  4. global shared make_shared<Print>
    • 全局 shared_ptr 的构造。
  5. C::Get2()
    • 调用函数,函数返回的局部静态对象 C::Get2() 初始化。
  6. 局部静态对象 C::p2
    • 这里 inline static Get1() C::p2,首次调用 Get1() 时初始化。
  7. main()
    • 到达 main,执行局部对象构造。
  8. main 局部 shared_ptr
    • ctor: main local shared make_shared<Print>
  9. C::Get3()
    • 调用函数,返回局部对象临时构造,随后析构。
  10. 函数内静态对象 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

解析:

  1. 局部静态对象在 main 结束后析构
    • 函数内静态对象 C::p3
  2. main 局部对象析构
    • main local shared make_shared<Print>
  3. 重复析构
    • 如果对象多次引用或 static inline,析构顺序可能重复出现(由 inline static 实现细节导致)。
  4. 全局对象析构
    • global shared make_shared<Print>
    • global auto A
    • inline static C::p1
  5. 析构顺序 = 构造顺序的逆序
    • LIFO(Last In First Out)原则
      dtor order=reverse(ctor order) \text{dtor order} = \text{reverse(ctor order)} dtor order=reverse(ctor order)

3⃣ 总结规律

  1. Pre-main(程序启动时)
    • 全局对象
    • 类内 inline static 成员
    • 静态局部变量(在第一次调用前不会初始化)
  2. main 内
    • 局部对象
    • 函数调用构造的临时对象
  3. 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 原则成立。

总结

  1. 全局对象和类静态成员在 main 之前初始化。
  2. 函数内静态局部变量在第一次调用时初始化(懒汉式)。
  3. 析构顺序与构造顺序严格逆序(LIFO)。
  4. main 内局部对象的生命周期仅在 main 内。
  5. shared_ptr 不同于对象自身,指向堆内存,析构时释放堆对象。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐