为什么说函数只存在值传递 & 引用类型是什么

1. 例子一

我们先来看几个例子. 先来Java的吧. Java最假了. 一般人都认为是引用传递(基本类型值传递). 所以我们又拿C++做例子.

写 a , b 是为了好区分开. 其实 写俩a都行. 不好书面表达

public class Demo {

    public static void main(String[] args) {
        A a = new A();
        System.out.println("初始化 : " + a.name);
        a.name = "main";
        System.out.println("init函数调用前 : " + a.name);
        // change 函数传递
        init(a);
        // 结果 ?
        System.out.println("init函数调用后 : "+a.name);
    }

    static void init(A b) {
        // a被初始化了..
        b = new A();
    }

    private static class A {
        String name;
    }
}

输出 :

初始化 : null
init函数调用前 : main
init函数调用后 : main

结果是啥. 如果真的是引用传递. 那么为啥a.name为啥不是空呢.

我用C++ 给大家写一下.

```c++

include

include

using namespace std;

class A { public: string name; };

void init(A *b) { cout << "未改变的地址 : " << b << endl; *b = A(); cout << "改变后的地址 : " << b << endl; } int main(int argc, char const *argv[]) { A *a = new A(); cout << "初始化 : " << a->name << endl; a->name = "main"; cout << "init函数调用前 : " << a->name << endl; init(a); cout << "init函数调用后 : " << a->name << endl; return 0; } ```

输出

初始化 : 
init函数调用前 : main
未改变的地址 : 0xe699d0
改变后的地址 : 0xe699d0
init函数调用后 : 

显然C++和Java的表现是不一样的. 为什么C++ 赋值可以改变值. 而Java却不是呢.

我们先解释C++的做法.

我们init(A *b)做了啥

首先是我们定义了一个变量 A *a=new A("main") , 我没有写构造函数, 假设的.

此时传递给函数init(A *b) , 此时 b=a ,a= 0xe699d0 . 所以呢 b= 0xe699d0 .

后面我们的操作就是基于这个 b 的. 此时我们将*b =A(); , 是不是将0xe699d0指针指向了 A() 对象呢(其实就是块内存,指向的是内存的首地址) . 此时是不是修改了0xe699d0指针 的指向呢. 那么a也等于 0xe699d0 .所以a的值也被修改了 .

然后我们看看Java的做法.

static void init(A b) {
    System.out.println("未改变的地址: 0x"+Integer.toHexString(a.hashCode()));
    // a被初始化了..
    b = new A();
    System.out.println("改变后的地址: 0x"+Integer.toHexString(a.hashCode()));
}

我们打印一下 hashcode. 因为Java的hashcode其实就是确定一个对象的唯一标识, 如果你想深入了解hashcode的话介意看看JVM的源码,C++和Java对象的映射关系 (如果是对象地址的话,那么内存回收会改变大量的内存地址,难道还是地址吗,我们这里不考虑这个.只要知道这个是Java对象的唯一值类似于hash码 ).

输出结果:

初始化 : null
init函数调用前 : main
未改变的地址: 0x6e8cf4c6
改变后的地址: 0x12edcd21
init函数调用后 : main

此时我们发现地址发生了改变. 其实理解了上面C++那部分聪明的就明白了.

由于Java引用类型传递如果是Hotspot虚拟机则实现的就是简单的指针传递. 这个变量指向Java对象的数据区域.

由于一开始 a=0x6e8cf4c6(Java引用对象) , 然后函数赋值 b=a , 此时 b= 0x6e8cf4c6.

关键点在于new A()地方; 执行的是,实例化一个A对象,在栈顶开辟一块空间, 将A对象保存在栈顶中, 此时栈顶值=0x12edcd21, 然后将栈顶值存入到变量b中. 此时b=0x12edcd21 . 那么改变原来0x6e8cf4c6指向的内容了吗,并没有.

我们再看看javac编译后的结果 :

static void init(com.jvm.reference.Demo$A);
descriptor: (Lcom/jvm/reference/Demo$A;)V
flags: ACC_STATIC
Code:
// 栈的深度需要3 , 变量表需要一个,参数一个
  stack=3, locals=1, args_size=1
      // 实例化一个对象
     0: new           #2                  // class com/jvm/reference/Demo$A
     3: dup
     4: aconst_null
     // 调用构造方法
     5: invokespecial #3                  // Method com/jvm/reference/Demo$A."":(Lcom/jvm/reference/Demo$1;)V
     // 将栈顶值存入变量0中
     8: astore_0
     // 返回
     9: return

准确点说Java的做法是 : 如下操作.

void init(A *b)
{
    cout << "未改变的地址 : " << b << endl;
    A a = A();
    b = &a;
    cout << "改变后的地址 : " << b << endl;
}

其实各种做法也有各种做法的好处. 没有绝对的好坏.

2. 例子二

其实你理解上面这个例子. 你就明白了为啥值传递了. 我们继续拿C++说话. Java查看地址不方便.

#include 

using namespace std;

void swap(int *a, int *b)
{
    int *temp = a;
    a = b;
    b = temp;
    cout << "a : " << *a << " , b : " << *b << endl;
}

int main(int argc, char const *argv[])
{
    int x = 1;
    int y = 2;

    cout << "x : " << x << " , y : " << y << endl;
    // swap 方法.
    swap(&x, &y);

    cout << "x : " << x << " , y : " << y << endl;
    /* code */
    return 0;
}

输出

x : 1 , y : 2
a : 2 , b : 1
x : 1 , y : 2

我们发现为啥函数内部 . a 和 b 成功交换了地址 . 所以 a 和 b的值就互换了 .

但是为啥呢.

我们再次打印一下地址

cout << "&x  : " << &x << " , &y : " << &y << endl;
// 输出 : 
&x : 0x61fefc , &y : 0x61fef8

&x = 0x61fefc , &y=0x61fef8 ,

执行swap函数. 此时 a=0x61fefc , b=0x61fef8 ,

然后经过一番操作, 此时 a=0x61fef8 , b=0x61fefc . 然后输出 *a=2, *b=1, 所以成功了.

但是为啥 x 和 y 没变呢. 是不是发现 &x 和 &y依旧着原来的地址呢.

正确的swap操作 , 必须修改指针指向的值.

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

3. Java的引用类型

创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

为什么说函数只存在值传递 & 引用类型是什么

本文第三节引用自 <<深入理解Java虚拟机>> .

这两种对象访问方式各有优势 :

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个软件开发的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

所以句柄访问方便管理, 直接指针访问效率高, 但是不方便管理.

展开阅读全文

页面更新:2024-02-19

标签:函数   句柄   开销   初始化   指针   例子   好处   对象   类型   情况   方式

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top