close

這問題被問蠻多次,類似的問題與回答在 另一份 blog 裡有提到一點,

裡面提的東西也較多,唯所撰內容過於繁雜,於此盡量「化繁為簡」。

以下之說明基於以下前提假設:

(1) sizeof(int) = 4 。
(2) 系統為二補數表示法。
(3) sizeof(pointer) = 4,意即指標大小為 4 bytes。

同時 address 一律以16進制示之,且只以單層指標、一維陣列為簡略說明。這裡的說明完整性,絕對比不上一本好書,只是盡力幫忙建立一些觀念。

1. 記憶體與變數

在程式碼中,任何一程式「通常」會宣告出許多變數,都是存放在「記憶體」裡面。比如,宣告一個 int a;,這個動作只是告訴作業系統:「幫我在記憶體裡面先配好一個整數的空間出來」,這時在記憶體裡面可能長這樣

(1) int a; 

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

 

 

value

 

????

 

 

 

 

 至於 value 會打上 ????,是只有「宣告」,而沒給賦值。至於位置為什麼會是0x40000004 ? 這一般的 coder 可以不用管,系統要配到哪個位置我們不在意,這裡的 0x40000004 也只是做假設之用,也就是假設系統是為整數變數 a 配到 0x4000 0004 此位置。若接下來進行「賦值」的動作

(2) a=10;

address(hex)

0x40000004

variable

a

value

0000 0000 0000 0000 0000 0000 0000 1010

明明是將 a 設成整數 10,為什麼會是一堆 0/1 ?簡單的說,電腦裡面存的永遠只有 0/1 而已,所有的處理都是以 0/1 在處理。那些印出來的英文字母、阿拉伯數字,全部都是再經過一些處理後顯示出來的 (較正確的說法是,在 Console 視窗裡面 "畫" 出來)。

為方便說明,日後直接做這種表示

a=10;

address(hex)

0x40000004

variable

a

value

10

 代表 a 之內容是十進制的 10,但還是要有觀念存在 : 實際上是以 2 進制去存。


2. 納入指標之概念 - 「依址取值 *」

* 用「依址取值」解釋其實不是很好,下面會再解釋為什麼這麼翻不好。先記得一件事:指標 "存" 的東西,永遠是記憶體的位址值。 C/C++ 中,對於指標之宣告語法大致如此

(1) int *p;

這代表的是,叫作業系統在記憶體裡配置一個「存位址」的大小的東西,所以至少可以明白的第一件事情是:所有指標大小都是一樣的,因為要存的是位址,所以用一定的大小去存就可以 (可能是 32 bits, 也可能是 64bits, 看系統,此處是假設用 32bits 存指標)。繼續延伸第一點的部份,在記憶體裡面,長成這樣

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

 

value

 

10

 

 ????

 

(2) p = (int*)0x40000010; //Dangerous !!! Be Care

剛上面有說了, p 是存「位置」的東西,但真也不是像上面這麼用。

假設我要做的事情是:讀取記憶體位置 0x4000 0010 之值,這時候我便將指標 p ,存入該位址值,

若直接寫 p = 0x40000010,這是將 p 看成整數,而不是看成指標,於是進行強制轉型的動作,將 0x40000010 解釋成指標 (指向一整數之指標)

p = (int*) 0x4000 0010;

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

???? 

value

 

10

 

 0x40000010

 ????

將 p 硬是記住 0x40000010 這個位置。接下來,我想看 0x40000010 這個位置裡面放的是什麼東西的話,就用 * 這個東西去取出來 ( "依址取值" ,依變數存的位址,去那個位址取出值出來)。

cout << *p << endl;  // printf("%08x\n", *p);

嗯,到這裡通常是會出現錯誤,跟你說 0x40000010 該記憶體不能為 read。為何?

*p 這個動作,分解大致如下:(1) 先去看 p 的內容存的是哪個位址,這裡存的是 0x40000010 (2) *p, 再到 0x40000010 這個位址,去讀取此位址的值為何。

關鍵就在 (2) 的部份,因前提是用硬用「暴力」的方法,讓 *p 指向 0x40000010 這位址,在執行動作 (2) 去讀記憶體位址,但那個記憶體位址 (也就是 0x40000010) 可能是被作業系統保護,不能讀,也不能寫。指標真不是這麼用的,這樣很有問題。

 


 

3. 納入指標之概念 - 「取得變數位址值 &」

有了「依址取值」之概念後,再談談「取得變數位址值」。

先考慮以下程式碼及假設之記憶體配置圖

int a = 10;
int *p;

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

 

value

 

10

 

 ????

 

接下來我想,將 p 的內容,放 「 變數 a 的位址值」,這時就是 「對變數取位址 &」,程式碼及記憶體結果如下所述

int a=10;
int *p;
p = &a;

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

 

value

 

10

 

0x40000004

 

 

上面重點部份都用彩色標出來了,分解動作大概是 (1) 將 a 的位址取取出來 : &a (2) 再將這個位址值給 p (也就是讓 p 記錄 a 的位址值) : p = &a;

嗯,好了,接下來可以用 p 去讀取 a 的值,程式碼如下

cout << *p << '\n' ; // printf("%d\n", *p);

結果將會輸出 10。在上一段曾提到: * 這東西就是「依址取值」,事實上這不是非常正確的說法,因「依址取值」是只有 讀取作用而已,但實際上它也可用來寫入。「間接」改變 a 的內容時,寫法便如下所述

*p = 5;

分解步驟大概二個。

(1) 先去看 p 的位址值是在哪,結果是 0x40000004

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

 

value

 

10

 

0x40000004

 

 

(2) 將 5 這個數字,寫到 p 指向的位址,也就是 a 的位址裡面去。

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

a

 

*p 

 

value

 

5

 

0x40000004

 

 

事實上,* 在 C/C++ 中,有個專屬之動詞 (還是名詞 ? 我覺得是動作,所以納到動詞),叫 dereference,中文叫「提取」,這解釋比剛剛說的「依址取值」貼切太多了,因「提取」有可能是為了讀取,也可能是為了寫入。

以上,為指標概念基礎,看不懂、不熟,多看,或再找書補充 (可能是我的表達方式不好),接下來一系列是在講  函式引數傳遞  的問題。

 


 

4. call by value / pass by value

考慮以下程式碼

void f(int sub_a)
{
    sub_a = 10;
}

int main()
{
    int main_a=5;
    f(main_a);
    cout << "main_a=" << main_a;
    return 0;
}

答案會等於多少?

在探討答案前,大致細說記憶體是怎麼跑的。首先, int a = 5 時,系統會為變數 a 配一個大小為 int (4bytes) 之記憶體空間,並給它初始值 5,記憶體大致如下

int main_a=5;

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

  

 

value

 

5

 

  

 

當呼叫 f(main_a) ,進入副函式時,實際上在副函式裡面,系統會再配置另一個位址給 sub_a。

f(main_a);

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 sub_a

 

value

 

5

 

  

 

同時因為在主程式裡面,傳進的是 main_a 當初始值,所以事實上一開始在副函式裡面的 sub_a 初始就是 5  (言下之意,上一張的狀態應該是不存在的,直接跳到這個狀態底下)

f(main_a);

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 sub_a

 

value

 

5

 

 5

 

接下來,再副函式裡面,將 sub_a 改成 10

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 sub_a

 

value

 

5

 

10

 

最後,副函式結束的時候,sub_a 的部份被系統整個收回去

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 ????

 

value

 

5

 

????

 

於是回到主函式 main 裡面時,再用輸出 main_a ,變數值和原本的一模一樣。因為從頭到尾,記憶體操作都不是同一個地方。

上面變數用 main_a 與 sub_a ,是方便說明與觀查用,實際上 主函式之變數名稱 與 副函式之引數名稱 相同,其實動作都一模一樣,都是在不同記憶體底下操作,所以絕不會改到 主函式裡面的 main_a

所以,main  裡面的輸出,最後還是 5 ,不會是 10。


5. call by pointer / pass by pointer

可能有部份原文書是寫 call by address、pass by address,但我認為用 pointer 比較不容易誤解,同時這部份中文翻譯也很有爭議,有些人認為翻「傳址呼叫」是傳神的;有些人認為翻「傳址呼叫」是誤解觀念的,這是我認為這部份不要翻譯較好的原因。

考慮以下範例碼

void f(int *p)
{
    *p = 10;
}

int main()
{
    int main_a=5;
    f(&main_a);
    cout << "main_a=" << main_a;
    return 0;
}

看到這裡應該會有十萬顆問號,但,若本文前面四點都知道的話,接下來要解釋真的不是難事。

int main_a=5;

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 

 

value

 

5

 

 

 

接下來是關鍵,一次看二個地方,

void f(int *p){......}
f(&main_a);

在副函式  f 中,一開始就明確定義,我要接受的是「一個指標」,指標要存的是一「位址值」,也因此,在 main  裡面加上一個 & ,對 main_a 取得位址值。這時記憶體內容大致如下

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 (副函式裡) *p

 

value

 

5

 

 0x40000004

 

再來將焦點放在副函式 f 裡面

void f(int *p)
{
    *p = 10;
}

這段在上面有說過了, *p = 10,就是把 10 這個常數,「放在 p 所指的位址裡」。現在 p 裡面存的位址是 0x40000004,所以 *p = 10,便是把 10 此常數放到 0x40000004 裡面 (注意到,這個 0x40000004 就是 a 的位址),記憶體位址圖便如下

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 (副函式裡) *p

 

value

 

10

 

 0x40000004

 

最後,副函式執行完畢時,關於 p  的東西全都被系統收回去

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

???

 

value

 

10

 

 ???

 

最後的結果發現,主程式裡面的變數,真的被改成 10 了。這也是和 call by value 最大不同點之一,一個會更改傳入的變數值,一個不會更改傳入的變數值。

 


 6. call by reference / pass by reference  (&)

reference 是 C++ 才有的東西,在 C 語言裡面沒有;

故call by reference 也是 C++ 才有的東西,在 C 語言裡面沒有。

reference 在大多的書上,都是形容它是 " nickname",簡單的說,就是同一個東西,用另一個稱號去叫它。

比如說:

int edisonx = 10;
int &handsome = edisonx;

在第一行 int edisonx = 10; 裡面,記憶體位址長這樣

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

edisonx

 

 

 

value

 

10

 

 

 

在第二行 int &handsome = edisonx; 時,並不會為 handsome 配置任何一個記憶體位址值,因 handsome 只是 edisonx 之別名 (nick name),硬要再畫記憶體圖時,大概是長這樣吧

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

handsome

 

 

 

value

 

10

 

 

 

reference

 

edisonx

 

 

 

 簡單的說,edisonx 與 handsome 是一樣的東西。 (雖然有點不要臉,不過上面的邏輯就是這樣)

於是接下來,對 edisonx 做讀寫,與對 handsome 讀寫之效力是一樣的。

#include <iostream>
using namespace std;
int main()
{
    int edisonx = 10;
    int &handsome=edisonx;
    cout << "edisonx=" << edisonx << '\n'; // output 10
    cout << "handsome=" << handsome << '\n'; // output 10
    handsome = 20;
    cout << "edisonx=" << edisonx << '\n'; // output 20
    cout << "handsome=" << handsome << '\n'; // output 20
    return 0;
}

 

reference 的東西不多說,要注意的地方很多,這部份要再翻其他的書做後備補充。接著直接考慮以下程式碼

void f(int &p)
{
    p = 10;
}

int main()
{
    int main_a=5;
    f(main_a);
    cout << "main_a=" << main_a;
    return 0;
}

這裡就沒什麼太多好說的部份,一開始  main_a = 5 時

address(hex)

0x40000000

0x40000004

0x40000008

0x4000000B

0x40000010

variable

 

main_a

 

 

 

value

 

5

 

 

 

使用 main_a 傳給 f(int &ref) 時,會將 ref 直接參照在 main_a 上,記憶體大致如下

address(hex) 0x40000000 0x40000004 0x40000008 0x4000000B 0x40000010
variable   main_a      
value   5      
reference   ref      

於是 f(int &ref) { ref = 10; } 的時候,會直接更改該位置上之值

address(hex) 0x40000000 0x40000004 0x40000008 0x4000000B 0x40000010
variable   main_a      
value   10      
reference   ref      

最後 f 執行完畢,回到 main ,nick name (ref) 消失

address(hex) 0x40000000 0x40000004 0x40000008 0x4000000B 0x40000010
variable   main_a      
value   10      

 


 

7. 傳遞陣列 / 指標

先想一下,陣列長怎樣

int a[5];
a[0] = 0, a[1]=1, a[2]=2, a[3]=3, a[4]=4;

address(hex)

0x0000

0x0004

0x0008

0x000B

0x0010

variable

a[0]

a[1]

a[2]

a[3]

a[4]

value

0

1

2

3

4

 在讀取陣列元素時,實際上可以有不同的方式。

雖 a 本身為一陣列,但若用指標來表示其陣列元素的話,
第 0 個元素可以表示第 *(a+0),也就是在記憶體位址 a 的地方,移 0 個 int 大小,也就是 a[0] 之意;
第 1 個元素可以表示第 *(a+1),也就是在記憶體位址 a 的地方,移 1 個 int 大小,也就是 a[1] 之意;
第 i 個元素可以表示第 *(a+i),也就是在記憶體位址 a 的地方,移 i 個 int 大小,也就是 a[i] 之意。

 記憶體可這麼看

address(hex)

0x0000

0x0004

0x0008

0x000B

0x0010

variable

*(a+0)

*(a+1)

*(a+2)

*(a+3)

*(a+4)

value

0

1

2

3

4

 

接著討論函式傳遞一維陣列的問題。在這種函式裡面

int show_array(int array[], int Size)
{
    for(int i=0; i!=Size; ++i) cout << array[i] << ' ';
}

事實上那紅色引數 int array[] 即相當於 int *array,也就是實際上在傳遞的時候,是傳一個「指標給它」,
於是紅色部份也比較不建議寫成 int array[],反而比較建議寫成 int *array,避免誤會。

考慮以下程式碼

#include <iostream>
using namespace std;

void show_array(int *a, int Size)
{
    for(int i=0; i!=Size; ++i){
       cout << a[i] << ' ';
    }
    cout << '\n';
}

int main()
{
    int a[5];
    a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4;
    show_array(a, 3);
    return 0;
}

 一開始在 main  裡面的配置如下

address(hex)

0x0000

0x0004

0x0008

0x000B

0x0010

variable

*(a+0)

*(a+1)

*(a+2)

*(a+3)

*(a+4)

value

0

1

2

3

4

 

 在 main 裡呼叫 show_array(a, Size) 時,實際上第一個引數傳的就是「變數 a 的位址值」,
所以在進入副函式時,記憶體會再配一個記憶體空間存變數 a 的位址值,另一個變數存Size,大致如下

0x0000

0x0004

0x0008

0x000B

0x0010

….

0x1000

….

0x1010

*a

*(a+1)

*(a+2)

*(a+3)

*(a+4)

 

(sub) *a

 

Size

0

1

2

3

4

 

0x0000

 

3

 

接下來在副函式裡面要印出 a[0] 時,就先去看 *a 在哪裡 -> 找到 0x0000,印出內容 0
要印出 a[1] 時,從 0x0000 移一個 int 大小(4bytes),到 0x0004 ,印出內容1,
要印出 a[2] 時,從 0x0000 移二個 int 大小(4bytes),到 0x0008,印出內容2,依此類推。


再小改一下,看記憶體怎麼跑的。

#include <iostream>
using namespace std;

void show_array(int *a, int Size)
{
    for(int i=0; i!=Size; ++i){
       cout << a[i] << ' ';
    }
    cout << '\n';
}

int main()
{
    int a[5];
    a[0]=0, a[1]=1, a[2]=2, a[3]=3, a[4]=4;
    show_array(a+1,3);
    return 0;
}

事實上這和上面的分析都很像,唯一不同地方在於,
傳入副函式時,記的位置是 a+1 ,也就是 a[1] 的位址。

0x0000

0x0004

0x0008

0x000B

0x0010

….

0x1000

….

0x1010

*a

*(a+1)

*(a+2)

*(a+3)

*(a+4)

 

(sub) *a

 

Size

0

1

2

3

4

 

0x0004

 

3

 

 
至於其他的分析都一模一樣,

先去抓 a 位址,0x0004,印出其內容,輸出 1
再抓 a+1 位址,即 a 移一個 int 大小,0x0008,輸出 2
再抓 a+2 位址,即 a 移二個 int 大小,0x000b,輸出 3
依此類推



本文敘述至此,由於吾人所看之書本,較少以畫圖方式詳解,於此觀念上盡以圖解方式說明,
其它觀念仍需再配合書籍參考,方可建立完整概念。

 

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 edisonx 的頭像
    edisonx

    Edison.X. Blog

    edisonx 發表在 痞客邦 留言(2) 人氣()