第6課:C的指標

指標是一個非常強大的程式設計工具,他們可以讓事情變得更簡單,幫助改善你的程式效率,甚至讓你處理無限量的資料,例如,使用指標的一個方式就是用函式修改傳給它的變數,也可以用來動態配置記憶體,這表示你可以寫程式在系統執行時處理幾近無限量的資料–在你寫程式時,你不需要知道你需要多少記憶體,哇,真是非常酷,ㄚ琪會在接下來的教學中介紹,至於現在,就讓我們了解一下指標的基本操作,以及你如何來使用它們。

什麼是指標?為什麼你應該在乎這個東西?

指標最貼切的名字:他們”指向”記憶體中的位置,想想看銀行裡一排各種不同尺寸的保險箱,每個保險箱都有一個編號與其關聯這樣你才可以快速尋找,這些數字就像是變數的記憶體位置,在保險箱的世界裡指標就是儲存另一個保險箱編號的東西,有可能你有個富有的叔叔他把貴重物品存在保險箱中,但是決定把其真實的位置放在另一個更小的保險箱中,這個保險箱就只放張有金銀珠寶的保險箱卡片編號而已,有卡片的這個保險箱可以儲存另一個保險箱的位置;它跟指標觀念一樣,在電腦的世界中,指標就只是儲存記憶體位址的變數,通常就是另一個變數的位址。

很酷的一件事就是一旦你用了這個變數的位址,你就能到那個位址來擷取存在那的資料,如果你碰到一塊大量的資料你想要傳遞到函式裡,傳遞位置到其函式比複製每個資料元素簡單多了!此外,如果你的程式需要更多的記憶體,你可以從系統請求更多的記憶體–如何取”回”那個記憶體?這個系統會告訴你記憶體的位置;也就是說,你可以取回記憶體位址,而你需要指標來儲存記憶體位址。

關於這個術語的說明:指標這個字可以指他自己的記憶體位址,也可以指儲存記憶體位址的變數,通常,這差別不是真的很重要:如果你傳一個指標變數到函式裡,你就是在傳存在指標裡的變數–記憶體位址,當我在說記憶體位址時,我說的就是記憶體位址;而當我想要一個變數儲存記憶體位址,我就會說是指標,當一個變數存了另一個變數的位址時,我會說它”指向”那個變數。

C指標語法

當你有了指標的時候,指標需要一些新的語法,你需要有能力來請求儲存的記憶體位置以及儲存在那個記憶體位置的值,此外,由於指標有些特別,你需要告訴編譯器當你宣告指標變數的時候,該變數是一個指標,以及告訴編譯器它指向的記憶體型別。

指標的宣告看起來像這樣:

<變數型態> *<變數名稱>; 

例如,你可以宣告指標儲存整數的位址,語法如下:

int *points_to_integer;

注意*的使用,這是宣告指標的關鍵;如果你在變數名稱前直接加入,那就是宣告變數是指標,小小的問題:如果你在同一行宣告多個指標,你必須在每個變數前加一個星號:

/* 一個指標,一個整數 */ 
int *pointer1, nonpointer1; 
/* 兩個指標 */ 
int *pointer1, *pointer2;

正如我所提到的,有兩種方法可以使用指標存取資訊:可以讓它將實際位址提供給另一個變數。為此,只需使用不帶 * 的指標名稱即可。但是,要存取實際的記憶體位置和儲存在那裡的值,請使用 *. 這樣做的技術名稱是提領(dereferencing)指標;本質上,您正在引用某個記憶體位址並追蹤它,以檢索實際值。追蹤何時應添加星號可能很棘手。請記住,指標的自然用途是儲存記憶體位址;所以當你使用指標時:

call_to_function_expecting_memory_address(pointer);

然後它計算出該位址。您必須新增額外的內容(星號*)才能檢索儲存在該位址中的值。你可能會經常這樣做。儘管如此,指標本身應該儲存一個位址,因此當您使用裸指標時,您會得到該位址。

指向某物: 取得位址

為了讓指標實際指向另一個變數,也必須擁有該變數的記憶體位址。 若要取得變數的記憶體位址(在記憶體中的位置),請將 & 符號放在變數名稱前面。 這樣就可以給出它的位址。 這稱為取址運算子,因為它會傳回記憶體位址。 方便的是,這個和(&)的符號和取值的英文字母都是以 a 開頭; 對外國人來說這可能是記住使用 & 獲取變數位址的有用方法。

例如:

#include <stdio.h>
 
int main()
{ 
    int x;            /* A normal integer*/
    int *p;           /* A pointer to an integer ("*p" is an integer, so p
                       must be a pointer to an integer) */
 
    p = &x;           /* Read it, "assign the address of x to p" */
    printf("Please enter a number: ");
    scanf( "%d", &x );          /* Put a value in x, we could also use p here */
    printf( "%d\n", *p ); /* Note the use of the * to get the value */
    getchar();
}

printf 輸出儲存在 x 中的值,這是為什麼?好吧,讓我們看一下程式碼,此整數稱為 x,然後將指向整數的指標定義為 p,然後使用取址運算子(&)將x的記憶體位置儲存在指標中,從而得到變數的位址,使用&符號有點像查看保險箱上的標籤以查看其編號,而不是查看盒子內部以獲取其存儲的內容,然後使用者輸入一個數字,該數字儲存在變數 x 中;請記住,這與 p 所指向的位置相同,事實上,由於我們使用 & 符號將值傳遞給 scanf,所以應該清楚 scanf 正在將值放入 p 指向的位址中。(事實上,scanf 之所以起作用是因為指標!)

下一行然後將 *p 傳遞到 printf 中,*p 對 p 執行「提領(dereferencing)」操作;它查看儲存在 p 中的位址,然後轉到該位址並傳回值,這類似於查看保險箱內部卻發現了另一個箱子的號碼(並且可能是鑰匙),然後您將其打開。

請注意,在上面的範例中,指標在使用之前被初始化為指向特定的記憶體位址,如果情況並非如此,那麼它可能指向任何事物。這可能會給程式帶來極不愉快的後果,例如,作業系統可能會阻止您存取它知道您的程式不擁有的記憶體:這將導致您的程式崩潰,如果它允許您使用記憶體,您可能會弄亂任何正在執行的程式的記憶體 – 例如,如果您在 Word 中打開了一個文件,您可以更改文字!幸運的是,Windows 和其他現代作業系統將阻止您存取該記憶體並導致程式崩潰,為了避免程式崩潰,您應該在使用指標之前對指標進行初始化。

也可以使用釋放的記憶體來初始化指標,這允許動態分配記憶體,它對於設定像是鏈結串列或資料樹之類的結構很有用,在這些結構中,您在編譯時不知道確切需要多少記憶體,因此必須在程式執行期間​​獲取記憶體,稍後我們將討論這些結構,但現在我們將簡單地研究如何向作業系統請求記憶體以及如何向作業系統返回記憶體。

函數 malloc 位於 stdlib.h 標頭檔中,用於使用釋放的儲存區(所有程式都可用的記憶體)中的記憶體來初始化指標,malloc 的工作方式與其他函數呼叫相同,malloc 的參數是請求的記憶體大小(以位元組為單位),malloc 取得該整數大小的記憶體空間,然後傳回指向配置的記憶體區塊的指標。

由於不同的變數類型有不同的記憶體需求,因此我們需要取得 malloc 應傳回的記憶體大小,所以我們需要知道如何取得不同變數類型的大小,這可以使用關鍵字 sizeof 來完成,它透過一個函數並傳回其大小,例如,sizeof(int) 將傳回儲存整數所需的位元組數。

#include <stdlib.h>
 
int *ptr = malloc( sizeof(int) );

這段程式碼設定ptr指向一個大小為int的記憶體位址,所指向的記憶體其他程式就變得不可使用,這意味著細心的程式設計人員應該在使用結束時釋放該記憶體,以免在程式執行期間作業系統丟失記憶體(這通常稱為記憶體泄漏,因為程式沒有追蹤所有的記憶體)。

請注意,透過直接使用指標取得指向的變數的大小來編寫 malloc 敘述會稍微乾淨一些:

int *ptr = malloc( sizeof(*ptr) );

這裡發生了什麼事?sizeof(*ptr) 將評估我們從提領 ptr 中傳回的任何大小的內容;由於 ptr 是指向 int 的指標, *ptr 會給我們一個 int,因此 sizeof(*ptr) 會傳回整數的大小。那為什麼要這樣做呢?好吧,如果我們以後重寫 ptr 的聲明如下,那麼我們只需要重寫它的第一部分:

float *ptr = malloc( sizeof(*ptr) );

我們不必返回並更正 malloc 呼叫以使用 sizeof(float),由於 ptr 將指向一個浮點數,*ptr 將是一個浮點數,因此 sizeof(*ptr) 仍然會給出正確的大小!

當您在宣告變數之後很久的時間後為變數配置記憶體時,這會變得更加有用:

float *ptr; 
/* hundreds of lines of code */
ptr = malloc( sizeof(*ptr) );

free 函數將記憶體傳回給作業系統。

free( ptr );

釋放指標後,最好將指標重設為指向 0,當將 0 配定給指標時,該指標將變為空指標,換句話說,它沒有指到任何的記憶體,透過這樣做,當你用指標做了一些愚蠢的事情時(這種情況經常發生,即使是經驗豐富的程式設計師),當你造成相當大的損害時,你會立即發現而不是後來才發現。

空指標的概念經常被用來作指出問題的一種方式——例如,當 malloc 無法正確配置記憶體時,它會傳回 0,您需要確保正確處理此問題 – 有時您的作業系統實際上可能會耗盡記憶體並為您提供此值!

指標盤點及結論

指標一開始可能感覺是一個非常令人困惑的話題,但我認為任何人都可以欣賞和理解它們,如果您覺得自己沒有吸收有關它們的所有內容,請深呼吸幾次並重新閱讀本課程,您不應該覺得自己已經完全掌握了何時以及為何需要使用指標的每一個細微差別,儘管您應該對它們的一些基本用途有所了解。

這篇翻譯教學是在2015年就開始在寫的,那個時間可能比較有空,後來因為小孩出生、工作轉職到現在,今天卻突然因為想要做一下普通考試的考題,才發現到這篇塵封已久的草稿,索性就給它完成了,這篇教學是在《程式語言教學 – C、C++、OpenGL、STL》裡提到的,各位可以在下面的C語言的整理目錄中看到,另外一些翻譯名詞有參考C語言初學指引【第五版】(修訂版)── 成為高手的奠基之路

感謝你看到這裡,很快就可以離開了,但最好的獎勵行動就是按一下幫我分享或留言,感恩喔~

點我分享到Facebook

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *