如果你是學高階語言(例如python),我們就可以結束這課教學,很可惜C語言的底層性質讓這一切的開始沒這麼簡單...orz
我們可以先有些mindset,進到C這門較底層的語言,記憶體對於程式設計師是裸露的;資料型態只是對記憶體區段賦予意義,協助程式了解該區段記憶體儲存的值應該要如何解釋;從這個角度一方面能理解資料型態間的轉換是多麼容易出錯(因為涉及對記憶體區段的重新解釋(re-intepretation)),另一方面也理解資料型態本身在這層意義上是抽象的,例如不同的機器用多少位元儲存"同樣的"資料型態,也將有差異;所幸現在已經有一些方法讓我們初學時不必太在意這些問題。
先從C語言提供的資料型態來看,雖然種類較為繁多、靈活,且難以掌握,但目前不避太過拘泥於細節;粗略有三種資料型態:
int
, float
, double
)char
, wchar_t
)bool
(邏輯真假值);簡言之,數字只要記得整數(int)和小數(double)、文字記得單一字元(char)和布林值即可。void 不是資料型態,而是如其名代表空 (
no data
),常用於聲明函數沒有返還值;一個比較貼近資料型態的概念是,void* ptr
代表聲明一個變數ptr
用於儲存指向某個記憶體位址,而且指向的位址沒有指定是用於儲存哪種資料型態的值。
基於數字類的資料型態,我們先提供一個便捷的思考方式 : 如果儲存的值只可能是整數,不管正負數皆使用long
(至少32bits);儲存的值有可能是小數則使用 double
;不過緊接著,我們就會介紹比較正統的思考方法...
不要因為它只用於儲存正整數就很 hack的想用
uint
,或計算float
儲存值得範圍,通常double
夠用了,再大部分的機器上也不會造成太嚴重的效能影響;至少初學時我們不必關注這些細節。
關於資料型態,以下介紹兩個大魔王級的概念: 1.資料型態的位元數和 2.字元的混亂型態:
關於資料型態的位元數,癥結點在於C提供了貼近機器的靈活設計,使得每個資料型態可以儲存多大的內容來表示數值有些具體的規範;例如int在某些機器上"最小"只能儲存16位元(bit),因為int預設是有號整數,這代表我們要分一半儲存負數(2^15)、另一半儲存正數(2^15-1),一個值給不屬於正數或負數的0
,技術上而言2^15+(2^15-1)+1=2^16
,剛好展示16bit能儲存的範圍;因此儲存的值從-32768(-2^15)至32767(2^15-1),超過就會rollback回來,甚麼是rollback不重要,總之我們總需避免而根據機器指定一個夠大的儲存位元;好消息是今年已是2024年,即使是C語言的使用者,我們也不是野蠻人硬測這台機器是16bit或32bit,交給標準函數庫吧。include<stdint.h>
,處理標準定義的函數庫允許我們顯示的指定位元數,以解決跨平台的需求;例如整數有int8_t
, int16_t
, ..., int64_t
,要幾位元就聲明幾位元,若機器不支援它也會告訴你的。
關於字元(char),最初它只能表示一個包含於 字元集(character set) 的字元,有點繞口..;讓我們看個例子,char c = 'a';
,很好,這樣ok!
讓我們看一些不ok的例子,char c = 'abc';
,它會給你一個警告(warning: overflow in conversion from ‘int’ to ‘char’
),因為你正在轉換multi-char
這個 多字元 整數至單一字元char
,而它會引發overflow,因為char如我們所知只能儲存"一個"字元集中的字元;multi-char
是個血脈上與char相近的型態,只要知道你用單引號框住的字元都是在聲明這個資料型態,而它是一個整數型態,例如'abc'
,不過讓我們先忘了它,只要記得我們不應在單引號內放置多於一個字元,否則就是在聲明multi-char
。
ok,理解濫用單引號的下場和知道char的極限後,發現我們只學了一個毫不實用的方法來定義文字的變數,因為它只能儲存一個字元;這個問題可以被陣列(array)解決,陣列是一次定義多個同一資料型態的變數,並且它會自動分派、釋放儲存著多個值的記憶體(等到介紹這個可怕的故事已經是好幾章之後的事了);例如 int arr[3] = {0, 1, 2};
,一次我們就定義了儲存三個整數的變數arr,取得整數二也很直覺arr[2]
,index為2是因為index從0起算。
如果你是高手精通指標,你可能會嘗試將範例改成
char* ch_ptr = "abd";
,這樣的舉一反三很好,但與陣列相比,我建議你可以併用strdup
(string duplication)來複製字串,以免這個非const的字串有更改其中內容的需求,由於指標是指向常量,而C語言對於如何儲存字串常量是未定義的,你可能會成功或失敗;因此建議修改前先複製一份 (char* ch_arr = strdup("abd");
),這樣好多了,之後就可以避免ch_arr[0] = 'B';
產生出乎意料的結果。
因此,我們同樣可用char ch_arr[3] = {'a', 'b', 'c'};
來儲存三個字元.. 我的天,數字還可以,文字這樣定義簡直要人命;前面我們提過單引號內放入多於一個字元不行,那放入雙引號可不可以?char ch_arr[3] = "abc"
好消息是這樣看似可行,但為何可行又是另一個長篇故事了;簡言之,雙括弧內放置的多個字元"abc"
會被解譯成字串(c style string, 簡稱c_string)常量,是字串而不是字元(char),因此等號間其實通過一個資料型態的轉換,將字串轉成字元陣列;但資料型態的轉換不是第一課要說明的,我們先理解要放多個字元就請用雙括弧吧!
最後,為了更深入的使用printf函數來理解我們定義的變數,我們稍微深入介紹一下printf。
先下一段函數的聲明:printf( "formatted_string", arguments_list);
,formatted_string是一個包含指定型態的字串,指定的型態就是我們之前提及的 int, double, char ... 等等;在指定時他們也有對應的英文字母 int
(%d
, digit)、char
(%c
, char), c_string(%s
, string),依此類推,請你查詢其他資料型態的英文字母;而arguments_list則按照順序存放各變數值。
讓我們下一段程式,統整一下上述的概念:
#include<stdio.h>
int main(){
int num = 1;
char c_str1[4] = "abc";
char no_stop_cstr[3] = "abc";
/* it's not necessary place '\0' in {}, but we do it explicitly
to show that how char array (c_string) declare the stop symbol! */
char c_str2[4] = {'d', 'e', 'f', '\0'};
printf("c lang lesson %d\n", num);
printf("char value : %c\n", c_str1[1]);
printf("c_string value : %s\n", c_str1);
printf("raw c_string value : %s\n", c_str2);
printf("so far so good..\n------------\n");
printf("boom! : %s, i can not stop to print out...orz", no_stop_cstr);
return 0;
}
執行程式
# 偷偷介紹一個旗標(flag),-Wall (warning all message) 會開啟所有編譯警告
# 如果 gcc 發現你的程式碼有甚麼奇怪的地方,它將會毫不吝嗇地告訴你
!gcc -o test.out -Wall test.c ; ./test.out
# 輸出
c lang lesson 1
char value : b
c_string value : abc
raw c_string value : def
so far so good..
------------
boom! : abcabc, i can not stop to print out...orz
好的,從程式的執行結果,看來我們忽略了一點關於c_string的說明,事實上我們並沒有告訴printf什麼時候要停,c_string是以'\0'
這個字符作為聲明字串的尾端;上面初始化的例子中我們直接用雙引號聲明了一個字串(c_string中文我就直稱字串了),而我們聲明的char array會在尾端自帶'\0'
,因此我們不必特別聲明,不過我們卻需要記得比輸入的字串多聲明一個記憶體的位址,如此'\0'
才不會被char值覆寫,以至於產生上述no_stop_cstr
的例外,它一路輸出了其他變數的值,因為他們的記憶體位址恰巧被連續分配在同一區塊,否則就不是這麼幸運,而會因非法入侵他人領土,引發一個著名的segment fault
錯誤
然而,工程師不能每次都期望恰巧的發現這個bug,或防患未然的多聲明一個位址空間;問題出在我們的字串常量(c_string literal)應該提供了足夠的資訊,例如至\0
之前包含幾個字元,但我們仍手動聲明要預先分配多少字元的空間,事實上定義array的方式可以更加聰明,如果你提供了足夠的資訊(例如各式常量)。
我們單獨修正聲明 array 的地方,刻意不提供分配多少空間,讓編譯器推論 ~
#include<stdio.h>
// 被我們發現C語言一些酷炫的東西了,之後再介紹巨集 ~
// sizeof "關鍵字" 回傳分配的記憶體量, 而此巨集用來計算陣列的長度,不過也有失靈的時候..orz
#define len(arr) sizeof(arr)/sizeof(arr[0])
int main(){
char c_str[] = "abc";
// google ascii code, 作為最後一個範例,內容是"BYE BYE"
int arr[] = {66, 89, 69, 32, 66, 89, 69};
printf("raw c_string value : %s\n", c_str);
// %lu, 印出 unsigned long數值,len 巨集回傳陣列長度
// 可以發現它會 "按照常量" 分配4個字元給 c_str,包括 '\0'
printf("allocated space : %lu\n", len(c_str));
// 一個偷懶將整數陣列印出字元群的做法
for(int idx=0; idx<len(arr);++idx)
printf("%c", arr[idx]);
}
程式執行結果
raw c_string value : abc
allocated space : 4
BYE BYE
可以看到,其他資料型態的陣列可以支援這種自動分派記憶體的方式,但要如何取得陣列的長度又是另一個問題了...
這邊我們先用一個簡單的巨集解決了,巨集這個強大但危險的功能會再後續慢慢介紹,如何在力量與危險兩者間取得平衡,將是一個很展現C語言程式設計師能力的問題 ~
預告 CH3 : 資料型態的轉換
update log : 2024/11/24 19:12,