函数调用与堆栈
堆栈的定义及应用

堆栈的定义及应用堆栈(Stack)是一种数据结构,它按照后进先出(LIFO)的原则存储数据。
也就是说,最后存入堆栈的数据元素最先被取出,而最先存入的数据元素最后被取出。
堆栈中包含两个主要操作:压栈(Push)和弹栈(Pop)。
压栈是指将数据元素存入堆栈,弹栈是指从堆栈中取出数据元素。
除此之外,还有一个查看栈顶元素的操作。
堆栈的实际应用非常广泛,以下列举几个常见的应用场景:1. 函数调用与递归:在程序中,每当一个函数被调用,系统将会为这个函数分配一段内存空间,这段内存空间就被称为函数的栈帧。
当函数执行完毕后,栈帧会被销毁。
函数调用过程中,每次调用都会将返回地址和相关参数等信息压入栈中,在函数执行完毕后再将这些信息弹出。
递归函数的实现也离不开堆栈,每次递归调用都会生成一个新的栈帧,直到递归结束后才开始回溯弹栈。
2. 表达式求值:在编程语言中,堆栈可以用于实现算术表达式求值。
例如,中缀表达式需要通过堆栈进行转换成后缀表达式来简化计算过程,然后再通过堆栈进行后缀表达式的计算。
在进行表达式求值时,通过堆栈可以保存运算符和操作数的顺序,确保运算的优先级正确。
3. 括号匹配:在编程或者数学等领域,括号匹配是一个常见的问题。
我们可以使用堆栈来判断一个表达式中的括号是否匹配。
遍历表达式,每当遇到左括号时,将其压入堆栈。
当遇到右括号时,从堆栈中弹出一个左括号,若左右括号匹配,则继续遍历。
若右括号没有对应的左括号或者堆栈为空,则括号不匹配。
4. 浏览器的历史记录:在浏览器中,通过点击链接或者前进后退按钮,我们可以在不同的网页之间进行切换。
这种网页切换也可以使用堆栈来实现浏览历史记录的功能。
每当访问一个新网页时,将其URL压入堆栈顶部;当点击前进按钮时,从堆栈中弹出一个URL;当点击后退按钮时,将当前页面的URL压入堆栈,然后再弹出上一个URL。
5. 撤销与恢复:在许多软件中,都提供了撤销与恢复功能。
当用户对文档进行操作时,软件会将操作信息(如添加、删除、修改等)压入堆栈中,当用户点击撤销时,软件会从堆栈中弹出最近的操作信息并进行撤销操作;当用户点击恢复时,软件会从堆栈中弹出已经撤销的操作信息并进行恢复。
调用函数的压堆栈方式

调用函数的压堆栈方式
在计算机编程中,当一个函数被调用时,会发生压栈操作。
这
是因为计算机需要保存当前函数的执行状态,以便在函数执行完毕
后能够回到调用该函数的地方继续执行。
下面我将从多个角度来解
释函数的压栈方式。
1. 参数传递,在调用函数时,参数会被压入栈中。
这样函数就
可以在栈中找到这些参数并使用它们。
2. 返回地址,在调用函数时,调用方的返回地址会被压入栈中。
这样函数执行完毕后可以通过返回地址回到调用方。
3. 保存旧的栈帧指针,在函数调用时,当前函数的栈帧指针会
被压入栈中,以便在函数执行完毕后能够回到调用方的栈帧。
4. 保存局部变量,在函数调用时,当前函数的局部变量会被压
入栈中,以便在函数执行期间可以使用这些局部变量。
5. 保存寄存器状态,在函数调用时,一些寄存器的状态会被保
存到栈中,以便函数执行期间可以使用这些寄存器。
总的来说,函数的压栈方式是为了保存当前函数的执行状态,以便在函数执行完毕后能够回到调用方继续执行。
这种方式是计算机实现函数调用和返回的基础,也是程序执行的重要机制之一。
希望这些解释能够帮助你理解函数的压栈方式。
堆栈的作用

堆栈的作用堆栈(Data Stack)是一种常见的数据结构,它按照“先进后出”的原则存储和访问数据。
它的作用广泛应用于编程语言、操作系统、数据库等领域,在计算机科学中有着重要的作用。
堆栈有许多实际应用。
例如,在编程语言中,堆栈常用于函数的调用和返回,以及变量的存储和访问。
当一个函数被调用时,它的局部变量和返回地址会被存储在堆栈中;当函数执行完毕后,这些数据会从堆栈中弹出,控制权返回到调用函数。
这种方式可以实现函数的嵌套调用和递归调用,使得编程变得更加灵活和高效。
在操作系统中,堆栈被用于保存进程的上下文信息。
当一个进程被中断或切换时,当前的执行状态、程序计数器和寄存器等数据会被保存在堆栈中,以便于恢复和切换进程。
这使得操作系统可以高效地管理并调度各个进程,提高计算机系统的整体性能。
堆栈还被广泛应用于数据库系统中,主要用于实现事务的管理和查询语言的解析。
在事务管理中,堆栈可以记录事务的执行过程和状态变化,保证事务在异常情况下的一致性和可靠性。
在查询语言解析中,堆栈可以将复杂的查询语句转化为逆波兰表达式,从而简化查询的处理和计算。
除了上述应用领域外,堆栈还可以用于解决其他一些具体的计算问题。
例如,递归算法中常用堆栈来存储和管理函数的调用栈;图算法中可以使用堆栈来实现深度优先搜索;表达式求值中可以使用堆栈来实现后缀表达式的计算。
堆栈的灵活性和高效性使得它在计算机科学领域中发挥着重要的作用。
在实际应用中,堆栈的空间和时间复杂度一般为O(n),其中n 为存储数据的数量。
这使得堆栈具有较好的性能和可扩展性,在处理大规模数据和复杂计算时仍能保持高效运行。
同时,堆栈的实现相对简单,大多数编程语言都提供了堆栈的内置支持,使得开发人员可以方便地使用和操作堆栈。
总的来说,堆栈作为一种重要的数据结构,在计算机科学领域有着广泛的应用。
它在编程语言、操作系统、数据库等领域发挥着重要作用,并且具有灵活、高效和易于实现的特点。
了解和掌握堆栈的原理和应用,对于提高编程能力和解决实际问题具有重要意义。
堆栈技术的原理和应用

堆栈技术的原理和应用什么是堆栈技术堆栈(Stack)是一种基于后入先出(Last-In-First-Out,LIFO)的数据结构,它可以用来存储和管理数据。
堆栈技术在计算机科学领域被广泛应用,包括操作系统、编程语言和网络等方面。
堆栈技术的原理在堆栈技术中,数据是按照先进后出的顺序被存储和检索的。
堆栈有两个基本操作:入栈(Push)和出栈(Pop)。
•入栈(Push)操作将数据放入堆栈的顶部,也就是最后一个元素的上方。
此时,数据成为新的堆栈顶部。
•出栈(Pop)操作将堆栈顶部的数据移除,并返回该数据。
此时,堆栈顶部被更新为上一个元素。
堆栈操作可以用指针或索引来实现。
当指针指向堆栈的顶部时,可以通过修改指针的位置来执行入栈和出栈操作。
堆栈技术的应用堆栈技术在计算机科学中有多种应用,下面列举了几个常见的应用场景。
1.函数调用:堆栈被用于保存函数调用的上下文信息。
每当一个函数被调用,相关的参数和返回地址等信息都会被压入堆栈。
当函数调用结束后,这些信息会被弹出堆栈,返回到调用点。
2.表达式求值:堆栈可以用于求解数学表达式,包括中缀表达式和后缀表达式。
在中缀表达式求值过程中,运算符和操作数会被依次压入堆栈,直到出现优先级更高的运算符或遇到右括号。
而在后缀表达式求值过程中,每当遇到一个操作数,都可以通过堆栈来存储和管理。
3.内存管理:堆栈技术在内存管理中起到重要的作用。
每当一个函数被调用,其本地变量、临时变量和返回值等数据会被存储在堆栈中。
这样可以方便地分配和释放内存空间,同时确保函数调用的独立性。
4.操作系统:堆栈技术在操作系统中被广泛应用,用于管理程序的执行和系统资源的调度。
操作系统会使用堆栈来维护进程的执行状态,包括程序计数器、寄存器和其他上下文信息。
5.编程语言:许多编程语言都支持堆栈数据结构,例如C语言中的函数调用堆栈、Java语言中的方法调用堆栈和Python语言中的运行时堆栈。
这些堆栈可以用于管理函数调用、异常处理和递归等操作。
C语言函数调用规定

在C语言中,假设我们有这样的一个函数:int function(int a,int b)调用时只要用result = function(1,2)这样的方式就可以使用这个函数。
但是,当高级语言被编译成计算机可以识别的机器码时,有一个问题就凸现出来:在CPU中,计算机没有办法知道一个函数调用需要多少个、什么样的参数,也没有硬件可以保存这些参数。
也就是说,计算机不知道怎么给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调。
为此,计算机提供了一种被称为栈的数据结构来支持参数传递。
栈是一种先进后出的数据结构,栈有一个存储区、一个栈顶指针。
栈顶指针指向堆栈中第一个可用的数据项(被称为栈顶)。
用户可以在栈顶上方向栈中加入数据,这个操作被称为压栈(Push),压栈以后,栈顶自动变成新加入数据项的位置,栈顶指针也随之修改。
用户也可以从堆栈中取走栈顶,称为弹出栈(pop),弹出栈后,栈顶下的一个元素变成栈顶,栈顶指针随之修改。
函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。
函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。
在参数传递中,有两个很重要的问题必须得到明确说明:当参数个数多于一个时,按照什么顺序把参数压入堆栈函数调用后,由谁来把堆栈恢复原装在高级语言中,通过函数调用约定来说明这两个问题。
常见的调用约定有:stdcallcdeclfastcallthiscallnaked callstdcall调用约定stdcall很多时候被称为pascal调用约定,因为pascal是早期很常见的一种教学用计算机程序设计语言,其语法严谨,使用的函数调用约定就是stdcall.在Microsoft C++系列的C/C++编译器中,常常用PASCAL宏来声明这个调用约定,类似的宏还有WINAPI和CALLBACK.stdcall调用约定声明的语法为(以前文的那个函数为例):int __stdcall function(int a,int b)stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸以上述这个函数为例,参数b首先被压栈,然后是参数a,函数调用function(1,2)调用处翻译成汇编语言将变成:push 2 第二个参数入栈push 1 第一个参数入栈call function 调用参数,注意此时自动把cs:eip入栈而对于函数自身,则可以翻译为:push ebp 保存ebp寄存器,该寄存器将用来保存堆栈的栈顶指针,可以在函数退出时恢复mov ebp,esp 保存堆栈指针mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向aadd eax,[ebp + 0CH] 堆栈中ebp + 12处保存了bmov esp,ebp 恢复esppop ebp ret 8而在编译时,这个函数的名字被翻译成_function@8注意不同编译器会插入自己的汇编代码以提供编译的通用性,但是大体代码如此。
Uboot系统初始化为何要初始化堆栈?为何C语言的函数调用要用到堆栈,而汇编却不需要初

Uboot系统初始化为何要初始化堆栈?为何C语言的函数调用要用到堆栈,而汇编却不需要初之前看了无数关于uboot的分析,其中就有说要为的运行,预备好堆栈。
而自己在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。
但是,从来只是看到有人说系统初始化要初始化堆栈,即正确给堆栈指针sp赋值,但是却从来没有看到有人说明,为何要初始化堆栈。
所以,接下来的内容,就是经过一定的探索,试图来说明一下,为何要初始化堆栈,即:为何C语言的函数调用要用到堆栈,而汇编却不需要初始化堆栈。
要明了这个问题,首先要了解堆栈的作用。
关于堆栈的作用,要具体讲解的话,要很长的篇幅,所以此处只是做简略介绍。
总的来说,堆栈的作用就是:保存现场/上下文,传递参数。
1.保存现场/上下文现场,意思就相当于案发觉场,总有一些现场的状况,要记录下来的,否则被别人破坏掉之后,你就无法复原现场了。
而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如r0,r1等等,对于这些寄存器的值,假如你不保存而挺直跳转到子函数中去执行,那么很可能就被其破坏了,由于其函数执行也要用到这些寄存器。
因此,在函数调用之前,应当将这些寄存器等现场,临时保持起来,等调用函数执行完毕返回后,再复原现场。
这样CPU就可以正确的继续执行了。
在计算机中,你常可以看到上下文这个词,对应的英文是context。
那么:1.1.什么叫做上下文context保存现场,也叫保存上下文。
上下文,英文叫做context,就是上面的文章,和下面的文章,即与你此刻,当前CPU运行有关系的内容,即那些你用到寄存器。
所以,第1页共5页。
double函数调用堆栈过程

double函数调用堆栈过程一、概述函数调用堆栈是编程中一个重要的概念,它用于存储函数调用的信息。
当一个函数被调用时,其参数、局部变量和返回地址等信息会被压入堆栈;当函数执行完毕返回时,这些信息会从堆栈中弹出。
double函数调用同样遵循这样的过程,它涉及到两个函数间的相互调用和参数传递。
1. 函数调用:当执行到double函数调用时,首先将当前函数的返回地址压入堆栈的顶部。
这是为了在后续返回调用函数时能够正确返回调用函数的返回值。
2. 参数传递:接下来,double函数会将需要传递给它的参数压入堆栈。
这些参数通常是从调用double函数的函数中传递过来的。
3. 局部变量:在堆栈中,double函数还会保存其自身的局部变量。
这些变量在double函数执行期间有效,当函数执行完毕后,这些变量会被清除。
4. 执行double函数:当double函数开始执行时,它会使用堆栈中的参数和局部变量进行运算或处理。
5. 返回调用函数:当double函数执行完毕后,它会将返回地址从堆栈中弹出,并跳转到这个地址处继续执行后续代码。
6. 清理堆栈:最后,当double函数返回后,其占用的堆栈空间会被释放,以便于下一个函数的调用。
三、注意事项1. 确保堆栈空间足够:在调用double函数之前,需要确保堆栈空间足够,以存储返回地址、参数和局部变量等信息。
2. 避免堆栈溢出:在处理大量数据或递归调用时,要特别注意堆栈溢出的问题。
可以使用适当的数据结构或算法来避免过大的数据占用过多的堆栈空间。
3. 正确处理返回值:在调用double函数时,需要确保返回地址能够正确返回调用函数的返回值。
如果返回地址被覆盖或错误处理,可能会导致程序错误或异常。
4. 合理使用局部变量:在double函数中使用的局部变量应当根据实际需求进行合理设置和分配。
过多的局部变量可能导致堆栈溢出或影响程序的性能。
5. 调试和错误处理:在编写代码时,需要对可能出现的错误和异常进行充分考虑和测试。
ARMC函数调用堆栈入栈顺序

ARMC函数调⽤堆栈⼊栈顺序ARM C函数调⽤堆栈⼊栈顺序堆栈指针是在函数⼀开头就确认了的,⽐如如下的xxx_func.cfi函数,它在函数的开头就将sp⾃减了0x170,这个0x170是xxx_fun.cfi函数局部变量total size + 需要⼊栈的reg total size然后会设置x29(fp,栈底指针),这⾥看到是sp - 0x110,可以看到需要⼊栈的reg total size为0x60,所以fp指向了函数局部变量列表的头部,它是不包含函数⾥的局部变量的00000000000441c8 <xxx_func.cfi>:441c8: d105c3ff sub sp, sp, #0x170441cc: a9117bfd stp x29, x30, [sp,#272]441d0: a9126ffc stp x28, x27, [sp,#288]441d4: a91367fa stp x26, x25, [sp,#304]441d8: a9145ff8 stp x24, x23, [sp,#320]441dc: a91557f6 stp x22, x21, [sp,#336]441e0: a9164ff4 stp x20, x19, [sp,#352]441e4: 910443fd add x29, sp, #0x110441e8: 90000008 adrp x8, 0 <__stack_chk_guard>当函数返回时,从stack弹出值到之前保护reg⽽⼊栈的reg⾥,这⾥可以看到出栈时顺序恰好和⼊栈时相反,即是后⼊先出,将fp、lr从stack 弹出到fp、lr即实现返回:447f4: a9564ff4 ldp x20, x19, [sp,#352]447f8: a95557f6 ldp x22, x21, [sp,#336]447fc: a9545ff8 ldp x24, x23, [sp,#320]44800: a95367fa ldp x26, x25, [sp,#304]44804: a9526ffc ldp x28, x27, [sp,#288]44808: a9517bfd ldp x29, x30, [sp,#272]4480c: 9105c3ff add sp, sp, #0x17044810: d65f03c0 ret函数调⽤stack变化,⼊栈顺序以main()⾥有int a、int b两个局部变量并call了⼀个test_func(int i, int j),在test_func()⾥有define⼀个int c变量为例,来看下调⽤test_func()时的堆栈变化:调⽤test_func时,test_func参数i、j先反序⼊栈,然后是给test_func的返回值预留空间,然后是test_func的返回地址;接下来是在test_func函数⾥将需要保存的reg⼊栈,其中x29、x30是必须要⼊栈的,此时会将fp指向当前的堆栈顶部,这⾥x29、x30是最后才⼊栈的;然后再是为test_func中的局部变量开辟堆栈空间,此时将sp指向此时的堆栈顶部:|--------------------||..... ||--------------------||int a ||--------------------||int b ||--------------------||int j ||--------------------||int i ||--------------------||test_func返回值预留 ||--------------------||test_func返回地址 ||--------------------|fp-->|x29, x30 | /*如果test_func还需要保护其它reg,将其它reg也⼊栈*/|--------------------|sp-->|int c ||--------------------|int test_func(int i, int j){int c = i + j;return c;}int main(){int a = 3;int b =4;test_func(3, 4);return0;}所以fp指针是指向的当前函数的stack底部、sp指向的是stack顶部,在fp和sp之间即是test_func的所有局部变量区间,如果test_func没有⼀个局部变量,fp、sp将指向同⼀个位置,即都指向当前函数堆栈顶部。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
mov operand, [ESP] ; add ESP, 1 ; push EIP ; jmp Label ; push EIP ; jmp operand; pop EIP ;
关键字extern的作用
错了! 错了!
系统栈与过程调用
栈顶
…. 局部变量 var_B2 局部变量 var_B1 其他信息 返回地址 参数 arg_B1=4 参数 arg_B2=3 局部变量 var_A2 局部变量 var_A1 其他信息 返回地址 参数arg_A1= 2 参数 参数 arg_A2 = 1 局部变量 var_main
变量的生存期
把程序运行时一个变量占有内存空间的时间段称为该变量的 生存期。C++把变量的生存期分为:静态 自动 动态 静态、自动 动态三种。 生存期 静态 自动和动态 静态生存期:全局变量都具有静态生存期,它们的内存空间从 静态生存期 程序开始执行时就进行分配,直到程序结束才被收回。 自动生存期:局部变量和函数形参一般都具有自动生存期,它 自动生存期 们的内存空间在程序执行到定义它们的复合语句(包括函数体) 时才分配,当定义它们的复合语句执行结束时内存被收回。 动态生存期:具有动态生存期的变量的生存时间是由程序员自 动态生存期 由控制的,其内存空间用new操作符分配,用delete回收。 在定义局部变量时,可以为它们加上存储类修饰符auto、static 和register来指出它们的生存期。 定义为static存储类型的局部变量具有静态生存期,它们也被存 放在静态数据区。
全排列的递归程序展开
• • • • • • • • • • • • • • • • • • #include <stdio.h> int n,x[100]; void output(int x[]) { int i; for (i=1;i<=n;i++) printf("%d ",x[i]); printf("\n"); } void bc(int t) { int i,p; if (t>n) output(x); else{ for (i=t;i<=n;i++) { p=x[t]; x[t]=x[i]; x[i]=p; bc(t+1); p=x[t]; x[t]=x[i]; x[i]=p; } } }
• int main() • { • int i; • scanf("%d",&n); • for(i=1;i<=n;i++) • x[i]=i; • bc(1); • return 0; • }
• • • • • • • • • • • • • • • • • • • • • •
i=1; i=2; I=3; 交换x[1],x[1];123 交换 交换x[1],x[2];213; 交换x[1],x[3];321; 交换 交换 bc (2); bc (2); bc (2); for(i=2;i<=3;i++){ for(i=2;i<=3;i++){ for(i=2;i<=3;i++){ i=2; i=2; i=2; 交换x[2],x[2];123; 交换 ; 交换x[2],x[2];213; 交换x[2],x[2];321; 交换 ; 交换 ; bc (3); bc (3); bc (3); for(i=3;i<=3;i++){ for(i=3;i<=3;i++){ for(i=3;i<=3;i++){ i=3; i=3; i=3; 交换x[3],x[3];123; 交换 ; 交换x[3],x[3];213; 交换x[3],x[3];321; 交换 ; 交换 ; bc (4); bc (4); bc (4); 输出“ 输出“123”; 输出“ 输出“ 输出“213”; 输出“321”; 交换x[3],x[3];123; 交换 交换x[3],x[3];213; 交换x[3],x[3];321; 交换 交换 } } } 交换x[2],x[2];123; 交换 交换x[2],x[2];213; 交换x[2],x[2];321; 交换 交换 i=3; i=3; i=3; 交换x[2]=x[3];132; 交换 交换x[2]=x[3];231; 交换x[2]=x[3];312; 交换 交换 bc(4); bc(4); bc(4); 输出“ 输出“132”; ; 输出“ 输出“ 输出“231”; ; 输出“312”; ; 交换x[2],x[3];123; 交换 交换x[2],x[3];213; 交换x[2],x[3];321; 交换 交换 } } } 交换x[1],x[1];123; 交换 交换x[1],x[2];123; 交换x[1],x[3];123; 交换 交换 }
• • • • • • • 2 • • • • 3 •
1
2
3
3
1
3Байду номын сангаас
1
2
2
3
1
2
1
保存上层函数的 栈帧EBP 栈帧 为局部变量分配空间
回收局部变量占 用的空间
参数y 参数 = 5 参数x 参数 = g 返回地址
EBP
地址高端
上层函数的栈帧EBP 上层函数的栈帧 局部变量a 局部变量 局部变量b 局部变量
注:图中每个格都表示4个字节 图中每个格都表示 个字节 地址低端
ESP
一个小结论: 一个小结论: 函数的参数都在EBP所指示的内存地址的正偏移处,函数内部 所指示的内存地址的正偏移处, 函数的参数都在 所指示的内存地址的正偏移处 的局部变量都在EBP所指示的内存地址的负偏移处。 所指示的内存地址的负偏移处。 的局部变量都在 所指示的内存地址的负偏移处
程序结构与堆栈
C/C++程序运行时的内存结构
全局变量、用static修饰的局部变量都存储在静态数据区。 程序指令和大部分字面常量都存储在代码区。 大部分函数的形参和局部变量都存储在栈区。 程序中动态分配的内存都存储在堆区。 一小部分函数形参和局部变量存储在CPU寄存器组中。 已初始化区 静态数据区 常量数据区 代码区 栈区 堆区 CPU寄存器组 未初始化区
func_B 的栈帧
func_A 的栈帧
main 的栈帧
栈底
….
CPU对过程调用的支持
相关的寄存器: 1. ESP:存放一个指针,该指针指向系统栈最上面一个栈帧 栈帧的 栈帧 栈顶,即整个系统栈的栈顶。 2. EBP:存放一个指针,该指针指向系统栈最上面一个栈帧 栈帧的 栈帧 栈底,即当前栈帧的栈底。有时也被称为栈帧寄存器。 3. EIP:指令寄存器,存放一个指针,指向下一条等待执行的指 令地址。 相关的机器指令:
斐波那契数列的函数运行过程
斐波那契数列运行时的堆栈状态
b ^ ^ f(1)=1 b ^ ^ f(2) b ^ ^ f(3) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(0)=1 b ^ ^ f(2) b ^ ^ f(3) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(2)=2 b ^ ^ f(3) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(1)=1 b ^ ^ f(3) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(3)=3 b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(1)=1 b ^ ^ f(2) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(0)=1 b ^ ^ f(2) b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(2)=2 b ^ ^ f(4) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(4)=5 b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(1)=1 b ^ ^ f(2) b ^ ^ f(3) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(0)=1 b ^ ^ f(2) b ^ ^ f(3) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(2)=2 b ^ ^ f(3) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(1)=1 b ^ ^ f(3) b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^ ^ f(3)=3 b ^^ ^^ f(5) a ^^^ ^^^ main() shall b ^^ ^^ f(5)=8 a ^^^ ^^^ main() shall a ^^^ ^^^ main()=0 shall 注:同种颜色的字的数据可以推出相同颜色单元格的数据