c语言 swap交换函数_深入理解函数调用(下)

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

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

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

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

public class Main {
	public static void main(String args[]) {
		int a = 1;
		int b = 2;
		int t = a + b;
	}
}

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

c2f00c3db68cf1cb1865c57fa1d3195c.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,送到操作数栈顶,那么现在操作数栈就变成了:

2bd8269da5de9af06de8860f0f2b1145.png

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

36a1428e1a8c5f123450b7d816e50230.png

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

4edd639391a8003cbce86f6271ea03f1.png

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

222976abcca9305b9bc1da7d056f93e9.png

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

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

211660b9d190376386ddcc7ace7af7e7.png

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

栈和堆

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

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

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

public class Main {
	public static void main(String args[]) {
		A a = new A(1);
		A b = new A(2);
		swap(a, b);
		System.out.println("a's value is " + a.value +", b's value is " + b.value);
	}

	public static void swap(A a, A b) {
		int t = a.value;
		a.value = b.value;
		b.value = t;
	}
}

class A {
	public int value;
	public A(int v) {
		this.value = v;
	}
}

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

d730257a67db873d65d23a3f4498c5f7.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命令查看它的字节码。看看能不能看懂(看不懂也没关系,我们后面会专门讲,但如果你现在就看懂了,后面会感觉很省力。)

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

下一节课:

目录:课程目录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值