C語言中取地址符&做函式形參?—— 引用的討論

Roninwz發表於2017-09-13

取地址符&做函式形參?

C語言強大且危險


引入

這個問題花去了整整一天的研究。

先看一段嚴蔚敏的《資料結構》中棧的例程:

棧的壓入

這裡面的&S第一遍看的時候想當然的認為是取了SqStack結構體的S的地址,沒有細想。然後又看到了這句。

// platform: VC++6.0
Status Pop(SqStack &S, SElemType &e); //取地址符?
  • 1
  • 2

我開始突然發現,這真的是取地址符嗎,對照了我自己寫的程式,仔細推敲發現不太對。 
仔細看這裡的&e,如果這是個整型的棧,那麼SElemType就是int,那麼這裡就等於:

Status Pop(SqStack &S, int &e); //很奇怪
  • 1

類比的疑問

我們都知道:

int *a,*b; /* 定義了兩個整型的指標 */
int **a, **b; /* 定義了整型指標的指標 */
  • 1
  • 2

那麼難道說是

int &e; // 定義了以一個整型數為地址的變數e?
  • 1

仔細看下接下來的函式定義:

這裡寫圖片描述

顯然這裡可以看出由於top指標指向的是SElemType型別,所以e是SElemType型別的。所以以上類比顯然是不對的。


C/C++中的引用引數

查詢了很多的資料發現,這個實際上是C++裡的形參符號,必須要在跟在資料型別的後面使用。在函式內部對形參的操作都等同於直接操作原變數。

先說形參和實參

學過C語言的都知道,一個經典的例子是關於寫一個交換兩個變數a,b的值的函式:

// “形參不等於實參”的經典錯誤示範
void swap(int a, int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

void main()
{
    int a = 1, b = 2;
    swap(a,b)l
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

我們都知道把a,b作為形參傳入時,會臨時的分配形參空間讀取實參a,b的值存入,這裡的形參a,b實際地址是不同於原來的實參。

形象的說,實參a是一份講義,你在呼叫函式的時候,函式就像學生一樣去要講義(傳遞的實參)。函式向系統要了張白紙(棧區空間),然後把這篇文章抄了一份拿去用了,取名也叫作a。然後他怎麼修改都不會

繼續準確點說, 在程式執行的時候會分配一個全域性區,我們這裡說的a,b實際上屬於全域性變數,儲存在全域性區,也有的地方叫做靜態區。而這裡的形參儲存在棧區,僅僅是儲存了全域性量的值,所以所有對形參a,b的操作都和靜態區的a,b無關。

這裡實參傳遞給形參的過程叫做值傳遞。

附:C/C++程式的記憶體分配知識

一個由C/C++編譯的程式佔用的記憶體分為以下幾個部分 :

1、棧區(stack)― 由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。 
2、堆區(heap) ― 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收 。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。這個空間是公用的,如果沒有釋放會使得可用堆區空間變小,最好在申請後手動釋放。 
3、全域性區(靜態區)(static)―,全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。 - 程式結束後由系統釋放 
4、文字常量區 ―常量字串就是放在這裡的。 程式結束後由系統釋放 
5、程式程式碼區―存放函式體的二進位制程式碼。 
所以我們可以理解為,這裡的&e是為了說明e變數不是僅僅的把值傳遞進了函式內部。

那怎麼通過函式操作函式外部的引數呢? 
根據C語言學習中標準解法,一是將實參的地址傳遞進函式中函式中,通過地址直接操作原變數的值;二是利用函式本身的返回

// 利用指標的經典解法
void swap(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

void main()
{
    int a = 1, b = 2;
    swap(&a,&b);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

必須先弄清楚各種地址

要理清引用引數的使用和原理,明白這個&符號和指標的區別,先必須搞清楚資料的地址。

在《作業系統》中,可以得知三種地址的區別:邏輯地址、線性地址和實體地址。 
關於這三者的區別可以看這裡:

作業系統邏輯地址、線性地址和實體地址 
http://www.cnblogs.com/dirichlet/archive/2011/03/18/1987746.html 
或者這裡 
http://blog.csdn.net/geekwill/article/details/52449855

這裡用圖簡單通俗的說下,為了通俗易懂,嚴格上並不準確:

這裡寫圖片描述

我們的程式在作業系統中執行的時候,會給我們的程式(程式)在記憶體中分配一些空間。為了方便說明,這裡假設記憶體是16位地址(實際上32位地址支援4G記憶體),我們可以看到a的實體地址是0x23。

然後0x2300是什麼呢,這個是程式資料段的首地址,一般我們習慣叫做程式執行的入口地址

像上面的圖所示,我們通過&a把a的邏輯地址傳遞進了函式swap中,然後swap函式通過*a找到a的實體地址,這個是作業系統完成的,其中會經過一些過程,需要先變換為線性地址。

那麼我們可以總結:

實際上在C語言中,使用&取地址符取出的是變數的[邏輯地址],就算用匯編進行操作也是一樣。變數的實體地址只有作業系統知道,實際上邏輯地址和實體地址都是32位整數(32位機)。兩個不同程式,就算邏輯地址一樣,實際的實體地址也不同。

這裡關於各種變數的記憶體地址相關可以參考:

C語言記憶體地址基礎 
http://blog.jobbole.com/44845/

關於C語言的函式呼叫過程更加深度嚴謹(也更難懂)的知識,牆裂推薦這篇文章:

深入理解C語言的函式呼叫過程 
http://blog.chinaunix.net/uid-23069658-id-3981406.html

通過引用傳遞和通過指標傳遞?

之前的兩個例子,分別用常規的值傳遞和指標的傳遞實現資料交換的過程看起來不同,其實都是差不多的。實質上都是值傳遞

第一個例子的執行過程:

這裡寫圖片描述

第二個例子的執行過程:

這裡寫圖片描述

可以看出實際上利用指標的方法也只是把a,b的邏輯地址作為一個整數通過值傳遞到形參裡儲存起來了,值傳遞的內容是a,b的邏輯地址。這兩種方式都需要額外的開闢棧區的記憶體,而且指標操作是易錯且不安全的。

下面是通過引用引數完成的交換過程。

// 引用引數實現交換
void swap(int &a, int &b){
    int temp;
    temp = a;
    a = b;
    b = temp;
}

// Using main to test
void main(){
    void swap(int&, int&);

    int a = 1, b = 2;
    swap(a,b);
    printf("%d %d\n",a,b);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

有些文章說道,通過引用的方式傳遞給函式的是變數的地址,這種方式叫做地址傳遞方式,還提到這是和“值傳遞”十分不同的方式。 
有些書說道:“引用實際上是取了個‘別名’” 
還有的書和文章說道引用是比通過指標傳遞更加高效的方式,因為不需要開闢新的記憶體空間用來拷貝實參的地址。 
真的嗎?

引用實現原理的討論

先討論引用實現的系列文章,大佬們講得比較透徹,而且論據豐富。

c++中的引用的使用原理以及使用例項 
http://blog.csdn.net/ztz0223/article/details/1639305 
C++ 引用 引數傳遞 機制【強烈推薦】 
http://blog.csdn.net/huqinweI987/article/details/50769096 
C++引用的本質與修改引用的方法 
http://blog.csdn.net/huqinweI987/article/details/24874403 
舉例剖析C++中引用的本質及引用作函式引數的使用 
http://www.jb51.net/article/80911.htm

如果不想看乾貨長文的就看下下面的通俗簡短討論吧。

我們看下下面這段小程式:

int i = 0;
int &j = i;     // 設j為i的一個引用
j = 10;         // 將j試著改變為10
int *p = &j;    // 觀察引用的取地址操作
  • 1
  • 2
  • 3
  • 4

彙編(偽彙編)解析如下:

;int i = 0;
mov dword ptr [i],0;    // i的內容置為0;

;int &j = i;
lea  eax, [i];          // 將i的地址放入eax暫存器
mov  dword ptr[j],eax;  // 將i的地址傳入j的內容

; j = 10;
mov eax, dword ptr[j];  // 取j的內容(i的地址)放入eax
mov dword ptr [eax], 0Ah; // 將eax地址指向修改為10;

;int *p = &j;
mov eax,dword ptr [j]     // 將j的內容傳給eax
mov dword ptr [p],eax     // 把eax內容傳入p的內容
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

實際上,通過對彙編的分析可以看出:

  1. “引用是一個別名”的說法並不準確,實際上實現過程中引用也可以看成是一種指標實際上引用變數儲存的就是引用物件的地址,也要佔用記憶體空間(和指標佔用大小不同),只不過C++的標準規定了引用初始化完畢之後,對引用的操作就等於是對實際物件的操作

  2. 雖然引用可以看做特殊的指標,對引用的操作會被編譯器解釋成對地址指向的目標的操作。但和*p這種取指標指向物件的方式不同,這種方式不會開闢臨時空間儲存指標指向的物件。如果指向物件很大,操作重複數很多,這個差異就會對效能有十分大的影響。

  3. 引用的本身值,即引用物件的地址不可以像指標變數一樣修改,對引用的操作只會解釋成對引用物件的操作,可以理解引用變數是一個靜態的指標

對第2條的解釋,關於指標操作拷貝副本和引用節省空間的詳細解釋可以看上面的文章—— C++ 引用 引數傳遞 機制【強烈推薦】


轉載來自:http://m.blog.csdn.net/JayRoxis/article/details/73060770

相關文章