c++调用powershell_深入理解函数调用(上)

前边一周我们介绍了使用栈来完成表达式求值,这一周的任务就是使用递归下降的文法分析来完成表达式求值。在那之前,我们先等一下,得把Java中的函数调用理解透彻,否则很难写出正确的递归。

从一开始写程序,我们就和各种各样的函数,成员方法打交道。在这个课程里,我把函数(function)和方法(method)混着用。不过,这里还是明晰一下这个概念。通常,我们说函数,是指可以在全局命名空间里独立存在的函数。而方法则是定义在类中,做为类的一个成员的函数。由于Java中的方法不能脱离类而独立定义,所以Java中其实是没有函数,只有方法的。

1. 调用时发生了什么

C/C++/Java的函数调用比较相似。调用一个函数的时候,会在一块特定的内存区域上,创建与被调用函数相对应的一小块内存。这一小块内存的作用是为了存放函数里的局部变量以及运行时所需要的其他信息。这一小块内存,有一个专门的名字,叫帧(frame)。

举一个例子来说吧。

public 

main在执行的时候,会创建一个属于main函数的frame,这里面记录了main中所定义的局部变量a, b。

077382109f791a9b6338ac16b0724c64.png

当main执行到 foo 调用的时候,就会创建一个属于 foo 函数的frame。foo的frame会紧跟在main的 frame 的后面。这里面记录了foo的两个参数 i 和 j:

8ee0467f60e1b2c4913bac58f6074fa0.png

当 foo 执行到 bar 调用的时候,会创建一个属于 bar 函数的frame,紧跟在 foo 之后,这里面记录了 bar 的两个参数 i 和 j:

b84f3242060f684397891d7948ea4c73.png

当 bar 执行完以后,bar 所对应的 frame 就会销毁了。bar中的变量 i, j所占的内存就都被回收了(这个说法不准确,但是初学阶段,先这样记吧。我们学习一个新的东西的时候,不要想着一下子把所有细节所搞清楚了,搞清楚大概,然后在实践中再慢慢细化。)我们又重新回到 foo 的 frame 中,又可以对 foo 中的值进行操作了。注意,到了计算 t 的时候,i 的值仍然是2, j 的值仍然是 3。初学者最容易犯的错误,就是觉得,i 和 j 在方法 bar 中都被加了1,怎么会没变呢?我们看函数帧就知道了,foo 在调用 bar 的时候,是把 foo 中的 i 和 j 做了一次拷贝,把这俩整数复制到了 bar 的帧里了。我们在bar 里对传进来的参数进行修改,只能改动 bar 这个帧里的变量,而不能改变 foo 里的变量。因此, foo 在执行完 i + j 以后,其帧是长这样的:

fa896063b7655831a6285782cac26b30.png

foo返回main的图我就省略了,过程基本上是相似的。

回顾上面的过程,可以看到,先创建的frame最后才被销毁,后创建的frame最先被销毁。这是啥?这不就是我们刚刚学过的栈吗?后进先出,先进后出,只是在这里,栈里的每一个元素是一个 frame。计算机的创造者们也懒得再去发明一个新的词来称呼这块内存区域了,干脆就叫它栈吧。所以,当我们在说栈的时候,有可能是指一种特定的数据结构,也有可能是指程序运行时所使用的这一块内存。栈上的一个元素,对应一个函数,叫做一帧。

这个过程C/C++/Java,基本上大同小异。

2. 返回时发生了什么

真正地理解返回值,需要我们深入地理解CPU的架构,比如寄存器,栈帧的结构等等,但我今天不想讲那么深。只想向大家展示一下基本的结构。后面再去慢慢细化。

大家可以自己想一下,如果让你设计一个函数向其调用者返回某一个值的时候,你会怎么做?

就比如,你同桌向你借了块橡皮,要 return 给你。你告诉她,放在桌子上吧。她放在桌子上了,你再从桌上取就好了(为什么不放在你的手上,注孤生)。方法 return 的过程与这个过程很相似,也是调用者和被调用者说好一个地方,然后被调用者把数据放到那个地方,调用者去取这个数据就好了。

看一个例子:

public 

看看这个代码的字节码文件,使用命令(请记住这个命令,后面我们会经常用到):

javac Main.java
javap -c Main

得到Main文件的字节码:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: iconst_2
       2: iconst_3
       3: invokestatic  #2                  // Method add:(II)I
       6: iadd
       7: istore_1
       8: return

  public static int add(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: ireturn

这个过程用图来表示是这样的,main方法的前三条指令会把1, 2, 3,分别压到操作数栈上。这个操作数栈是位于 main 方法的帧里的。

b13c24dd0e07718869a1394a9815cabd.png

接着,第四条指令“invokestatic”就会调用 add 函数,由于 add 函数接受两个参数,所以就把2, 3出栈。(注意!!这里的操作数栈是我们上一节课所讲的数据结构的栈,而不是函数栈,千万别搞混了,函数栈在函数调用时只会创建新的栈帧,没有任何的出栈动作。),然后在 add 函数 里计算2 + 3。

f08128be55de2afc00b295205f4a0fb7.png

当 add 执行完以后,会把返回值再放到操作数栈里。可见,那个所谓的“约定的地方”,在java bytecode里看来,就是操作数栈顶部。

56e8fc27861dffc9622e6c7d9288454b.png

好了。今天的课程很简单,很容易就看懂了。今天的作业不是代码,是动手画图的题目:

1. 分析以下代码的函数栈的情况,并说出代码的输出是什么,解释为什么。

public 

2. 分析以下代码的运行过程:

public 

上一节课:用栈进行表达式求值

下一节课:深入理解函数调用(下)

目录:课程目录

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值