寫在前邊
- 上次聊到Java8新特性 lambda時,有小伙伴在評論區提及到了lambda對於局部變量的引用,補充着博客的時候,知識點一發散就有了這篇對於值傳遞還是引用傳遞的思考。關於這個問題為何會有如此多的誤區,這篇就來破解ta!
果然知識網的發散是無止境的!
文中的一些定義可能帶有個人見解,引發了歧義,決定重新補充一下(如有錯誤還望指出!)
知識儲備--堆和棧
- 堆是指動態分配內存的一塊區域,一般由程序員手動分配,比如 Java 中的 new、c里邊的malloc。
- 棧是編譯器幫我們分配好的區域,一般用於存放函數的參數值,局部變量等
有關堆棧的相關知識在 迷途指針 中有所提及。
數據類型
Java中除了基本數據類型,其他的均是引用類型,包括類、數組等等。
基本數據類型和引用類型的區別
先看一下這兩個變量的區別
void test1(){
int cnt = 0;
String str = new String("melo");
}
- cnt是基本類型,值就直接保存在變量中(存放在棧上)
- 而str是引用類型,變量中保存的只是實際對象的地址。一般稱這種變量為"引用",引用指向實際對象,實際對象中保存着內容。
比如我們創建了一個 Student student = new Student("Melo");
- 在堆中開辟一塊內存(真正的對象存放在堆上),其中保存了name等數據 , 而student只是保存了該對象的地址(存放在棧上)
當我們修改變量時
void test1(){
int cnt = 0;
cnt=1;
String str = new String("melo");
str="Melo";
}
對於基本類型 cnt,賦值運算符會直接改變變量的值,原來的值直接被覆蓋掉了。
ta無依無靠,不像下邊一樣有房子可以住。
對於引用類型 str,賦值運算符只會改變引用中所保存的地址,雖然原來的地址被覆蓋掉了,str指向了一個新的對象,但是原來的那個老對象沒有發生變化,他還是老老實實待在原來的地方!!!
有學過c語言的同學應該很清楚,這里借助c語言中的“指針”打個比喻。
- 引用類型str就相當於一個指針(旗子),插在了一個房子門口。現在給這個旗子挪個位置,只是讓這個旗子放置在了另一個新的房子,原本的老房子還在那里,不會說因為你改變了旗子的位置,房子就塌了。
當然,原來那個房子沒有旗子插着了,沒有人住了。也不能總是放任ta在那占着空間,過段時間也許就會有人來把他給拆了回收了(JVM)。
這種沒有地方引用到的對象就稱為垃圾對象。
對於傳遞的錯誤理解(11-6補充)
錯誤理解一:值傳遞和引用傳遞,區分的條件是傳遞的內容,如果是個值,就是值傳遞。如果是個引用,就是引用傳遞。
錯誤理解二:傳遞的參數如果是普通類型,那就是值傳遞,如果是對象,那就是引用傳遞。
值傳遞(11-6補充)
- 看到比較多的解釋: 在方法被調用時,實參通過形參把它的內容副本傳入方法內部,此時形參接收到的內容是實參值的一個拷貝,因此在方法內對形參的任何操作,都僅僅是對這個副本的操作,不影響原始值的內容。
我們上次聊到lambda的時候,提及到了值傳遞,那里的拷貝副本,就是我們這里要說的值傳遞
- 如果我們這里的方法塊訪問了外部的變量,而這個變量只是一個普通數據類型的話,相當於只是訪問到了一份副本。當外部對這個變量進行修改時,lambda內部(只有副本)是無法感知到這個變量的修改的。
我們只是將實參傳遞給了方法的形參,將cnt值復制一份,賦值給形參val,所以,函數內對形參的操作完全不會影響到實參真正存活的區域!而伴隨着函數調用的結束,形參區域和其內的局部變量也會被釋放。(方法棧的回收)
//基本類型的值傳遞
void unChange(int val) {
val = 100;
}
unChange(cnt); // cnt 並沒有被改變
引用傳遞(11-5修改!!!)
實參傳遞給形參時,形參其實用的就是實參本身(而不再單純只是拷貝一份副本出來了),當該形參變量被修改時,實參變量也會同步修改。
看到比較多的解釋:**實際參數的地址引用傳遞(pass by reference)是指在調用函數時將**直接傳遞到函數中,那么在函數中對參數所進行的修改,將影響到實際參數。
形參相當於是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作為局部變量在棧中開辟了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。
被調函數對形參的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量。
正因為如此,被調函數對形參做的任何操作都影響了主調函數中的實參變量。
- 而不僅僅說成 : 傳遞的是地址就說明是引用傳遞。(說成這樣我們就誤以為傳遞引用參數,傳遞指針變量就是引用傳遞了)
C++實例
#include <iostream>
using namespace std;
int main()
{
//&標識符
void swap(int& x,int& y);
int a = 5;
int b = 8;
swap(a,b);
return 0;
}
void swap(int& a,int& b){
int temp;
temp = a;
a = b;
b = temp;
}
值傳遞和引用傳遞的區別
這里我們需要注意的是一個方法可以修改引用傳遞所對應的變量值,而不能修改值傳遞所對應的變量值
注意看清楚,這里說的是引用傳遞所對應的變量值
什么意思呢?引用傳遞所對應的變量值是指什么。我們先看下邊兩個例子 "內卷實例"和"反內卷實例"。
Java中到底是引用傳遞還是值傳遞呢?
內卷實例
//內卷
void involution(Student temp){
temp.setScore(100);
}
public static void main(String[] args) {
Student student = new Student();
student.setName("Melo");
student.setScore(0);
System.out.println("躺平時的成績->"+student.getScore());
new TestQuote().involution(student);
System.out.println("卷了幾天后的成績->"+student.getScore());
}
- 這里看起來,好像符合我們引用傳遞的定義誒?
- 對形參temp的修改,會反饋到外部實參student那里去?看起來操作的是同一個變量的樣子
反內卷實例
看下邊這段"反內卷"的代碼實例
//反內卷
void againInvolution(Student temp){
temp = new Student();
temp.setScore(100);
}
public static void main(String[] args) {
Student student = new Student();
student.setName("Melo");
student.setScore(0);
System.out.println("企圖內卷前的成績->"+student.getScore());
new TestQuote().againInvolution(student);
System.out.println("遭受反內卷后的成績->"+student.getScore());
}
- 細心的同學可能發現了,我們這里多了一步操作 --> **temp = new Student(); **
先給出答案吧,Java里邊其實只有值傳遞?為什么這么說
其實我們這里的形參temp,只是拷貝了一份student的地址。可以理解為temp拷貝了這條指針,他也指向了student所指向的對象。
- 也就是說,temp只是跟temp同樣指向了一個對象而已,在第一個例子中,我們沒有去重新修改temp的指向,所以會造成一種假象:我們對temp的修改似乎等價於對student的修改? 其實只是剛好兩個指向了同一個對象而已
- 而如果我們對temp重新賦值了呢, temp = new Student();
- 對temp重新賦值后,此時temp就指向了另一個區域了,后續再對temp修改,根本不會影響原來的student指向的區域
小結
- 所以一個方法可以修改引用傳遞所對應的變量值,而不能修改值傳遞所對應的變量值,這里邊說到的引用傳遞對應的變量值 , 實際上就是那個棧中的student吧(個人見解)。
- 我們這里修改temp,並沒有辦法修改到student。我們temp存放的並不是主調函數放進來的實參變量student的地址(在棧中的地址)
回過頭看前邊引用傳遞的概念:
- 形參相當於是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作為局部變量在棧中開辟了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。
被調函數對形參的任何操作都被處理成間接尋址,即通過方法棧中存放的地址訪問主調函數中的實參變量。
正因為如此,被調函數對形參做的任何操作都影響了主調函數中的實參變量。
- 所以如果這里是引用傳遞的話,那temp應該存放的是main棧中實參變量student在棧中的地址,然后通過間接尋址,可以訪問到實際的對象。
指針傳遞(C語言)
形參為指向實參地址的指針,當對形參的指向操作(不改變其原本指向的情況下)時,就相當於對實參本身進行的操作。
- 拿最老套的C語言手寫swap來講
#include <stdio.h>
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 5;
int b = 8;
//需要傳遞地址
swap(&a, &b);
printf("a = %d\n", a);
printf("b = %d", b);
}
指針和引用區別(11-5補充)
引用傳遞和指針傳遞是不同的,雖然它們都是在被調函數棧空間上的一個局部變量,但是引用傳遞的話,任何對於引用參數的處理都會通過一個間接尋址的方式操作到主調函數中的相關變量。
- 而對於指針傳遞的參數,如果改變被調函數中的指針地址,它將影響不到主調函數的相關變量。
類似於Java里邊讓temp = new Student();
指針實例
#include<stdio.h>
void do_something(int* x,int* y)
{
int x1,y1;
x=&x1;
y=&y1;
(*x)=100;
(*y)=200;
}
int main()
{
int x=1,y=2;
do_something(&x,&y);
printf("the value: x=%d, y=%d\n",x,y);
}
就指針傳遞而言,確實是值傳遞,像這里只是拷貝了指針,形參和實參指向同一個區域
但是拷貝終究只是拷貝,如果修改了形參的指向,那形參和實參就毫無瓜葛了。。
這里類似上邊Java中的反內卷實例,是大同小異的,都是改變了形參的指向后,形參和實參就毫無關系了.
但引用傳遞呢(引用傳遞其實跟指針傳遞不太一樣),比如下邊這個例子:
- 最后輸出的x和y是 100 和 200 ,我們在方法里去修改了形參x和y,外邊的實參x和y也同樣被改變了指向(形參和實參是共存活的,形參就相當於實參)
C++引用傳遞實例
#include<stdio.h>
//引用傳值(跟指針傳值不太一樣)
void do_something(int& x, int& y)
{
int x1, y1;
x1 = 100;
y1 = 200;
x = x1;
y = y1;
}
int main()
{
int x = 1, y = 2;
do_something(x, y);
printf("the value: x=%d, y=%d\n", x, y);
}
總結一下
如果是對引用類型的數據進行操作,分兩種情況,
- 一種是形參和實參保持指向同一個對象地址,則形參的操作,會影響實參指向的對象的內容。
- 一種是對形參改動使得其指向新的對象地址(如重新賦值引用 = new xxx( ) ),則形參的操作,不會影響實參指向的對象的內容。
為什么會有誤區呢?
- 其實還是因為Java中數據類型的問題,基本數據類型看起來就像是值傳遞,而引用傳遞因為存放了地址,讓我們能夠訪問到實參所指向的對象,容易讓我們誤以為我們的形參其實就等價於實參。
最后
- 其實關於Java到底是引用傳遞還是值傳遞這個問題。我們只需要理解好本質就好了,通過上邊的那兩幅圖,理解好本質才是關鍵,萬變不離其宗。