网页资讯视频图片知道文库贴吧地图采购
进入贴吧全吧搜索

 
 
 
日一二三四五六
       
       
       
       
       
       

签到排名:今日本吧第个签到,

本吧因你更精彩,明天继续来努力!

本吧签到人数:0

一键签到
成为超级会员,使用一键签到
一键签到
本月漏签0次!
0
成为超级会员,赠送8张补签卡
如何使用?
点击日历上漏签日期,即可进行补签。
连续签到:天  累计签到:天
0
超级会员单次开通12个月以上,赠送连续签到卡3张
使用连续签到卡
03月01日漏签0天
c语言吧 关注:801,792贴子:4,376,675
  • 看贴

  • 图片

  • 吧主推荐

  • 视频

  • 游戏

  • 首页 上一页 1 2 3 4 下一页 尾页
  • 59回复贴,共4页
  • ,跳到 页  
<<返回c语言吧
>0< 加载中...

回复:C语言递归调用转化为栈处理的一般式, 申请精华.

  • 取消只看楼主
  • 收藏

  • 回复
  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
LZ必须要重申, 因为许多人还在问LZ为什么要化解什么的.
LZ本文的真正价值倒不一定就是化解递归的一般式, 而是让各位C语言和数据结构的初学者深刻地理解C语言的递归. 因为C语言的递归和LZ要讲的东西之间关系太紧密太紧密, 以致于, 如果你不理解LZ所讲的东西, 你未必就能够理解C语言的递归. LZ是在阐述递归本身, 而不是在阐述一种化解方式.
其实LZ想要说的只是:你真的理解C语言的递归了么???真的么? 如果你有自信回答说是, 那么恭喜你, LZ这文章你不用看了. 可惜的是, 如果你说你理解了递归, 但是LZ让你把汉诺塔转换为"非递归形式"时, 你却不会转换, 那么LZ显然有理由认为你可以读读LZ这篇文章, 因为在LZ看来, 要把汉诺塔转换为"非递归形式"太简单了.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

首先我们要把递归的涵义限制为有终止的递归. 因为没有终止的递归是上帝处理的事情, 有终止的递归才是人类处理的事情.
人类可以理解没有终止的递归的概念;人类也能理解“无限”的概念;但是人类只能处理有终止的递归的情形;人类也只能处理有限的情形;因此,是否能够处理“有限和无限”的“所有情形”是划分上帝和人类的标志之一.


2026-03-01 21:19:47
广告
不感兴趣
开通SVIP免广告
  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

微积分党请勿乱入. 那个Limit符号只不过是个推知; 正如同并非所有的函数都收敛一样, 请绝对要相信全知全能的只有上帝, 而不是你.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

ok.
在阐述运行时刻环境之进程栈之前, LZ想要先提下, 什么是栈(Stack).
1. 栈的广义的, 唯一的涵义, 即, 其概念的内涵, 只有一个: LIFO, Last In First Out, 后进先出. 所有的能够称之为"栈"的东西, 都是这个概念内涵的外延, 或者说, 是这个抽象概念的实例.
2. 进程栈/线程栈正是栈的实例. 为什么将它们称之为栈, 是因为它们是栈的实例.
3. 我们要实现的ADT栈也是栈的实例. 为什么将这个数据结构称之为栈, 也是因为这个数据结构是栈的实例.
那么自然, 进程栈能够处理的事情, ADT栈一样可以处理.
所以LZ标题说的
“把递归化解为栈处理”
它的实际涵义是这样的:
1). 递归本来就是栈处理的. 这里的"栈"是广义的栈.
2). 所谓的化解是指, 因为进程栈的空间有限, 为了防止进程栈空间溢出而导致程序dump, 将其转化为ADT栈处理. 因为ADT栈一般在堆(Heap)上管理内存, 而堆相对来说"内存空间是够用的".


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
2.1 递归就是栈处理的
为了理解"递归本来就是栈处理"这一事实, 让我们回到递归的定义本身.
在递归的形式化定义中已经说过, 所有的(能够终止的)递归都必须:
1). 具有基准情形/简单情形,
2). 一组能够将所有情形规约/递推[reduce]至基准情形的规则.
也即, 一般来说, 需要通过递归来处理的事情, 我们的出发点不是“基础情形”, 而是"想要知道现在的情形", 但是"现在的情形"需要"根据规约规则回到基础情形, 然后再根据基础情形知道现在的情形".
如果我们称"情形"为case, 或者称之为状态state/status, 或者称之为情况situation, 我们称"现在想要知道的情形"为wanted case, 称"基础情形"为base case, 那么我们有:
figure 2.1
===============================================================
wanted case <<<<<<<<<<<<<<<<<<<<<<<<<<<<< stack base
--> middle case i - 1 <<<<<<<<<<<<<<< stack middle 1
--> middle case i - 2 <<<<<<<<<<<<< stack middle 2
--> ...
--> middle case 1 <<<<<<<<<<<<< stack middle i - 1
--> base case <<<<<<<<<<<<<<< stack top
===============================================================
见figure 2.1. 在C语言中, 你想要知道的情形/状态, 要么是一个变量值, 要么是几个变量值的组合[变量值即变量的状态, 很容易理解吧]. 没错, 这是基本的抽象; 至于你想要基于这个状态/变量值输出什么内容/执行什么动**怎么干怎么干对吧.
figure 2.1描述了, 想要知道wanted case[就是说, wanted case里面的变量值/状态现在是未知的, 在等待确定], 只能不断地向base case推进; 直到你知道了base case, 才能得知wanted case[现在, wanted case中的变量值确定下来] -- 请请请注意了, 你不是通过base case一下子就知道了wanted case的!过程是这样的:
1. 根据规约规则/递推规则不断往基础情形推进.
wanted case ==> middle case i -1 ==>...==>base case.
2. 根据base case, 往wanted case回溯.
wanted case <== ... <== middle case 1 <== base case.
好的, 让我们来看行进路径:
先从wanted case开始走, 然后我们走到了base case; 此时我们从base case往回走, 直到退回到wanted case. 最后的base case最先回退.
于是, 这和LIFO[Last In First Out, 后进先出]是完全契合的: 最后到达的最先返回/最后进入的最先出来.
在figure 2.1中, 如果我们把wanted case(就是几个变量是吧)放到一个小箱子中, 这个小箱子称之为stack base; 此时箱子中的变量的值/状态还不确定; 然后我们开始在这个箱子之上再放箱子, 这个箱子称之为stack middle 1, 用来把middle case i - 1的状态装进去... 如是直到我们将base case放到stack top的箱子中; 此时我们知道箱子中的值了; 由于我们的箱子是一个一个叠起来放的; 因此我们只能一次又一次地拿开箱子以确定下面的箱子中的变量的状态; 最后直到最底下的箱子状态确定, 于是我们知道了wanted case. 因此, 最低下的箱子被称之为栈底(stack base), 最顶上的箱子被称为栈顶(stack top).
如是, 我们非形式化地证明了一个结论; 也就是本小节的主题; 这个主题是:
递归就是栈处理的.
ok, 本小节完毕.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
2.1 递归就是栈处理的
为了理解"递归本来就是栈处理"这一事实, 让我们回到递归的定义本身.
在递归的形式化定义中已经说过, 所有的(能够终止的)递归都必须:
1). 具有基准情形/简单情形,
2). 一组能够将所有情形规约/递推[reduce]至基准情形的规则.
也即, 一般来说, 需要通过递归来处理的事情, 我们的出发点不是“基础情形”, 而是"想要知道现在的情形", 但是"现在的情形"需要"根据规约规则回到基础情形, 然后再根据基础情形知道现在的情形".
如果我们称"情形"为case, 或者称之为状态state/status, 或者称之为情况situation, 我们称"现在想要知道的情形"为wanted case, 称"基础情形"为base case, 那么我们有:
figure 2.1
===============================================================
wanted case <<<<<<<<<<<<<<<<<<<<<<<<<<<<< stack base
@@@@--> middle case i - 1 <<<<<<<<<<<<<<< stack middle 1
@@@@@@--> middle case i - 2 <<<<<<<<<<<<< stack middle 2
@@@@@@@@--> ...
@@@@@@@@@@--> middle case 1 <<<<<<<<<<<<< stack middle i - 1
@@@@@@@@@@@@--> base case <<<<<<<<<<<<<<< stack top
===============================================================
见figure 2.1. 在C语言中, 你想要知道的情形/状态, 要么是一个变量值, 要么是几个变量值的组合[变量值即变量的状态, 很容易理解吧]. 没错, 这是基本的抽象; 至于你想要基于这个状态/变量值输出什么内容/执行什么动**怎么干怎么干对吧.
figure 2.1描述了, 想要知道wanted case[就是说, wanted case里面的变量值/状态现在是未知的, 在等待确定], 只能不断地向base case推进; 直到你知道了base case, 才能得知wanted case[现在, wanted case中的变量值确定下来] -- 请请请注意了, 你不是通过base case一下子就知道了wanted case的!过程是这样的:
1. 根据规约规则/递推规则不断往基础情形推进.
wanted case ==> middle case i -1 ==>...==>base case.
2. 根据base case, 往wanted case回溯.
wanted case <== ... <== middle case 1 <== base case.
好的, 让我们来看行进路径:
先从wanted case开始走, 然后我们走到了base case; 此时我们从base case往回走, 直到退回到wanted case. 最后的base case最先回退.
于是, 这和LIFO[Last In First Out, 后进先出]是完全契合的: 最后到达的最先返回/最后进入的最先出来.
在figure 2.1中, 如果我们把wanted case(就是几个变量是吧)放到一个小箱子中, 这个小箱子称之为stack base; 此时箱子中的变量的值/状态还不确定; 然后我们开始在这个箱子之上再放箱子, 这个箱子称之为stack middle 1, 用来把middle case i - 1的状态装进去... 如是直到我们将base case放到stack top的箱子中; 此时我们知道箱子中的值了; 由于我们的箱子是一个一个叠起来放的; 因此我们只能一次又一次地拿开箱子以确定下面的箱子中的变量的状态; 最后直到最底下的箱子状态确定, 于是我们知道了wanted case. 因此, 最低下的箱子被称之为栈底(stack base), 最顶上的箱子被称为栈顶(stack top).
如是, 我们非形式化地证明了一个结论; 也就是本小节的主题; 这个主题是:
递归就是栈处理的.
ok, 本小节完毕.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

呃, 收工先... 呆会继续.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

2.2 运行时刻环境[Run Time Environment]
2.2.1 前言
LZ之前有两篇帖子, 一篇是《程序员的基本概念》, 一篇是《C语言函数调用的汇编级解释》,这两篇帖子都是理解C语言程序如何运行的一些基础, 都发在本贴吧之中, 找精品贴应该能找到. LZ现在简单地阐述一下.
计算机语言就是人类和计算机沟通的方式(The way to communicate with computer).
首先我们把语言分类. 目前的语言分类一般从两个角度去分, 一是该语言所遵从的范式, 二是该语言的解释/执行方式.
从语言的范式(Paradigm)上来说, 语言一般分为声明式的(Declarative Language), 函数式的(Functional Language)和命令式的(Imperative Language).
声明式的语言如Prolog(Programming in Logic). 只需要给出一个问题的"正确描述", 它就自动找到答案.
函数式的语言如erlang, haskell, clojure, 某种程度上说, 它们的主要宗派是Lisp[List Processing]. Lisp不太像是一门语言, 它没有"官方"的说法, 各种各样的实现都是Lisp.
命令式的语言. C/C++, Java, C#, Objective C等等等等. 所谓命令式, 在于"指定计算机指令执行的过程, 计算机就照此执行"的意思. C++是集多重范式于一身的语言, 命令式范式和纯函数式范式, 乱七八糟的各种范式只要程序员想要C++几乎都提供支持, 以至于LZ常常对C++的复杂性感到不可思议[换句话说C++是怪胎, 但偶尔解决某些问题时又特方便好用]. LZ最终在语言上的选择是回归简单, 由于LZ是C语言出身, 所以最终LZ手上的语言是C语言. 除C语言之外, Shell script, Makefile, Lisp scheme, sed awk LZ都略懂. C++/Java也是略懂[LZ有段时间工作时常看大段的Java(单个文件一不小心就上万行, 几十个文件的那种--当然LZ才不会傻到想去看完它们)].
语言的解释/执行方式分类某种程度上使语言有了一些"本质上的"差别.
从这个角度, 语言分为"编译, 然后执行的", 和"不需要编译, 由解释器解释执行的"语言.Java则是怪胎, 它是"编译, 然后解释执行"的混合物, 好听的说法是混血, 不好听的说法就是杂(^^)种. 另外这种差别也使得, "是否能够在运行期维护类型系统?" -- 这种差别导致了静态类型和动态类型的区分.
============> 分割线 <============
LZ现在废话好多... 难道是因为LZ退出开发生涯并从事了讲师这个职业的原因吗...
============> 分割线 <============
OK... 正题开始...
C语言是静态弱类型的编译型语言.
静态类型是说它的类型系统只在编译期存在. 弱类型是说它允许不一定(运行期)安全的隐式类型转换. 编译型语言是说它必须要先编译然后执行.
静态类型限制程序员太随意自由; 弱类型给予程序员自由; 编译型就是说它写出来的程序运行时非常快, 非常非常快, 目前没有比它更快的高级语言, C语言就是高级一点的汇编而已.而且, 如果汇编代码写得挫, 未必就比C语言快, 因为编译器的优化偶尔是很恐怖的. 汇编码的优势在于有时有C语言有无法操作到的东西, 比如CPU内部寄存器, 这一点一般通过C语言内嵌汇编码来解决.
============> 分割线 <============
既然是编译型语言, 那么自然需要编译了.
目前来说, 大多C语言实现, 都将编译C语言分为4步.
第一步是预处理. 就是处理掉宏, 还有包含头文件之类的事情.
第二步是编译, 这是真正的编译, 这一步把C语言代码转化为对应平台的汇编码. 这一步是由C语言编译器(也就是通常说的C语言实现)来做的.
第三步是汇编, 这一步将汇编代码转换为目标文件. 目标文件已经不是纯文本文件了; 目标文件是二进制文件. 在目标文件中, 所有的代码段以地址0开始, 所有的数据段以地址0开始.
第四步是链接, 这一步将各个目标文件中的代码段汇总, 将各个目标文件中的数据段汇总, 然后给予汇总的代码段和数据段以运行时起始地址, 并解决和动态库的符号链接问题, 如果是静态库则当作一般目标文件一样处理.
以上这四步通常统称编译期(Compiler Time). 因为这是一个C语言程序从源代码构建出来开始出生的过程, 因此通常就称之为编译或者构建了, 因为其中最主要的步骤是第二步也即编译[实际上每步都很重要].
============> 分割线 <============
在上面的4步需要用到的工具, 即, 预处理器, 编译器, 汇编器, 链接器, 统称为编译工具链[Compile Chain]. Linux系统上的GCC, 即GNU Compiler Collection, 亦可称之为GNU Compile Chain. 不过, 链接器因为目标文件/二进制文件处理的原因, 在其代码维护中被放到binutil[ Binary Utilities , 二进制工具]一类了.
其实...要说吧, 是否理解这四步中的工具到底做了什么, 还有一些二进制工具该怎么用, 其细节如何, 通常已经是区分一个C程序员是否经验老到的标志了. 一个经验老到的C程序员, 清楚他所写下每一个字节. 话题太长, 不便展开. 这些东西虽然基础, 但对C程序员来说实际上极为重要(通常它们也是C程序员理解计算机系统的极其重要的途径). 不过毕竟只是程序员的基本功底, 东西要称得上"计算机科学", 怎么都得和可计算理论和各种算法沾边, LZ本文描述的只是基础, 而不是"计算机科学".


2026-03-01 21:13:47
广告
不感兴趣
开通SVIP免广告
  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
2.2.2 运行时刻环境
当C语言程序被构建为一个可执行程序之后, 它还是一个文件 -- 虽然它是一个二进制的可执行文件. 它静静地躺在硬盘中, 等待程序员, 或者别的什么人去执行它.
假设这个程序叫做app. 当程序员在shell命令行上敲下:
./app
或者, 当某人在windows系统中, 用鼠标双击了某个文件...
于是, 此时程序开始运行.
程序开始运行, 此时, 就是程序运行期[Run Time]的开始.


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

============> 分割线 <============
程序在开始运行之前, 其实还有事情要做的.
这个要做的事情叫做加载(Load).
简单地说, 就是把文件从硬盘上挪到内存中.
现代操作系统一般借助于现代CPU的异常处理来完成加载步骤. 这个异常一般称之为缺页异常. 在Linux Kernel中, 做异常处理的函数名字大约是叫do_page_fault.
细节不用关注太多. 总之一句话, 程序需要加载才能运行. 如果此时有操作系统,那么完成加载的就是操作系统,通常操作系统就是加载器. 如果此时连操作系统都没有,那么程序员需要把这段二进制文件先做成一个磁盘镜像,或者做成一段放到FLASH中的东西,或者...不同的CPU有不同的指定。一般来说,任意CPU都会在起电后跳到某个指定物理地址去执行。把这段二进制代码放到那个指定地址去它就会运行。这些代码一般都被早前的程序员们写得很成熟了,它们一般被叫做BootLoader或者BIOS.
程序在加载之前, 该程序文件中已经有了分段/节(Section). 也即是说, 在二进制文件中, 所有的程序指令被分到一个Section, 所有的程序数据存储空间被分到一个Section, 文件中存储二进制程序指令的Section被称之为.text Section也即代码段, 文件中存储所有全局初始化数据/静态初始化数据的Section被称之为.data Section也即数据段.
当看C语言的基础书籍时, 里面提到的“静态存储区”,就是指二进制文件中的数据段。
很显然,静态存储区在加载后在内存中它也被称之为静态存储区。代码段被加载到内存中之后它仍然被称作代码段。
============> 分割线 <============
问题出现了。
文件中的静态存储区大小是有限的[意味著,被加载到内存中之后,它的大小也是固定不变的]。就是说,静态存储区在编译期已经规划好。
可是现在来到了运行期。C语言中有局部变量。函数中的局部变量在函数被调用时开始生存,在函数返回后,它被销毁。
还有。C语言中有指针,指针的基本用处表现在它能动态地获取内存区域,能malloc, 能free.
在程序加载开始运行时,这些问题必须要解决。
============> 分割线 <============
没错。前面提到了栈。栈即为C语言的运行时环境,用来完成存储函数中局部变量的事情。
堆则用来在运行时刻给C语言中的指针分配内存。


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

2.2.3 进程栈/线程栈
figure 2.2
===============================================================
@@ main <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< stack base
@@@@ --> func1 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< stack frame 1
@@@@@@ --> func2 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< stack frame 2
@@@@@@@@ --> ...
@@@@@@@@@@ --> func_i <<<<<<<<<<<<<<<<<<<<<<<<<< stack frame i
@@@@@@@@@@@@ --> func_return <<<<<<<<<<<<<<<<<<< stack top
===============================================================
见figure 2.2. 注意到这个图和figure2.1并没有本质的区别. LZ通过这个图来解释进程栈和函数调用链.
main, func1, func2...都是函数. 这些函数都有自己的局部变量. stack base, stack frame 1, stack frame 2...都是栈帧. 把函数中的局部变量理解成盒子. 把局部变量中的值/状态理解为盒子中装的东西. 把stack frame[栈帧]理解为箱子, 箱子用来装盒子.
我们假设,程序开始执行时从main函数开始[毫无疑问,这只是个假设]。
于是figure 2.2描述了一个函数调用链,这个函数调用链的执行路径是这样的:
1. 调用
main ==> func1 ==> func2 ==> ... ==> func_i ==> func_return
2. 返回
main <== func1 <== func2 <== ... <== func_i <== func_return
于是,这个路径和 LIFO 又是完全契合的。
如果每个函数都把它自己的所有局部变量(盒子)放到栈帧(箱子)中,
比如, main函数在调用func1之前,把自己所有的盒子先放到叫做stack_base的箱子中, 然后跳转到func1那去执行. func1在调用func2之前, 把自己的盒子先放到叫做stack_frame1的箱子中, 然后跳转到func2那去执行. 如是, 直到函数func_return被函数funci调用并开始执行. func_return也一样把自己的盒子放到叫做stack_top的箱子中.
func_return执行, 并可能改变stack_top箱子中的盒子中的内容. 然后它发现自己没事可干了, 因为它的代码执行完了,它该返回了. 它返回的意思就是说stack_top箱子中的内容没有用了 -- C语言函数不正是这样的吗[先用stack_top箱子, 装传入的参数/变量/盒子, 然后它可能改变传入的参数, 然后它执行完了, 该返回了, 于是它的箱子和盒子都没用了]. 既然箱子没有用了, 那么就可以把它销毁[至于返回值 -- 返回值是属于调用方的盒子, func_return只是负责把返回值计算出来, 至于调用方要不要, 或者拿去干什么, 它是不管的].
同理. func_i等到func_return执行完, func_i接下来执行调用func_return之后的代码. 然后func_i也执行完了. 于是func_i的箱子也没用了, 于是销毁掉.
于是:
1) 建立栈帧并调用
main ==> func1 ==> ...... ==> func_return
stack base ==> stack frame1 ==> ...... ==> stack_top
2) 销毁栈帧并返回
main <== func1 <== ...... <== func_return
stack base <== stack frame1 <== ...... <== stack_top
没错, 这就是函数调用链和栈的关系和总结. 它们的路径都是 LIFO 的.
箱子总要有个地方放对吧。没错。于是在程序开始运行前,操作系统给程序指定一个放箱子的起始地址,诺,你把箱子从那个地方开始叠。这个指定的地址就是栈底。然后函数开始执行,开始调用,开始叠箱子。每次正在执行的函数都只能访问自己的箱子,也就是栈顶的那个箱子。
如果没有操作系统给指定起始地址怎么办?这时候既然操作系统都没有,那就程序自己干。方法是嵌入一段汇编码。
进程栈/线程栈阐述得差不多了。之所以叫进程栈,是因为运行起来的程序一般不再称之为程序,而称之为进程。进程的栈自然就是进程栈了。
线程栈呢? -- 一个进程里面有多个线程怎么办呢?简单,一个函数调用链对应一个栈呗。既然这个函数调用链被称之为线程,那么这个函数调用链所对应的栈就叫线程栈。


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
擦...
终于...终于要开始讲递归了.
其实关于进程栈/线程栈中还有一些细节,... 嗯, 讲化解的时候会提,不码了。
累, @ 御坂美琴みさか
炮姐, 就叔叔前面码的这些字,也能给精华了吧...


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

终于要开始讲递归了...
关于栈还有一些实现细节... 算了,以后再说吧。码字好累。
......


  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼

算了。加码一节,免得后来解释麻烦。
2.3 关于进程栈的一些实现细节
在2.2.3 figure 2.2中,LZ给出了:
1) 建立栈帧并调用
main ========> func1 =========> ...... ==> func_return
stack base ==> stack frame1 ==> ...... ==> stack_top
2) 销毁栈帧并返回
main <======== func1 <========= ...... <== func_return
stack base <== stack frame1 <== ...... <== stack_top
这里有个实现细节要考虑。
童鞋们肯定会说:我确实写了一个C语言函数(func1)并让它调用另外一个C语言函数(func2)。可是我没有建立栈帧呀?我也没有销毁栈帧呀?那栈帧是怎么建立的,怎么销毁的?
C语言函数是不是要被编译器编译成汇编码啊?是的。
那编译器能不能在编译C语言函数的时候在函数开头部分和函数结束部分插上汇编代码啊?可以。
所以编译器会在函数开头插入建立栈帧的汇编代码;在函数返回前插入销毁栈帧的汇编代码。请参见LZ的《C语言函数调用的汇编级解释》。这里略提。
栈帧,也即箱子,直观地画就像下面这样子:
figure 2.3
|------------------|
|main 函数栈帧,====|
|即, main函数的箱子|
|------------------|
|------------------|
|func1函数栈帧=====|
|------------------|
|------------------|
|======*****=======|
|------------------|
|------------------|
|func_return函数===|
|栈帧==============|
|------------------|
这些箱子其实是连在一起的。下面画出最顶层的箱子。栈顶指针一直指向最顶层的箱子。每个箱子都大同小异,每个箱子中的细节是:
figure 2.4
|-----------------------|
|参数i==================|
|参数i - 1==============|
|参数...================|
|参数0==================|
|-----------------------|
|Program Counter[eip/rip on x86/x86_64]
|-----------------------|
|Stack Base Pointer[ebp/rbp on x86/x86_64]
|-----------------------| <====== Current Stack Base Pointer
|now, contianer,========|
|to save those auto var=|
|or status here=========|
|-----------------------| <====== Stack Top Pointer一直指向这里,
================================== esp/rsp on x86/x86_64
其中, Program Counter即程序计数器,是放的返回到调用方时要用的返回地址。参数和PC都是调用方放进来的,因为调用方才知道该传什么参数给被调用函数,调用方才知道调用完函数后该返回到哪里。Stack Base Pointer放的是调用方的栈基地址,Current Stack Base Pointer即当前栈帧的栈基指针,Stack Top Pointer一直是当前正在执行的函数的栈顶指针。
基本上,
[下面代码中,]
[rbp代表ebp/rbp on x86/x86_64] 栈基指针
[rsp代表esp/rsp on x86/x86_64] 栈顶指针
[pc代表eip/rip on x86/x86_64] 程序计数器,或者正在执行指令的下一条指令的地址
1. 调用方Caller调用函数的汇编代码是:
push 参数i
push 参数i - 1
push 参数...
push 参数0<========>此处设i个参数总共占用4i/8i个字节
call callee_addr [== push pc, jmp callee_addr]
add $4i/8i, %rsp
2. 被调用方Callee被调用函数的汇编代码是:
// 入口代码,entry code:
push %rbp
mov %rsp, %rbp
push %可能破坏的寄存器
sub $num, %rsp [num代表局部变量/盒子至少需要的空间, 或者更多]
===============[sub这一步就是为局部变量分配空间]
// 出口代码, exit/return/out code:
leave [== pop %可能破坏的寄存器, mov %rbp, %rsp, pop %rbp]
ret [== pop pc, jmp caller_addr]
========================================================
码完. 睡了...
========================================================


2026-03-01 21:07:47
广告
不感兴趣
开通SVIP免广告
  • elf0223
  • 强能力者
    7
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼
=========================> Chapter 3 <=========================
递归的形式
===============================================================


登录百度账号

扫二维码下载贴吧客户端

下载贴吧APP
看高清直播、视频!
  • 贴吧页面意见反馈
  • 违规贴吧举报反馈通道
  • 贴吧违规信息处理公示
  • 首页 上一页 1 2 3 4 下一页 尾页
  • 59回复贴,共4页
  • ,跳到 页  
<<返回c语言吧
分享到:
©2026 Baidu贴吧协议|隐私政策|吧主制度|意见反馈|网络谣言警示