问题##

1、Java到底是按值传递(Call by Value),还是按引用传递(Call by Reference)?

2、如下面的代码,为什么不能进行交换?

public CallBy swap2(CallBy a,CallBy b) {
CallBy t = a;
a = b;
b = t;
return b;
}

3、如下面的代码,为什么能够交换成功?

public int swap2(CallBy a,CallBy b) {
int t = a.value;
a.value = b.value;
b.value = t;
return t;
}

简单的C++例子##

为了解决上面的三个问题,我们从简单的例子开始,为什么是C++的例子呢?看完了你就会明白。

假设我们要交换两个整形变量的值,在C++中怎么做呢?

我们来看多种方式,哪种能够做到.

#include <iostream>

using namespace std;

// 可以交换的例子
void call_by_ref(int &p,int &q) {
int t = p;
p = q;
q = t;
} // 不能交换的例子
void call_by_val_ptr(int * p,int * q) {
int * t = p;
p = q;
q = t;
} // 不能交换的例子
void call_by_val(int p,int q){
int t = p ;
p = q;
q = t;
} int main() {
int a = 3;
int b = 4; cout << "---------- input ------------" << endl;
cout << "a = " << a << ", b = " << b << endl << endl; call_by_val(a,b);
cout << "---------- call_by_val ------------" << endl;
cout << "a = " << a << ", b = " << b << endl << endl; call_by_val_ptr(&a,&b);
cout << "---------- call_by_val_ptr ------------" << endl;
cout << "a = " << a << ", b = " << b << endl << endl; call_by_ref(a,b);
cout << "---------- call_by_ref ------------" << endl;
cout << "a = " << a << ", b = " << b << endl;
}

因为例子非常简单,看代码即可知道只有call_by_ref这个方法可以成功交换。这里,你一定还知道一种可以交换的方式,别着急,慢慢来,我们先看看为什么只有call_by_ref可以交换。

1、call_by_ref###

void call_by_ref(int &p,int &q) {
push %rbp
mov %rsp,%rbp
mov %rdi,-0x18(%rbp)
mov %rsi,-0x20(%rbp) //int t = p;
mov -0x18(%rbp),%rax
//关键点:rax中存放的是变量的实际地址,将地址处存放的值取出放到eax中
mov (%rax),%eax
mov %eax,-0x4(%rbp) //p = q;
mov -0x20(%rbp),%rax
//关键点:rax中存放的是变量的实际地址,将地址处存放的值取出放到edx
mov (%rax),%edx
mov -0x18(%rbp),%rax
mov %edx,(%rax) //q = t;
mov -0x20(%rbp),%rax
mov -0x4(%rbp),%edx
//关键点:rax存放的也是实际地址,同上.
mov %edx,(%rax)
}

上面这段汇编的逻辑非常简单,我们看到里面的关键点都在强调:

将值存放在实际地址中.

上面这句话虽然简单,但很重要,可以拆为两点:

1、要有实际地址.

2、要有将值存入实际地址的动作.

从上面的代码中,我们看到已经有“存值”这个动作,那么传入的是否实际地址呢?

// c代码
call_by_val_ptr(&a,&b); // 对应的汇编代码 lea -0x18(%rbp),%rdx
lea -0x14(%rbp),%rax
mov %rdx,%rsi
mov %rax,%rdi
callq 4008c0 <_Z11call_by_refRiS_>

注意到,lea操作是取地址,那么就能确定这种“按引用传递“的方式,实际是传入了实参的实际地址。

那么,满足了上文的两个条件,就能交换成功。

2、call_by_val###

call_by_val的反汇编代码如下:

void call_by_val(int p,int q){
push %rbp
mov %rsp,%rbp
mov %edi,-0x14(%rbp)
mov %esi,-0x18(%rbp) //int t = p ;
mov -0x14(%rbp),%eax
mov %eax,-0x4(%rbp) //p = q;
mov -0x18(%rbp),%eax
mov %eax,-0x14(%rbp) //q = t;
mov -0x4(%rbp),%eax
mov %eax,-0x18(%rbp)
}

可以看到,上面的代码中在赋值时,仅仅是将某种”值“放入了寄存器,再观察下传参的代码:

// c++代码
call_by_val(a,b); // 对应的汇编代码
mov -0x18(%rbp),%edx
mov -0x14(%rbp),%eax
mov %edx,%esi
mov %eax,%edi
callq 400912 <_Z11call_by_valii>

可以看出,仅仅是将变量a、b的值存入了寄存器,而非”地址“或者能找到其”地址“的东西。

那么,因为不满足上文的两个条件,所以不能交换。

这里还有一点有趣的东西,也就是我们常听说的拷贝(Copy)

当一个值,被放入寄存器或者堆栈中,其拥有了新的地址,那么这个值就和其原来的实际地址没有关系了,这种行为,是不是很像一种拷贝?

但实际上,在我看来,这是一个很误导的术语,因为上面的按引用传递的call_by_ref实际上也是拷贝一种值,它是个地址,而且是实际地址。

所以,应该记住的是那两个条件,在你还不能真正理解拷贝的意义之前最好不要用这个术语。

2、call_by_val_ptr###

这种方式,本来是可以完成交换的,因为我们可以用指针来指向实际地址,这样我们就满足了条件1:

要有实际地址。

别着急,我们先看下上文的实现中,为什么没有完成交换:

void call_by_val_ptr(int * p,int * q) {
push %rbp
mov %rsp,%rbp
mov %rdi,-0x18(%rbp)
mov %rsi,-0x20(%rbp)
//int * t = p;
mov -0x18(%rbp),%rax
mov %rax,-0x8(%rbp)
//p = q;
mov -0x20(%rbp),%rax
mov %rax,-0x18(%rbp)
//q = t;
mov -0x8(%rbp),%rax
mov %rax,-0x20(%rbp)
}

可以看到,上面的逻辑和call_by_val非常类似,也只是做了将值放到寄存器这件事,那么再看下传给它的参数:

// c++代码
call_by_val_ptr(&a,&b); // 对应的汇编代码
lea -0x18(%rbp),%rdx
lea -0x14(%rbp),%rax
mov %rdx,%rsi
mov %rax,%rdi
callq 4008ec <_Z15call_by_val_ptrPiS_>

注意到,lea是取地址,所以这里实际也是将地址传进去了,但为什么没有完成交换?

因为不满足条件2:

将值存入实际地址。

call_by_val_ptr中的交换,从汇编代码就能看出,只是交换了指针指向的地址,而没有通过将值存入这个地址而改变地址中的值

Java##

通过上面的例子,我们掌握了要完成交换的两个条件,也了解了什么是传引用,什么是传值,从实际效果来讲:

如果传入的值,是实参的实际地址,那么就可以认为是按引用传递。否则,就是按值传递。而实际上,传值和传引用在汇编层面或者机器码层面没有语义,因为都是将某个”值“丢给了寄存器或者堆栈。

所以,类似Java是按值传递还是按引用传递这种问题,通常没有任何意义,因为要看站在哪个抽象层次上看。

如果非要定义一下,好吧,也许你会在面试中碰到这种问题,那么最好这样回答:

Java是按值传递的,但可以达到按引用传递的效果。

那么,什么是有意义的呢?

上文的那两个条件。

但从编译的角度讲,引用和地址有很强的关系,却不是一回事。

Java按值传递的行为###

我们回顾下开头的三个问题中的第二个问题:

如下面的代码,为什么不能进行交换?

public CallBy swap2(CallBy a,CallBy b) {
CallBy t = a;
a = b;
b = t;
return b;
}

我们首先从比较简单的bytecode看起:

public com.haoran.CallBy swap2(com.haoran.CallBy, com.haoran.CallBy);
descriptor: (Lcom/haoran/CallBy;Lcom/haoran/CallBy;)Lcom/haoran/CallBy;
flags: ACC_PUBLIC
Code:
stack=1, locals=4, args_size=3
0: aload_1
1: astore_3
2: aload_2
3: astore_1
4: aload_3
5: astore_2
6: aload_2
7: areturn
LineNumberTable:
line 45: 0
line 46: 2
line 47: 4
line 48: 6
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/haoran/CallBy;
0 8 1 a Lcom/haoran/CallBy;
0 8 2 b Lcom/haoran/CallBy;
2 6 3 t Lcom/haoran/CallBy;

集中精力看这一块:

//t = a
0: aload_1
1: astore_3 //a = b
2: aload_2
3: astore_1 //b = t
4: aload_3
5: astore_2 6: aload_2

代码很简单,注释也表明了在做什么, 那么需要不需要看传递给它什么参数呢?

不需要,先看下汇编代码,满足不满足”将值放到实际地址“这个条件,我们截取a = b这一句来观察:

// a = b bytecode
2: aload_2
3: astore_1
/***************************************
* aload_2对应的汇编(未优化)
****************************************/
mov -0x10(%r14),%rax\n //---------------------------------------- movzbl 0x1(%r13),%ebx
inc %r13
movabs $0x7ffff71ad900,%r10
jmpq *(%r10,%rbx,8)
/***************************************
* astore_1对应的汇编(未优化)
****************************************/
pop %rax
mov %rax,-0x8(%r14) //---------------------------------------- movzbl 0x1(%r13),%ebx
inc %r13
movabs $0x7ffff71ae100,%r10
jmpq *(%r10,%rbx,8)

如果将上面的代码和c++实例中的call_by_ref和call_by_val对比,就会发现,上面的代码缺失了这样一种语义:

将值放入实际地址中。

其仅仅是将值放入寄存器或者堆栈上,并没有将值放入实际地址这个操作。

为什么不需要观察给这个方法传参的过程?

这是一个很简单的必要条件问题,所以,不需要观察。

从上面的过程来看,Java的行为是Call by Value的。

Java按引用传递的行为###

现在来讨论第三个问题:

如下面的代码,为什么能够交换成功?

public int swap2(CallBy a,CallBy b) {
int t = a.value;
a.value = b.value;
b.value = t;
return t;
}

还是从bytecode先看起:

public com.haoran.CallBy swap2(com.haoran.CallBy, com.haoran.CallBy);
descriptor: (Lcom/haoran/CallBy;Lcom/haoran/CallBy;)Lcom/haoran/CallBy;
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: aload_1
1: getfield #2 // Field value:I
4: istore_3
5: aload_1
6: aload_2
7: getfield #2 // Field value:I
10: putfield #2 // Field value:I
13: aload_2
14: iload_3
15: putfield #2 // Field value:I
18: aload_1
19: areturn
LineNumberTable:
line 41: 0
line 42: 5
line 43: 13
line 44: 18
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/haoran/CallBy;
0 20 1 a Lcom/haoran/CallBy;
0 20 2 b Lcom/haoran/CallBy;
5 15 3 t I

聚焦putfield这句字节码,其对应的是在b.value的值放到操作数栈顶后,拿下这个值, 给a.value:

a.value = b.value;

因为putfield字节码对应的汇编代码非常长,我们进一步聚焦其宏汇编,因为CallBy类的value字段,是个int型,所以我们关注itos:

// itos
{
__ pop(itos);
if (!is_static) pop_and_check_object(obj);
// 关键点:只看这里
__ movl(field, rax);
if (!is_static) {
patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no);
}
__ jmp(Done);
}

其中关键的一句:__ movl(field, rax);

void Assembler::movl(Address dst, Register src) {
InstructionMark im(this);
prefix(dst, src);
emit_int8((unsigned char)0x89);
emit_operand(src, dst);
}

movl对应的汇编代码:

// 对应的关键汇编代码
...
mov %eax,(%rcx,%rbx,1)
...

上面的汇编代码的意思是:

将eax中的值存入rcx + rbx*1所指向的地址处。

其中,eax的值就是b.value的值,而rcx+rbx*1所指向的地址就是a.value的地址。

上面的过程满足了这样的语义:

将值存入实际地址中.

所以,a.value和b.value可以交换,类似这样,Java的按值传递方式也可以表现出按引用传递的行为

另一种交换值的方式##

还记得在C++的实例中,我们提到还有一种交换值的方式,是什么呢?

call_by_WHAT###

void call_by_WHAT(int * p,int * q) {
int t = *p;
*p = *q;
*q = t;
}

这样传参:

int main() {
int a = 3;
int b = 4; cout << "---------- input ------------" << endl;
cout << "a = " << a << ", b = " << b << endl << endl; call_by_WHAT(&a,&b);
cout << "---------- call_by_WHAT ------------" << endl;
cout << "a = " << a << ", b = " << b << endl;
}

会不会交换呢?

// 输出

---------- input ------------
a = 3, b = 4 ---------- call_by_WHAT ------------
a = 4, b = 3

从这种方式中,我们也看到了所有能够交换值的方式的统一性:

1、指针p、q或者对象引用objectRef,能够直接指向对象的实际地址。

2、要有一个将值放入实际地址的操作:

// C/C++
*p = *q;
... // Java
putField -> a.value = b.value
... // 汇编
mov reg_src , (reg_dst)
mov reg_src , (addr_dst)
...

结语##

老生常谈,并无LUAN用,终。

你会swap吗,按值传递还是按引用?的更多相关文章

  1. java学习——java按值传递和按址传递

    先复制一个面试/笔试的题: 当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递? 答案: 是值传递.Java语言的方法调用只支持参 ...

  2. C++基础学习4:引用

    C++引用(Reference) 引用(Reference)是C++语言相对于C语言的又一个扩充,是C++常用的一个重要内容之一.类似于指针,只是在声明的时候用"&"取代了 ...

  3. java按值传递理解

    Java没有引用传递只有按值传递,没有引用传递只有按值传递,值传递. 通过下面代码解释: public class Test { public static void main(String[] ar ...

  4. java按值传递相关理解

    Java没有引用传递只有按值传递,没有引用传递只有按值传递,值传递. 1. public class Test {     public static void main(String[] args ...

  5. C++ Primer : 第九章 : 顺序容器的定义、迭代器以及赋值与swap

    顺序容器属于C++ STL的一部分,也是非常重要的一部分. 顺序容器包括: std::vector,包含在头文件<vector>中 std::string, 包含在头文件<strin ...

  6. Java方法的参数是按值传递的.【转】

    在Java中,所有的方法参数,都是"按值传递". 有那么一种说法,Java中基本类型是按值传递,对象是按引用传递.这个说法其实是不确切的,确切的说法是 Java中基本类型将值作为参 ...

  7. java按值传递理解(转)

    ava没有引用传递只有按值传递,没有引用传递只有按值传递,值传递. 通过下面代码解释: 1 public class Test { 2 public static void main(String[] ...

  8. JavaScript进阶(三) 值传递和引用传递

    从C语言开始 有时候讲一些细节或是底层的东西,我喜欢用C语言来讲,因为用C更方便来描述内存里面的东西.先举一个例子,swap函数,相信有一些编程经验的人都见识过,声明如下,函数体我就不写了,各位脑补一 ...

  9. C++ 参数传值 与 传引用

    参数传值 在 C++ 中,函数参数的传递有两种方式:传值和传引用.在函数的形参不是引用的情况下,参数传递方式是传值的.传引用的方式要求函数的形参是引用.“传值”是指,函数的形参是实参的一个拷贝,在函数 ...

随机推荐

  1. jQuery无刷新上传之uploadify简单试用

    先简单的侃两句:貌似已经有两个月的时间没有写过文章了,不过仍会像以前那样每天至少有一至两个小时是泡在园子里看各位大神的文章.前些天在研究“ajax无刷新上传”方面的一些插件,用SWFUpload实现了 ...

  2. 读书笔记——Windows核心编程(13)Windows内存体系结构

    对于32位进程(0x0000 0000~0xFFFF FFFF),有4GB的地址空间. 每个进程都有自己专有的地址空间,当进程的各个线程运行时,它们只能访问属于该进程的内存. 这4GB其实是虚拟地址空 ...

  3. MyCat 学习笔记 第十一篇.数据分片 之 分片数据查询 ( select * from table_name limit 100000,100 )

    1 环境说明 VM 模拟3台MYSQL 5.6 服务器 VM1 192.168.31.187:3307 VM2 192.168.31.212:3307 VM3 192.168.31.150:  330 ...

  4. CI 框架中 AR 操作

    Model 层中的部分代码 /** * CI 中的 AR 操作 * @author zhaoyingnan **/ public function mAR() { /*************** 查 ...

  5. 【嵌入式开发板】8月终极暑促迅为Cortex-a9四核入门开发板

    核心板参数 尺寸 50mm*60mm 高度 连同连接器在内0.26cm CPU Exynos4412,四核Cortex-A9,主频为1.4GHz-1.6GHz 内存 1GB 双通道 DDR3(2GB  ...

  6. Linux上Eclipse项目右键菜单没有Maven

    在Centos 7上安装了eclipse以后,着实很兴奋.eclipse luna版本自带maven.但是用mvn eclipse:eclipse创建的java工程,在右键菜单居然没有Maven按钮, ...

  7. 对"构建之法“的理解和困惑

    对"构建之法"的理解和困惑        本人"学沫沫"一个,对于之前的编程学习虽不大"感冒",但秉着对自己负责的态度进行了基础学习.   ...

  8. TestNG之执行测试类方式

    TestNG提供了很多执行方式,下面做简单介绍. 1.XML指明测试类,按照类名执行,其中可以指定包名,也可指定无包名: 带包名,运行ParameterSample类和ParameterTest类 & ...

  9. selenium处理div生成弹框

    目前遇到的弹框有两种,一种是alert,一种是div,如果遇到div模拟的弹框,在用alert就不行了. 1. public static Alert getAlert(WebDriver dr) { ...

  10. Linux 常用基本命令

    这两天有俩哥们问了我linux的事,问我在工作中需不需要用到,需不需要学会 一个是工作1年不到的,我跟他说,建议你学学,在以后肯定是要用到的,虽然用到的机会不多,但是会总比不会好 另一个是工作6年的, ...