作者LoganChien (簡子翔)
看板b97902HW
標題[計程] 指標 1
時間Sat Dec 13 00:23:52 2008
指標
前言
這一次我們的主題是指標(Pointer),指標有什麼用呢?這是一個攏
統的問題,答案因人而異。
1. 對程序員來說,指標是一個用來操作記憶體的強大工具;
2. 對軟體公司而言,指標是用來判斷一個程序員是不是個人才;
3. 對教授來說,
指標是用來當人的強大工具。
所以你說指標重不重要呢?如果你還沒有搞懂指標是什麼,趕快把這
一篇文讀完。
┌──────────────────┐
│一段故事: │
│ │
│有一天上課的時候,我因為太想睡了,我│
│就放縱自己,小睡片刻,不過在半夢半醒│
│之間我好像聽到: │
│ │
│老師有沒有在課堂上講 │
│指標沒學好你的程式會怎樣 │
│會當掉 會當掉 會當掉 │
│結果咧 │
│你沒有在聽嘛 │
│所以你的計程就被當了嘛 │
│ │
│老師在這裡要再不厭其煩的告訴你 │
│好的指標帶你上天堂 │
│不好的指標令你防不甚防 │
│就這麼簡單嘛 │
│ │
│好 休息一下 下課十分鐘 │
│前十個推文的老師有優待 │
│ │
│以上純屬虛構,如有雷同純屬巧合。 │
└──────────────────┘
指標概觀
我們先想像一下生活中馬路上的路標。
1. 路標會寫有一個地點的位置,例如:台北向北 17 公里,基隆向
北 50 公里,這裡路標指向一個地方。
2. 我們看到路標,我們可以只看看路標上寫什麼。
3. 我們也可以跟著路標走,看看「路標指向的地方」長什麼樣子。
4. 我們可以拿油潻去改路標。
5. 我們也可以跟著路標去,然後在「路標指向的地方」題上到此一
遊。
在 C 語言之中,我們也有類似的工具,我們稱之為
指標(Pointer)。
所有路標有的功能,指標基本上都會有。
1. 指標會存放一個叫做
位址(Address)的東西,會記錄一個變數在記
憶體中的位置。
2. 直接寫變數名就可以知到指標的值。
3. 我們可以用
取值(Dereference)運算子(*)來取得指標指向的變數
的值。
4. 我們可以用指派(Assign)來改變指標的值。
5. 我們在取值之後,可以用指派來改變「指標所指向的變數」的值。
正如地球上每一個點都有其對應的經度、緯度,程式的
每一個變數也
都有一個唯一的位址(Address)。
在 C 語言之中,每一個 byte 都會有一個位址,位址是以 byte 為
最小分割單位。而位址的大小因 CPU 的定址能力、作業系統、編譯
工具…等平台相關的規格而異。在一個 32 位元的平台位址本身就要
用 32-bits 的非負整數來儲存,也就是要用 4-bytes;在一個 64
位元的平台,就要用 8-bytes 的整數來儲存。下面的範例程式碼在
記憶體中的配置情況如右。
位址(Address) 值
int A = 0x11223344; 0x00112223: 0x11 ┐
int *B = &A; 0x00112222: 0x22 │
↑ 0x00112221: 0x33 │
┌──────────┐ │ 0x00112220: 0x44 ┘A ←┐
│備註:這是在 32 位元│ │ 0x00112219: 0x00 ┐ │
│Little Endian 平台可│ │ 0x00112218: 0x11 │ │Point-to
│可能的執行結果,實際│ │ 0x00112217: 0x22 │ │
│結果因電腦而異。 │ │ 0x00112216: 0x20 ┘B ─┘
└──────────┘ (位址僅供參考)
就和路標一樣,指標也可能會有錯。對於路標而言,在你真得走到錯
誤的地方之前你不會知道指標是錯的;對於指標而言,大多數的情況
也是如此,
1. 如果你試圖讀取一個不屬於你的程式的記憶體區塊,你會得到毫
無意義的資料。若作業系統夠好,則會觸發 Segmentation fault
錯誤,之後作業系統會強制中止你的程式。
2.
如果你試圖寫入一個不屬於你的程式的記憶體區塊,你會得到
「本程式即將要關閉」或者是你的程式會莫名的終止(因為你有
可能寫到自己程式的程式碼區)。
加上 C 語言本身的設計,沒有設定初始值時,變數的值可能會是一
個未定義的值,當你定義一個指標而沒有給定初始值時,你是定義一
個不知到指到哪裡的指標。
為了區分哪些是沒有指向東西的指標,哪些是有的,0x00000000 這
一個位址被保留下來了,並且在 C 語言之中被定義為 NULL。我們之
後會再談。
指標的宣告
鬼扯了這麼多,現在就讓我們看看怎麼用好了。在 C 語言中,如果
我們要宣告一個指標,我們是在宣告的時候,
在變數名稱之前多加一
個星號(*)。
int *ptr;
上面我們就宣告了一個可以指向 int 的指標 ptr。
嚴謹的來說 ptr 的型別是 int (Type-of-int),是一個可以指向 int
的變數 (Pointer-to-Type-int)。在這裡我不建議各位把 int * 當
成一個型別。事實上 ptr 的型別是 int,經過星號的修飾,而成為
一個可以指向 int 的指標。我們可以看下面的例子:
int* ptr, var; /* is var pointer? */
上面我們並不是宣告了二個指標,我們
只宣告了一個指標 ptr,另一
個是一個整數 var。從這一個範例,我們可以看出來 int * 本身不
是一個型別[註1]。如果我要讓 ptr 與 var 都是指標,你應該要用:
int *ptr, *var;
或者,你可以用 typedef 來為 int * 取一個別名︰
typedef int *IntPtr;
IntPtr ptr, var;
------------------------------------------------------------
[*註1] 其實這一句是有待商榷的,在 C++ 這一種東西被叫作
Compound Type 複合型別,不過和 C 一樣,你的 * 只會作
用於 * 之後的變數,有興趣者可以自行參閱。
------------------------------------------------------------
在宣告指標的時候,我強烈建議各位
順手就把它初始化,就像我們初
始化一個變數一樣,我們可以用指派運算子(Assign Operator, =)
來指定初始值。
int *ptr = 0;
或者,為了有更高的抽象意義,stdlib.h 定義了一個 NULL,你也可
以這樣寫:
int *ptr = NULL;
值得注意的是 ptr 可以指派 0 是一個例外,如果你指派非零的值,
通常編譯器會發出警告:
int *ptr = 12345;
/* warning: initialization makes pointer from integer without a cast */
為什麼要用 0 或 NULL 呢?如前所述,0 是一個被保留下來的位址,
我們在 ptr 取值(Dereference) 之前,前檢查是不是等於 NULL,如
果是,我們就不應取值;如果不是,則我們可以取值。這是一個安全
慣例,用來確保 ptr 的確有指向某個的地方(但不一定正確,我們
有其他的慣例用來保證正確性)。
如果我們希望初值化的時候就指向另一個變數,我們就可以善用取址
運算子(我們後面會再談)。
int a = 0;
int *ptr = &a;
有時候我們想要向系統要求記憶體,我們可以用 malloc 動態配置。
(我們後面再談)例如:
int *a = (int *)malloc(sizeof(int));
free(a);
取址(Address-of) 與 取值(Dereference)
接著我們來討論
取址(Address-of) 運算子與 取值(Dereference)
運算子。他們的用途分別如下所示︰
取址(Address-of) ``&'' 位址(Address) 值
0x00112223: 0x11 ┐
取得一個變數在記憶體中的位址。
0x00112222: 0x22 │
↑ 0x00112221: 0x33 │
B = &A 情境對話:
│ 0x00112220: 0x44 ┘A
│ 0x00112219: 0x00 ┐
B: 我說 A 呀,你到底住哪裡呀?
│ 0x00112218: 0x11 │
A: 0x00112220。
│ 0x00112217: 0x22 │
│ 0x00112216: 0x20 ┘B
取值(Dereference) ``*'' │ 0x00112215: 0x11 ┐
│ 0x00112214: 0x22 │
取得在特定位址的值。
│ 0x00112213: 0x33 │
│ 0x00112212: 0x44 ┘C
C = *B 情境對話:
C: 我看一下,B 上面寫的地址是
0x00112220,我來去看看。
A: 你來我家有事嗎?
C: 告訴我你家有什麼?
A: 0x11223344
所以我們來看一下 code 的範例:
int i = 0;
int j = 1;
int *ptr = NULL; /* 宣告指標 */
printf("%p", ptr); /* 印出 0 */
ptr = &i; /* 指向 i */
printf("%p", ptr); /* 印出 i 的位址 */
printf("%d", *ptr); /* 印出 i 的值 */
*ptr = 8; /* i = 8 */
printf("%d", i); /* 印出 i 的值 */
ptr = &j; /* 指向 j */
printf("%p", ptr); /* 印出 j 的位址 */
printf("%d", *ptr); /* 印出 j 的值 */
*ptr = 16; /* j = 16 */
printf("%d", j); /* 印出 j 的值 */
const 常數修飾字
在宣告指標的時候,我們可以用 const 來修飾我們的指標。const
的套用規則是:
如果左邊有東西,則 const 會套用在他身上,如果
沒有,則會套用在右邊的的東西。
例如︰
(1) const int *ptr = ...;
這一個是一個可以指向 const int 的指標,ptr 本身可以改動,
但是 ptr 的取值(Dereference)不能改動。
int a = 0;
int b = 1;
const int *ptr = &a;
printf("%d", *ptr); /* Prints 0 */
*ptr = 2; /* Compile error */
ptr = &b; /* Ok, ptr itself is variable */
printf("%d", *ptr); /* Prints 1 */
(2) int * const ptr = ...;
這是一個可以指向 int 的常數指標,ptr 本身不可以改動,但
ptr 的取值可以更改。
int a = 0;
int b = 1;
int * const ptr = &a;
ptr = &b; /* Compile error */
*ptr = 2; /* Ok, dereference of ptr is variable */
printf("%d", *ptr); /* Prints 2 */
printf("%d", a); /* Prints 2 */
(3) int const * const = ...;
const int * const = ...;
這二個都是可以指向常數的常數指標。
int a = 0;
int b = 1;
ptr = &b; /* Compile error */
*ptr = 2; /* Compile error */
(4) const int const * const = ...;
這一個乍看之下和 (3) 一樣,不過會是
Compile error。因為
你重覆使用 const 去修飾 int。
動態配置 malloc/free
有時候我們不會知到我們在編譯的時候不會知道我們要多少記憶體空
間,我們要在執行期才能決定大小,例如我們要寫一個可以讀入無窮
長度的字串、一個很巨大的矩陣、等等。
這時候我們就可以用 malloc 向系統要更多的記憶體。malloc 的函式
原型如下:
void *malloc(size_t size);
我們可以向系統要大小為 size 的記憶體區塊,注意,系統
不一定會
接受你的要求,
如果系統拒絕,就會回傳 NULL。如果接受你的要求,
就會回傳一個位址。不過它是 void 的指標,你還要轉成你要的型別。
char *a = (char *)malloc(256);
↑ ↑
│ └── 256 bytes
└── 轉型到正確的型別。
在用 malloc 之前,你必需要用引入 stdlib.h。
#include <stdlib.h>
然後我們可以用 malloc(要多少 bytes) 向系統要若干 bytes。
char *a = (char *)malloc(256);
上面這一個就是長度為 256 bytes 的字元陣列。不過系統不一定願
意給我們,我們要用 if 來檢查。
if (a == NULL) {
printf("ERROR: unable allocate.\n");
} else {
/* Allocated Successfully! */
}
如果你是要用 int,我們可以善用 sizeof。
int *b = (int *)malloc(sizeof(int));
b 就會是指向一個配置好的整數。
不過,malloc 有一點像是向系統借空間,有借要有還,所以我們要
用 free 把要來的空間還回去。
free(b);
b = NULL; /* 把 b 設為 0 */
指標安全使用手則
1. 初始化的時候一定要順手給定初始值。
2.
取值之前,檢查是不是 NULL,如果是則不應取值。
3.
當一個指標指向的東西被 free 或 指向失效的變數時,應該要重
設成 NULL。
後記
我們在這一篇文學到了指標的基本使用,從指標的基本概念、到取址
取值、const 修飾字、到 malloc/free。希望對大家有幫助。切記:
好的指標帶你上天堂!
---------
我循著一條細長的線不斷找尋,
有時我會遇到一些人,但他們只能給我下一站的方向,
有時我會遇到一些事,但它們只是通往目標的考驗,
有時我會發現我已經走到線的另一端,卻撲了空!
我會退後一步,說不定我忽視了另一條人跡罕至的小徑。
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 140.112.241.166
推 benck:太強大了 12/13 00:27
推 dennis2030:感謝!! 對觀念很有幫助!! 12/13 01:06
→ dennis2030:之前因為不熟 一直能不用就不用 現在一定要用就頭痛 12/13 01:07
→ dennis2030:了 不過看完這篇以後有如神助阿XD 12/13 01:07
推 dennis2030:話說 學會寫詩是成為強者的其中一個條件嗎XD? 12/13 01:16
推 jimmy319:推 12/13 17:32
推 jimmyken793:推 指標這塊以前幾乎都沒碰... 12/13 21:10
推 hrs113355:推 12/14 00:05
推 chopssin:推 很有幫助+1 12/15 03:06
→ crystal0825:太感謝了~~~ 12/16 00:55