Java的方法参数是按值传递的,这篇主要是把Java的传值机制和C语言的指针和二重指针进行比较,并基于汇编代码更深入的理解传值和传引用。

和C语言的比较

C语言和Java一样是 pass by value的。在C中,如果你想在另一个函数中改变一个结构体内部数据的值,你只要向那个函数传递指针即可;而如果你想在另一个函数中改变它的指针的指向,你需要使用二重指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  int x, y;
} Point;

int edit_point(Point *point) {
  point->x += 1;
  point->y += 1;
  return 0;
}

int change_point(Point **point_ref) {
  int x = (*point_ref)->x;
  int y = (*point_ref)->y;
  free(*point_ref);

  *point_ref = realloc(*point_ref, sizeof(Point));
  if (*point_ref == NULL) return -1;
  (*point_ref)->x = x * 10;
  (*point_ref)->y = y * 10;
  return 0;
}

void show_point(Point *point) {
  printf("(%d, %d)\n", point->x, point->y);
}

int main() {
  Point *point = malloc(sizeof(Point));
  point->x = 1;
  point->y = 2;
  show_point(point);

  edit_point(point);
  show_point(point);

  change_point(&point);
  show_point(point);
}

输出结果:

1
2
3
4
5
▶ gcc pass.c -o pass
▶ ./pass
(1, 2)
(2, 3)
(20, 30)

以上C代码用Java实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Pass {
  static class Point {
    int x, y;

    public Point(int x, int y) {
      this.x = x;
      this.y = y;
    }

    @Override
    public String toString() {
      return String.format("(%d, %d)", this.x, this.y);
    }
  }

  static void editPoint(Point pointCopy) {
    pointCopy.x += 1;
    pointCopy.y += 1;
  }

  static Point changePoint(Point pointCopy) {
    return new Point(pointCopy.x * 10, pointCopy.y * 10);
  }

  public static void main(String[] args) {
    Point point = new Point(1, 2);
    System.out.println(point);

    editPoint(point);
    System.out.println(point);

    point = changePoint(point);
    System.out.println(point);
  }
}

由于Java没有二级指针这个概念,所以没办法在另一个方法中得到原来对象的真实引用(除非是字段,即全局变量),只能通过返回新的引用,并在调用者那里进行赋值。

和汇编代码的比较

二重指针在汇编的层次就是首先进行一次寄存器寻址,再进行一次间接寻址。可以结合下面的图加以理解。

pointer-of-pointer

接下来为使用gdb调试得到二重指针的内存地址和Point的内存地址:

注: %rsp的值已经改变

asm-value

汇编代码如下,几个重要的步骤我已经加上注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
edit_point:
	addl	$1, (%rdi)
	addl	$1, 4(%rdi)
	movl	$0, %eax
	ret

change_point:
	pushq	%r12
	pushq	%rbp
	pushq	%rbx
	movq	%rdi, %rbx
	movq	(%rdi), %rdi # 此时%rdi为0x7fffffffe040,这条指令把0x7fffffffe040内存位置中的值(0x555555756260)复制到%rdi
	movl	(%rdi), %r12d # 0x7fffffffe040中存储的是0x555555756260,这才是Point所在的内存地址,把point->x复制给%r12
	movl	4(%rdi), %ebp # 把 point->y 复制给%ebp
	call	free@PLT
	movl	$8, %esi
	movq	(%rbx), %rdi
	call	realloc@PLT
	movq	%rax, (%rbx)
	testq	%rax, %rax
	je	.L4
	leal	(%r12,%r12,4), %ecx
	leal	(%rcx,%rcx), %edx
	movl	%edx, (%rax)
	movq	(%rbx), %rcx
	leal	0(%rbp,%rbp,4), %edx
	leal	(%rdx,%rdx), %eax
	movl	%eax, 4(%rcx)
	movl	$0, %eax
.L2:
	popq	%rbx
	popq	%rbp
	popq	%r12
	ret
.L4:
	movl	$-1, %eax
	jmp	.L2

main:
	subq	$24, %rsp
	movq	%fs:40, %rax
	movq	%rax, 8(%rsp)
	xorl	%eax, %eax
	movl	$8, %edi
	call	malloc@PLT
	movq	%rax, (%rsp)
	movl	$1, (%rax)
	movl	$2, 4(%rax)
	movq	%rax, %rdi
	call	show_point
	movq	(%rsp), %rdi
	call	edit_point
	movq	(%rsp), %rdi
	call	show_point
	movq	%rsp, %rdi # %rps中保存的是二级指针的内存地址(0x7fffffffe040)
	call	change_point
	movq	(%rsp), %rdi
	call	show_point
	movq	8(%rsp), %rdx
	xorq	%fs:40, %rdx
	jne	.L11
	movl	$0, %eax
	addq	$24, %rsp
	ret

参考