指標 (pointer) 是著名的 C 語言大魔王,因為它很難安全的使用(C++ 11規格甚至有smart pointer來管理指標)。筆者猜想有幾個導致的原因 (鬼故事) :
calloc
為我們在heap區段新增一塊記憶體位址,或需用ralloc
等;不過大部分我們只是酷愛這種很hack的行為,這樣筆者認為不如適當的用陣列取代,讓系統自動管理記憶體的釋放。*
,放在變數聲明(聲明指標變數)和表達式(取得指向位址的值)卻有不同意義;而這違反了不同功能 應賦予 不同外觀 的設計原則。ok, 嚇完了還未學習過指標的讀者,如果你還沒關閉視窗或開始搜尋rust
語言,相信你的勇氣應該足夠看完整篇對指標的介紹 (勇氣可佳!!)
先從變數的記憶體位址簡單介紹一下,C/C++程式設計師被賦予其中一項強大的能力/責任,就是可以取得變數的記憶體位址 和 決定如何解釋儲存於該位址的值;首先,&
是取址符號,可以讓我們取得變數的記憶體位址double var = 3.14; &var;
(&var
是變數var
的記憶體位址),而指標則是儲存記憶體位址的複合資料型態(composed data type),因此可以在然地和CH2介紹的資料型態相複合,例如聲明一個指向整數變數的位址的指標聲明為int* ptr
;若沒有特別指定資料型態則聲明為void* ptr;
,代表儲存任意資料型態的記憶體位址。
類似於聲明指標變數,我們同樣可以藉由放置*
符號於變數前,來取得指向位址所儲存的值*ptr;
;先下一個小範例感受一下:
#include<stdio.h>
int main(){
double var = 3.14;
void* ptr = &var;
printf("value stored in pointed address : %p\n", ptr);
printf("address : %p\n", &var);
// 報錯!!
printf("corresponding value : %f", *ptr);
}
恩,*ptr
所在的這行給出了error: invalid use of void expression
,往前看有個警告warning: dereferencing ‘void *’ pointer
,這個報錯的原因在於我們尚未決定如何解釋儲存於該位址的值(由於指標只記錄位址,並沒有位址資料型態的資訊),這個例子我們儲存著double
變數,因此我們需要提供位址儲存的值是什麼資料型態,編譯器才能正確地解讀var
變數的值;實際上只要將 void
指標轉型為指向double
變數位址的指標 (還記得CH3介紹的顯示轉型方法void*ptr ; (double*)ptr;
) 就可以存取變數值,修改一下上面的例子 :
#include<stdio.h>
int main(){
double var = 3.14;
void* ptr = &var;
printf("value stored in pointed address : %p\n", ptr);
printf("address : %p\n", &var);
printf("corresponding value : %f\n", *(double*)ptr);
// or you can directly declare double pointer..
double* doub_ptr = &var;
printf("double pointer value : %f", *doub_ptr);
}
上面的例子告訴我們兩件事,第一、如果你很清楚指標要儲存的資料型態為何,最好直接聲明指向該資料型態位址的指標(例如指向整數的指標int* int_ptr;
、指向字元的指標char* c_str;
),指向任意資料型態的指標void* ptr;
通常用於不預設處理對象的資料型態;第二、指標的轉型面臨和資料型態轉型一樣的風險,例如將指向double
變數的指標轉型為int* ptr = &var;
,與CH3介紹的情況一樣,轉型產生的錯誤結果將再重演...
為了簡便解釋,我們可以將var 聲明為
float
,據IEEE-754標準,浮點數由三個部分組成,sign
(1 bit)、exponent
(8 bits)、mantissa
(24 bits),因此每個二進位值在各個區段都意義,例如在exponent
是指數加倍mantissa
的值;但當我們轉型為整數址標,這個區段不再有特殊意義,再看3.14
於單精度浮點數的16進位碼是4048f5c3
,直接轉成int
(當成mantissa
)是1078523331
,與我們程式結果的錯誤輸出一致,Fail Successfully ~ www
我們認識指標的概念後,陣列是什麼? 它會讓我們的工作更輕鬆嗎? 會的!
陣列(array) 是一個儲存一連串同樣資料型態變數位址的指標,既然是一連串變數,是否意味著要儲存多個位址呢? 幸好答案是否定的,從陣列的聲明可以窺知一二 int arr[ ] = {1, 3, 5, 7, 9};
,根據右側的初始化列表(initialized list),編譯器知道要為其配置5個連續的整數空間,並只要儲存指向第一個值的位址即可;存取第三個值5時會表示arr[3-1]
(index從0開始),由於陣列的記憶體配置從我們來看是連續的,因此底層只是基於第一個值的位址 加上 兩個整數所佔空間的位移量,這部分的細節完美的隱藏在陣列的操作,而這帶給我們許多便利。
敏銳的讀者應該察覺到陣列與指標的關聯,事實上int arr[3];
幾乎等同於int * const arr = calloc(3, sizeof(int));
,也就是程式執行時會先在堆積(heap)區域(在4G記憶體的電腦,上限約至3G)要一塊連續的記憶體;此範例中我們採用calloc
,因此也會同時初始化記憶體內容(填0或NULL),爾後聲明一個一經賦予指向位址 即不可變更(const關鍵字會於後續章節介紹)指向位址的指標;並且於後續所在函數或程式結束時,系統也會自動釋放(auto free)該記憶體區塊,而不會繼續保留(這點就與malloc
、calloc
等自行管理的細節不一樣了),導致記憶體洩漏的額外問題。
這樣看來陣列確實方便許多,不過偶爾也有一些陷阱;讀者可能上面的簡短範例有些疑惑:為何我們大多讓系統動態決定陣列大小,也就是採用int arr[] = {1, 3, 5};
的方式來聲明陣列,而非顯示地用變數動態儲存陣列的大小,例如int N = 3; int arr[N] = {1, 3, 5};
? 一方面當然是初始化列表({1, 3, 5}
)已經提供了足夠的訊息,而另一方面則是編譯器不允許我們這麼做!
#include<stdio.h>
int main(){
int const N = 3;
int arr [N] = {1, 3, 5};
}
由於我們將陣列聲明的長度用一個變數儲存,這基本上代表此陣列的長度在賦值前是動態可變的,如果程式運行後才發現N==1
,則陣列分配的空間就變得不足以儲存初始化列表的值(int arr[1] = {1, 3, 5};
),因此編譯器在無法確定我們分配的陣列記憶體有多少,又面臨我們對其用初始化列表賦值時,就會產生錯誤error: variable-sized object may not be initialized
。解決的方法仍是採用之前的作法int arr[] = {1, 3, 5};
,動態的讓系統決定,而這樣的缺點在於如何取得陣列的長度,我們可以如下採用sizeof
的作法取得陣列長度,因此在大多情況下並不會造成困難。
請注意,如範例所示,即使我們將變數聲明為不可變更的(
const
),編譯器仍不允許我們這麼做!
回顧CH2資料型態的介紹,對於 c_string 這類 char*
以指針為本的字串型態,我們也可以藉由定義字元指標的陣列 (char* arr[]
) 來輕易的進行操作,而這種聲明方式也適當的隱藏了儲存指標記憶體位置的指標(簡稱 指標的指標) 這個概念,讓我們目前使用這些語法時不需太過拘泥於細節:
#include<stdio.h>
#define len(arr) sizeof(arr)/sizeof(arr[0])
int main(){
char* arr [] = {"Arrow", "Button", "Cycle", "DINO"};
printf("1. arr version traversal\n");
// len(arr) - 1 to skip NULL dummy value, that's the endpoint for pointer
for(int idx=0 ; idx < len(arr) ; ++idx)
printf("%s, ", arr[idx]);
}
完成了指標的介紹,對C語言熟悉的讀者可能意識到有許多細節沒有提及,例如 位址的位移量計算、calloc
、free
等輔助我們管理記憶體資源分配與釋放的函數,但筆者邀請讀者嘗試從另一個思維來看;基於陣列提供的許多便利機能(自動管理記憶體、index隨機存取特定的變數),我們更應該在多數可行的情況下盡量採用陣列,而非總是自行進行一些細節的操作--尤其是自行管理記憶體,一方面避免指標的汙名化,另一方面也省去許多基於這方面細節所產生的bug。
事實是,有一些常見的情況下,我們仍需要自行管理記憶體,例如從函數取得多個同樣資料型態的返回值,由於變數在函數結束時即脫離其生存範圍,一般的情況下變數資源會被釋放掉;我們仍需要藉由
calloc
索取儲存內容,這個內容即使函數結束也不會被釋放,也才能在主程式繼續使用 (反過來說,忘記釋放就是一場記憶體洩漏的災難了)。這部分的內容,可能會到後面的章節再說明。
另一方面,也有一些關於指標的概念是值得進一步作介紹的,例如 指標位移、指標的指標等;其中有些概念未必具有實用性質,而是助於我們理解某些底層機制的運作 (例如 struct
的記憶體模型可用不同的指標位移解釋它如何儲存不同的資料型態),算是滿足求知的慾望...;有一些概念則具有實用性質,來彌補實務遇到的棘手問題,(例如陣列傳遞至函數所產生的退化現象,使得我們採用指標的指標來構建多維陣列反而較容易);希望在下一章能帶給讀者足夠的收穫。
預告 : C 語言-從放棄到入門 CH4 : 指標 (下)
update log : 2024/12/07 15:09,