c++ swap函数_深入理解函数调用(下)

上一节课,我们讲了函数栈与栈帧,以及形式参数和实参的区别。这一节课,我们再细化一点,看一下Java的栈帧里的具体内容。

昨天的课程里,有同学私信我,问Java栈帧和操作数栈是什么关系。可能是我的图画得比较有误导性,所以我今天把图重新组织了一下。

Java虚拟机在执行字节码的时候,会使用一个叫做操作数栈的数据结构来进行运算,还会在函数栈帧上保存一个叫做局部变量的结构,用来存储各个局部变量的值。

还是拿上节课的例子来讲:

public 

那么函数栈帧是怎么组织的呢?结合上节课的内容,虚拟机会开辟一段内存做为栈帧。栈帧上有局部变量表,还有操作数栈。我们以暗蓝色代表局部变量表,那么,函数对应的帧大体上是这个样子的:

7163fa6d0dd5c843423a60467ae9b50b.png

一开始,a, b, t 都只是占了一个位置,而没有具体的值。我们来看字节码,一行行地分析JVM是如何执行字节码的。还是老规矩,把字节码打印出来:

  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_3
       8: return

第0条指令,iconst_1,代表把常量1,送到操作数栈顶,那么现在操作数栈就变成了:

a6cf741abc6d8ff18e450b0edeb21734.png

第1条指令,istore_1,代表取出栈顶的值,并将其存入到局部变量表的第一个位置,也就是变量a对应的位置。这时,栈帧的内容变成了这样:

ed331a0d39e235c8429861905d263629.png

iconst_2和istore_2与上面的过程是一样的,我就不再重复画了。执行完这两行之后,栈的内容变成了:

c07c761b4cefd2c845412b75e1dba58a.png

接下来,是第4行和第5行,iload_1就是把局部变量表的第一个位置,也就是a的值1,送入栈顶,iload_2是把局部变量表的第二个位置,就是b的值2,送入栈顶。这时栈,就变成了这个样子:

717223f1e49741841b14b9c307550e3c.png

这里有同学可能会有疑问,1和2的位置是不是画反了。还真不是,在x86或者x64这个体系结构上,栈是从高地址向低地址增长的,所以栈底在上面,栈顶在下面。

接下来,又是iadd了。这条指令,我们已经连续三节课分析它了,是老朋友了。相信大家都能记住了,iadd三件事:从栈里弹出两个数,求和,把和送到栈顶。所以,经过了iadd指令以后,栈帧就变成这样了:

dc90b07955bc227c4ef50b390679ee64.png

最后再执行istore_3,把栈顶上的3送到局部变量表的第3个位置。就是t,这个过程与前面的istore_1, istore_2是相似的。所以不再画图了。

栈和堆

下面继续今天的重点。我们上节课,介绍了在函数调用的时候,往新的栈帧传递参数的时候,会把参数从调用者的栈帧里往被调用者的栈帧里复制一份。在被调用者的栈帧里对参数进行修改,只能修改被调用者的那份副本,而调用者栈里的那一份数据是改变不了的。但是,必须要加一个条件,那就是参数都是基础数据类型,比如int, float, double等等。对于普通的Java对象就不成立了。

大家可以先想一下,假设参数是Java对象,JVM在调用函数的时候,把对象拷贝一份进入到谳用者的栈帧里去,如果对象很大,比如有几十上百个成员变量,那这个消耗就很可观了。有时,返回值还可能是一个大的对象。怎么解决这个问题呢?其实从C语言的时代,针对这个问题,就有了解决方案,那就是堆和指针。Java继承了这一方案,堆的概念在Java仍然适用,只是指针变成了引用(reference)。

堆(heap)是一块独立的内存空间。在创建对象的时候,我们可以把真正的对象放到堆里,只在栈里记录这个对象的地址就可以了。我们可以凭借这个地址找到真正的对象,所以,引用这个名称还是挺形象的。先看代码:

public 

对照昨天的作业,我们看到,如果swap函数的参数类型是整型的时候,并不能交换两个数的值,而如果参数类型是普通的Java class,则可以交换这两个对象的value值。好了,上图:

06368ac97bdd2c2a6a8c440b430a8a73.png

可以看到,在main的栈里,和swap的栈里,都只是存了一个指向堆里真实对象的引用,也就是一个地址。如果在swap里,把A1中的value和A2中的value对换,那么,当swap结束调用以后,在main里,看到A1的value和A2的value就已经对换了。

这就是函数调用时传值与传引用的区别。

好了。今天的课程就到这里了。我们虽然还没有把函数的全部机制都讲清楚的。但是掌握这些知识已经足够我们去完成递归下降的函数设计了。明天就会真正地开始使用递归下降去写表达式求值了。

今天的作业:

1. 去网上找一下Java语言规范,文档的英文名:The Java Language Specification Java SE 8 Edition。看看Java里还定义了哪些字节码。尝试着理解一下。

2. 写一个带有 if , while, for 的程序,使用javap -c命令查看它的字节码。看看能不能看懂(看不懂也没关系,我们后面会专门讲,但如果你现在就看懂了,后面会感觉很省力。)

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

下一节课:

目录:课程目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值