這問題被問蠻多次,類似的問題與回答在 另一份 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
依此類推。
本文敘述至此,由於吾人所看之書本,較少以畫圖方式詳解,於此觀念上盡以圖解方式說明,其它觀念仍需再配合書籍參考,方可建立完整概念。
留言列表