一直以為對於參照型別做為引數在方法呼叫時加不加 ref 關鍵字是沒有區別的。但是今天一偵錯蹤了一下變數記憶體情況才發現大有不同。
直接上程式碼,結論是:以下程式碼是使用了 ref 關鍵字的版本,它輸出10;如果不使用ref 關鍵字則輸出 1,2,3
一些說明:
0x000000c088d7e3d0 //表示這個變數在記憶體中的地址
*&myArray: 0x0000029b3f84ae00 //表示這個變數指向的記憶體空間的物件的地址
方法一:
在即時視窗輸入取地址符+變數名如 &a 這是會輸出如下 兩行:
0x000000325637e570
*&a: 0x00000209ba0dad58
第一行 0x000000325637e570 代表變數本身的記憶體地址,第二行 *&a: 0x00000209ba0dad58 表示變數指向的物件的記憶體地址
方法二:
【偵錯】-【視窗】-【記憶體】-從列出來的4箇中選一個,然後會調出記憶體檢視視窗。在記憶體檢視地視窗中的【地址】裡輸入[取地址符]+[變數名]如 &a ,這時地址中的&a會變成變數的十進位制表示的記憶體地址,如:0x000000325637E570
補充幾張偵錯中斷在不同語句時的一些記憶體情況截圖:(加上ref關鍵字後的參照型別傳參情況圖)
1.
2.
3.
4.
2022-07-31再次總結:
我們知道,不論值型別還是參照型別,記憶體儲存單元中的資料是依靠儲存單元地址來存取的。
對於值型別資料的記憶體模型就是直接把值放在記憶體單元裡,需要存取值時直接用記憶體地址就能獲取這個地址中儲存的資料了。這個模型直觀而簡單很好理解。
而參照型別的記憶體儲存模型是由棧記憶體+堆記憶體的結構共同實現的。具體細節就是:參照型別變數的資料內容(命名為content)放在堆記憶體(我們給這個堆記憶體地址一個名字叫H),然後還需要有一個棧記憶體(再把棧記憶體地址命名為S),這個地址為S的棧記憶體裡存放的值就是H,是的 就是堆記憶體的地址,這樣就要存取content就需要先存取S,得到S中的內容才得到了地址H,最後才能存取到H地址裡的內容content。基於S中存放的值是另一個記憶體地址而不是資料內容本身的原因,所以人們常把S及其值叫做指標(參照型別資料使用的正是這種間接存取資料的設計模型)。
接下來是在C#語言的方法中,對傳遞參照型別引數的設計及實現細節的說明。
先不考慮ref關鍵字,對於方法的參照型別引數,其在方法接收外部變數時的接收細節是這樣:方法內部會建立一個新的棧記憶體也就是個指標,其記憶體單元就是用來接收那個外部傳進來的變數所在的堆記憶體的地址,採用這樣的方式來實現對外部變數的接收也就是說本質上是傳遞堆記憶體地址。但是注意,外部變數原來那個棧記憶體指標也指向同樣的堆記憶體。即在方法內部和外部這兩個指標都指向同一塊堆記憶體但這兩個指標各是各,是不同的棧記憶體地址。基於這種設計,我們可以看出,在前面的範例中,如果不使用ref關鍵字,則在SetArray方法內部 array=new int[]...這行指令實際上是先按new int[]指令建立了一個新的堆記憶體(放新的陣列),然後把指標array的儲存的值(賦值前它是原堆記憶體地址)更新為新的堆記憶體的地址,那麼原堆記憶體地址在方法內部也就無法再存取了,而且這個地址值更新的程序也與方法外部的指標myArray無關,即方法外部的myArray依然指向它原先那個堆記憶體地址。
最後,我們來考慮ref關鍵字。一個方法的參照型別引數使用 ref 關鍵字後會使得在方法在接收外部變數時改變預設傳遞引數的行為。具體表現就是加上ref後,在方法內部不再在棧記憶體上建立一個新的指標用來接收外部變數其堆記憶體的地址,而是直接使用外部變數的指標,等於把外部變數的指標本身給傳進來了。
關鍵歸納:不加ref傳外部變數堆記憶體地址;加上ref傳外部變數的棧記憶體地址即指標地址本身。
我覺得這次總結的還不錯,希望對您有所幫助。也希望自己不要再忘記這些關鍵的知識點了。