# C語言解釋 - 字元陣列 ###### tags: `c`, `char`, `c string` [TOC] ## 題目 在一長字串中,尋找特定字元的次數。要解釋這個題目之前,要先暸解陣列、陣列指標以及字元陣列在C語言的特性。 ## 陣列跟陣列指標 在認識字串前,我們要先知道陣列以及陣列指標。 如果把int, double, char 等資料型態看做單數,那其複數型態就是用陣列來表達,用陣列儲存資料應該不是太大問題,一個蘿蔔一個坑,指標才是讓人容易搞不懂的地方。 簡單來講,當你定義一個指標變數時,它的內容是用來儲存位址(address),而非資料,我們會透過`*`運算子(稱做value of or dereferencing, 取值),去存取此位址實際上的資料。 看個簡單的例子,下面程式碼有陣列叫做`data`, 又有一個指標變數叫做`ptr`,要讓指標變數跟陣列建立關聯,就是透過`&`運算子(稱做 address of or referening,取位址)。 ```clike= #include <stdio.h> int main(void) { int data[] = {5, 9, 2, 3, 0, 8, 7}; int *ptr = &data[0]; ptr = &data[0]; printf("ptr+4: %d\n", *(ptr+4)); } ``` 一樣看一下這段程式碼在實際記憶體長什麼樣子。 ```clike= (lldb) p data (int[7]) $3 = ([0] = 5, [1] = 9, [2] = 2, [3] = 3, [4] = 0, [5] = 8, [6] = 7) (lldb) p &data (int (*)[7]) $4 = 0x000000016fdfea90 (lldb) x/8x &data 0x16fdfea90: 0x00000005 0x00000009 0x00000002 0x00000003 0x16fdfeaa0: 0x00000000 0x00000008 0x00000007 0x16008001 (lldb) p ptr (int *) $6 = 0x000000016fdfea90 (lldb) p &ptr (int **) $7 = 0x000000016fdfea88 (lldb) p *ptr (int) $8 = 5 ``` 第一行`p data`,印出整個data的內容。 第三行找出陣列所在位址,0xfea90. 第五行實際看一下data的記憶體內容,data總共七筆資料,因為資料型態是int會佔4 bytes 長度,所以會看到每筆資料都佔這麼多零。 那也因為data 有七筆,所以會看到第八筆是其他資料,並不在我們預期內。 重點來了!注意第八行,直接印出ptr變數內容,看起來是個奇怪的數字,跟data並無關係,但注意看第三行內容,是不是跟data所在位址一模一樣。 第十行則是印出指標變數ptr 自已所在的位址。我用的是取位址運算子`&`。會看到0xfea88,這個是ptr變數自已的位址。 第十二行則是使用取值運算子`*`,對ptr變數裡面所存的位址內容取值。類似下圖的方式,以間接的方式去取得data的內容。 ```graphviz digraph graphname{ 指標變數-> 指標內容; 指標內容 -> 存取陣列; ptr -> fea90h; fea90h -> 5; } ``` 指標的存在主要是避免陣列內容的大量的移動或複製,簡單來說,可以依一個變數就可去瀏覽及修改整個陣列內容。 這裡容易誤解的是`*`, `&`這兩個運算子。先解釋`&`符號。 | 運算子 | 解釋 | |:-----:|:-----:| | & 位元運算子 | AND Bitwise 位元運算,需要二個運算元 | | && 邏輯運算子 | AND 邏輯運算,需要二個運算元 | | & 取值運算子 | 取位址運算,只需一個運算元,取得變數所在位址 | | 運算子 | 解釋 | |:-----:|:-----| | * 乘法運算子 | 乘法算術運算,需要二個運算元 | | * 取值運算子 | 針對指標變數,間接取得所在位址的資料內容 | | * 定義指標變數 | 定義變數為指標變數。只有定義指標變數時,才會使用| 同樣或類似的符號,有不同的使用方法,像`*`可以使用在乘法運算上,這應該容易搞清楚;讓人混亂的是用在**定義指標變數**以及**取值運算**時,這兩件都跟指標有關,而且都只需要一個運算元,重點,它都使用相同符號,星號,只要分得清一個是在定義指標變數時使用,另一個就是取值運算子,剩下就是需要乘法運算時會使用。 ## 字元跟字串 認識陣列跟指標後,進入第二個主題,字串。 C語言並沒有字串這種資料型態,而是由字元陣列所組成,直接看例子。 ```clike= int main(void) { char str[] = "Hello world"; char single_char = 'C'; printf("%s\n", str); printf("%c\n", single_char); } ``` 一樣看記憶體內容。 ```clike= (lldb) p str (char[12]) $25 = "Hello world" (lldb) x/16b &str 0x16fdfeaa8: 0x48 0x65 0x6c 0x6c 0x6f 0x20 0x77 0x6f 0x16fdfeab0: 0x72 0x6c 0x64 *0x00 0x01 0x00 0x00 0x00 (lldb) p single_char (char) $27 = 'C' (lldb) x/16b &single_char 0x16fdfeaa7: 0x43 0x48 0x65 0x6c 0x6c 0x6f 0x20 0x77 0x16fdfeaaf: 0x6f 0x72 0x6c 0x64 0x00 0x01 0x00 0x00 ``` [Wiki ASCII](https://zh.wikipedia.org/wiki/ASCII) 主要是觀察字元以及字元陣列。 第三行 印出str所在內容,可以查詢上述連結,ASCII 0x48為大寫`H`,0x65為`e`,以此類推,可以找到完整的"Hello world". 特別注意第五行第四個數字0x00, 我特別標示一個星號在旁邊。 第八行一樣印出變數`single_char`所在內容。single_char變數是一個用單引號包住的大寫`C`,所在位址是0xeaa7,ASCII 為0x43. 字元陣列str是由一個雙引號包住的字串,特別提這個跟剛剛被標記星號的0x00有關係,在C語言中,單引號包著的字串,在結尾並不會被多加一個0x00;而雙引號帶出來的字串,在結尾處會被C語言加一標記0x00. 這次很剛好的,變數single_char的位址剛好就在陣列str之前,剛好也驗證被單引號包住的變數不會以0x00結尾。 ## 計算字元出現次數 ```clike= #include <stdio.h> #include <string.h> // strlen #include <ctype.h> // isdigit isalnum #define print_obj(obj, fmt) printf(#obj ": " #fmt "\n", obj); int main(void) { char str[] = "asdflv389454nczasvkldqwaladaaaaa309$#%%&@$1#^5461@3wqldk"; unsigned int cnt_char_a = 0; unsigned int cnt_digit = 0; unsigned int cnt_symbol = 0; for(unsigned int idx=0; idx<strlen(str); idx++) { if(str[idx]=='a') { cnt_char_a++; } if(isdigit(str[idx])) { cnt_digit++; } if(!isalnum(str[idx])) { cnt_symbol++; } } print_obj(cnt_char_a, %d); print_obj(cnt_digit, %d); print_obj(cnt_symbol, %d); } ``` 跟字串相關的函式庫我們會用到<string.h>以及<ctype.h>。 <string.h> 會用到strlen() 計算字串長度; <ctype.h> isdigit()可以協助判斷這個字元是不是數字,另一個是isalnum()判斷字元是不是文字(a-Z A-Z)或數字(0-9). 完整內容可以參考下面連結。 [cplusplus strlen](https://cplusplus.com/reference/cstring/strlen/) [cplusplus isdigit](https://cplusplus.com/reference/cctype/isdigit/) [cplusplus isalnum](https://cplusplus.com/reference/cctype/isalnum/) 延伸你的題目,除了計數字母'a'外,我們也計算數字以及非文數字的符號出現次數。所以第八到第十行設定三個變數用來負責計數。計數前,要記得歸零。 再來重點就是第12行到第22行,我們利用strlen()得知字串總長度,用迴圈的方式從頭到尾瀏覽一遍,每次遇到的字元,會經過三個if...else,若符合條件,對應的變數就會遞增1,直到逛完整個字串。 ```clike= #include <stdio.h> #include <ctype.h> // isdigit, isalnum #define print_obj(obj, fmt) printf(#obj ": " #fmt "\n", obj); int main(void) { char str[] = "asdflv389454nczasvkldqwaladaaaaa309$#%%&@$1#^5461@3wqldk"; char *ptr = &str[0]; unsigned int cnt_char_a = 0; unsigned int cnt_digit = 0; unsigned int cnt_symbol = 0; while(*ptr!='\0') { if(*ptr=='a') { cnt_char_a++; } if(isdigit(*ptr)) { cnt_digit++; } if(isalnum(*ptr)) { cnt_symbol++; } ptr++; } print_obj(cnt_char_a, %d); print_obj(cnt_digit, %d); print_obj(cnt_symbol, %d); } // Output // cnt_char_a: 9 // cnt_digit: 15 // cnt_symbol: 10 ``` 剛剛是用陣列方式瀏覽整個字串,現在我們試著用指標的方式取代, 這裡就會用到字串結尾是0x00的特點。 第七行,我們定義一個指標變數ptr並且與長字串str建立關聯。 接下來與上一段的for迴圈類似,只是改用while迴圈,迴圈會一直判斷是否已經到結尾,若還沒遇到,一樣會有三個if...else來計數。 其實使用廻圈都要非常小心,結束條件一定要成立,要不然很容易寫成無窮廻圈,程式無法結束。 第22行會讓ptr變數遞增,往下走,再搭配字串結尾為0x00的判斷,才可以順利結束迴圈,很簡短,但是它是程式能順序走完的關鍵。 ```clike=3 #define print_obj(obj, fmt) printf(#obj ": " #fmt "\n", obj) ``` [ISO/IEC 9899:2011 N1570 PDF](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf) 有沒有好奇C語言的規則是從哪裡來的?C語言是一個ISO工業標準,就跟之前介紹的IEEE-754一樣都是由國際上訂定,大家一同遵守的規則。 上述連結是C11正式發佈前的草稿(正式文件需要付費)。雖然是草稿,但是聽說與正式文件相差不遠,也許只修改幾個字,但是大意應該是一致。 提這件事是因為第三行,我用了preprocessor `#define` 取代的功能,我建立一個叫`print_obj`的preprocessor去取代我原先應該寫成 `printf("cnt_char_a: %d", cnt_char_a);`的程式碼。 > 6.10.3 Macro replacement