b97902HW 板


LINE

前言 上一篇文章我們簡單地介紹了陣列的基本概念,我們可以把陣列想像 成一個表格,每一個表格可以有若干個儲存格,每一個儲存格有一個 索引(Index),可以存一個元素(Element)。 在這一篇我要來介紹陣列的進階用法。在這一篇文章我會提到字串 (String)。 在 C 語言中,如果我們要輸出、讀入一串文字,我們都會用到字串。 即使是像 Hello World 這一種簡單的程式我們也已經用到字串了, 所以字串的重要性可見一斑。我們在這一篇文章會討論這一個問題。 printf("Hello world!\n"); 預備知識 1. 字元型別有二種,一種是 char,另一種是 wchar_t。前者主要用 於 ASCII、BIG5 環境,後者主要用於 Unicode 環境。不過因為 一些因素,本文將只討論 char 的使用。 2. 字元字面常數(Character Literal Constant),一般而言我們會 用數字來代表字元,不過,因為並不是所有的環境都使用相同的 數字來表示同一個字元(例如: ASCII v.s. EBCDIC),所以為了可 移植性(Portability),我們會用 'A' 來代表「用以表示 A 這一 個字元的數字」,其餘類推。 3. 脫逸(Escape),因為有些字元如果想用字元字面常數來表示,就 會破壞 C 語言的語法規則,所以我們不能直接用 '<字元>' 來表 示,而又有不少是很常見的字元,所以我們有其他的表示法。例 如換行字元,我們就用 \n 來代替;單引號字元,我們就用 \' 來代替。又因為 \ 被拿來當作脫逸符號,所以我們就用 \\ 來 代替反斜線。部分替換表如下: ┌───┬───┬───┬──────────┐ │字元脫逸ASCII說明 │ ├───┼───┼───┼──────────┤ │Null\00x00空字元(字串終止字元)│ ├───┼───┼───┼──────────┤ │Tab │\t │0x09 │ │ ├───┼───┼───┼──────────┤ │換行 │\n │0x0A │ │ ├───┼───┼───┼──────────┤ │" │\" │0x22 │在字串內的雙引號 │ ├───┼───┼───┼──────────┤ │' │\' │0x27 │'\'' 單引號的代號 │ ├───┼───┼───┼──────────┤ │\ │\\ │0x5C │脫逸脫逸字元 │ └───┴───┴───┴──────────┘ 字串 在 C 語言中,字串的定義為:一個以字元型別為元素型別的陣列, 而且它必需以 空字元(\0) 結尾。另外,還有一種東西也可以算是字 串,那就是 字串字面(String Literal)。所謂的字串字面就是指以 雙引號夾起來的東西,我們下面會再提到。 ┌────────────────────────────┐ │一段故事:如果你有 Windows 程式設計經驗的話,你一定有看 │ │過「匈牙利命名法」。一種會在變數名稱前面加上型別資訊的命│ │名方法。在匈牙利命名法中,有一個規則就是字串變數的名字會│ │加上 sz,例如 szName 之類的。其中,sz 指的就是 Zero-ter-│ │minated String 的意思。微軟早期的工程師就是用這一種方法 │ │來讓錯誤的程式碼容易看得出來。不過,要不要用匈牙利命名法│ │呢?我想...,微軟的程式品質...,嗯,總之師其意可,師其法│ │則萬萬不可。(因為有其他的缺點) │ └────────────────────────────┘ 根據上面的定義,我們知道,下面 array1 是字串,而 array2 和 array3 不是,為什麼? char array1[] = {'A', 'B', 'C', 'D', '\0'}; char array2[] = {'A', 'B', 'C', 'D', '\n'}; int array3[] = {'A', 'B', 'C', 'D', '\0'}; 很顯然地,array1 滿足我們字串的定義,它是一個以 char 為元素 型別的陣列而且以空字元(\0)結尾。不過,array2 就不是,因為它 不是以空字元(\0)結尾;而 array3 也不是,因為它的元素型別是 int 並不是字元型別。如果我們對 array2 做以下修改,我們也可以 稱它為字串,因為有以空字元(\0)結尾,即便 \0 不是在陣列的最後 一個位置,也仍然是字串。 array2[2] = '\0'; /* After this assign operation, array2 is a string. */ 可是如果每一次要用字串的時候,我們都需要用上面 array1 的方式 來宣告,那寫 C 語言程式的時候根本是一場夢魘。所以有一種叫作 字串字面的東西就被發明了。向上面的 array1 可以簡寫成像 array4 這樣: char array4[] = "ABCD"; 我們可以直接用一對雙引號括住若干個字元,它就會變成字串。當然 也因為如此,你的字串中如果有 ",你就必需用脫逸字元來處理。另 外,如果你很細心的話,你會發現,array4 沒有用 \0 來終止字串, 你想必會質疑:為何它可以是字串? 事實上,字串字面會自動的幫我們在字串的後方加上一個空字元,所 以 "ABCD" 是 等價 {'A', 'B', 'C', 'D', '\0'} 的。也因為如此, 如果你寫出下面的程式碼,編譯器會向你抱怨:陣列過小,因為事實 上 "ABCD" 有五個字元。 char array5[4] = "ABCD"; /* ERROR! */ 當然也因為 字串字面(String Literal) 會自動加上 \0,所以和字 元字面常數是不同的東西。簡單來說 "A" 不等價 'A'。 字串函式 C 語言的標準函式庫裡面有若干個有用的函式,可以幫你簡化字串處 理的繁鎖工作。它的功能涵蓋輸入輸出、字串比較、字串串接、等字 串處理工作。本文將其中一部分簡述如下,不過限於篇幅,有些函式 被我省略掉了,如要更詳細的資訊,大家可以自行去尋找「C Stand- ard Libray」的相關資料。 #include <stdio.h> stdio.h 這一個標頭檔,專門放輸入輸出的函式原型。所以如果要輸 入抑或輸出,你都必需引入 stdio.h 標頭檔。 scanf("%s", ...); 首先,字串不會總是寫死在程式裡,有時候我們也必需要讓使用者 輸入字串。在 C 語言中,我們可以用 scanf 來輸入字串。用法和 我們讀入數字的方法很像,不同點是我們用的不是 %d 而是 %s。 另外,我們不用使用取址(Address-of)運算子,直接把陣列名稱傳 入就好了(為什麼會這樣呢?我們有機會說到指標再談)。例如: char input[16]; scanf("%s", input); 這樣我們就可以讀入一個字串。假如我輸入 xyz 之後按下 Enter, input 的前四個元素就會是 'x', 'y', 'z', '\0'。也就是說: input[0] == 'x'; input[1] == 'y'; input[2] == 'z'; input[3] == '\0'; scanf 會一直讀入字元,寫到陣列中,直到遇到「空白、換行、 Tab」等字元,它才會在陣列加上 \0,然後停止。不過,細心一點 的讀者很快就會發現問題:我輸入多少字,不論陣列夠不夠大, scanf 都會照單全收!進而產生緩充區溢位(Buffer Overflow)! 這是很多程式會有安全漏洞的原因。要怎麼避免呢?你可以在 s 的前面在上數字,表示字串(不含 \0)最多可以多長。例如: char input[16]; scanf("%15s", input); 就表示:最多可以輸入 15 個字元,加上 1 個 \0 做為字串結尾。 如此一來就可以確保不會溢位。 printf("%s", ...); 當然我們常常會有輸出字串的需求,我們要怎麼輸出字串呢?當然 直接用 printf(字串) 也是一個方法。不過,他卻不一定是一個好 方法。舉例來說:如果字串是由使用者輸入的,你本來不預期使用 者會輸入 % 字元,可是使用者輸入的話會發生什麼事呢?請自行 想像!XD 所以正確的方法是什麼呢?標準答案是 printf("%s", 字串); printf("%s", 字串); 使用 %s 來告訴 printf 函式:我們要印出一個字串,請把字串中 除了 \0 之外的所有字元都印到螢幕上。例如: char str[] = "String!"; printf("%s", str); printf("%s", "This is my string!"); fgets(str, n, stdin); 有時候,我們想要讀入的字串含有空白,我們希望以換行字元為分 怎麼辦呢?這時候 fgets 就可以上場了,fgets 可以幫我們讀 入字元,直到遇上換行字元。第一個引數是字串,第二個引數是你 希望最多讀入多少字元(含 \0),第三個引數大家先打 stdin 就可 以了。 char str[16]; fgets(str, 16, stdin); 以上會讀入最多 15 個字元加上一個 \0。會以換行為分界。如果 輸入的字元少於 15 個字元,在 \0 之前會有一個 \n。如果你不 想要他,你可以用下面的方式來處理。 if (strchr(str, '\n')) { *strchr(str, '\n') = '\0'; } #include <string.h> string.h 這一個標頭檔顧名思義,就是要用來處理字串用的。不過 除了處理字串的函式之外,還有一些是處理記憶體的,或者正確的說 是處理一段記憶體空間。 值得注意的是:以 str 開頭的函式,在引數的部分會假定輸入的字 串都是以真得字串,如果你傳入的只是一個字元陣列,而且不以 \0 結尾,則你的程式執行起來會如何,沒有人會知道。所以寫程式的時 候要小心! strlen(str); 這一個函式以一個字串為引數,然後它會回傳這一個字串的字數 (STRing LENgth)。所謂的字數,指得是「非 \0 的字數」,也就 是在碰到 \0 之前一共有多少個字。例如: char str1[] = "ABCD"; char str2[] = {'q', 'w', 'e', 'r', 't', 'y', '\0'}; size_t strlen1 = strlen(str1); /* == 4 */ size_t strlen2 = strlen(str2); /* == 6 */ strcmp(stra, strb); 這一個函式是用來比較二個字串的大小(STRing CoMPare)。如果二 個字串全等,則傳會 0。如果二個字串之間第一個不相同的字元的 索引是 i,則回傳值會和 stra[i] - strb[i] 同號。也就是說在 ASCII 環境下, strcmp("A", "a") < 0 strcmp("Ab", "AB") > 0 strcmp("A", "A") == 0 strcpy(dest, src); 這一個函式的用途是把一個字串複製到另一個字元陣列(STRing CoPY)。第一個引數是目標陣列,第二個引數是來源字串。這一個 函式會把 src 所有「在 \0 之前包含 \0 的字元」複製到 dest。 char receive[1024]; char source[] = "ABCDEFG"; strcpy(receive, source); printf("%s", receive); /* We get "ABCDEFG" as output */ 不過必需注意,如果 dest 的大小,不足以容納 src,則你的程式 極可能發生執行階段錯誤。這也是不少安全漏洞的來源,所以寫程 式的時候請千萬小心。 char buffer[1]; char source2[] = "This Is Going To Overflow!!!! HA! HA! HA!"; strcpy(buffer, source2); /* 錯誤!緩衝區溢位 */ strncpy(dest, src, n); 這一個函式也是要用來複製字串。不過和 strcpy 不同的是,它最 多只會複製 n 個字元。如果 strlen(src) < n,\0 會被複製,如 果 strlen(src) >= n,則 \0 就不會被複製。所以 strncpy 複製 出來的東西不一定是字串。 char buffer1[1024], buffer2[1024]; char buffer3[1024], buffer4[1024]; strncpy(buffer1, "ABCDE", 4); /* buffer1 不是字串 */ strncpy(buffer2, "ABCDE", 5); /* buffer2 也不是字串 */ strncpy(buffer3, "ABCDE", 6); /* buffer3 是字串 */ strncpy(buffer4, "ABCDE", 7); /* buffer4 是字串 */ 不過如果你手動補上 \0,buffer1、buffer2 也可以是字串。 buffer1[4] = '\0'; buffer2[5] = '\0'; 我們會有: strcmp(buffer1, "ABCD") == 0 /* 要補上 \0 才能滿足條件 */ strcmp(buffer2, "ABCDE") == 0 /* 要補上 \0 才能滿足條件 */ strcmp(buffer3, "ABCDE") == 0 strcmp(buffer4, "ABCDE") == 0 strcat(dest, src); 這一個函式的用途是把 src 字串接到 dest 字串之後(STRing conCATenate)。strcat 會先找到 dest 字串中的 \0 然後,從這 一個字元開始複製 src 的字元直到 src 結束為止。當然,src 的 \0 會被複製,所以執行完之後 dest 仍然會是一個字串。例如: char dest[1024] = "abc"; strcat(dest, "ABCDEFG"); printf("%s", dest); /* 輸出 "abcABCDEFG" */ strcpy(dest, "12345"); strcat(dest, "HIJKL"); printf("%s", dest); /* 輸出 "12345HIJKL" */ 不過和 strcpy 一樣,strcat 不會在乎 dest 是否有足夠的空間, 所以也會有溢位問題,你必需確定 dest 可以再加上 strlen(src) 個字元才能用這一個函式。請小心使用,我就不再贅述。 strncat(dest, src, n); 這一個函式和 strcat 一樣是用來串接字串;和 strcat 不一樣的 是有串接字數限制。和 strncpy 一樣有字數限制,不同的是 \0 字元永遠會被複製(事實上你可以想像成原本 dest 的 \0 被搬到 後面)。所以 dest 只要可以再加上 n 個字元就不會溢位。例如: char str1[10] = "ABCDE"; strncat(str1, "abcdefg", 4); /* OK! */ printf("%s", str1); /* 輸出 "ABCDEabcd" */ memcpy(dest, src, n); 這一個函式可以把記憶體中的特定區塊複製 n bytes 到另一處。 和 strcpy 不同,memcpy 會把從 src 算起一共 n bytes 搬到 dest 的位置。我們直接看範例: char str1[6]; char str2[] = "ABCDE"; memcpy(str1, str2, 6); printf("%s", str1); /* 輸出 "ABCDE" */ char str3[] = "ABCDE"; char str4[] = "abcde"; memcpy(str3, str4, 2); printf("%s", str3); /* 輸出 "abCDE" */ 有了基本概念之後,我們來看看變化型。我們在前一章有說到陣列 不可以 assign,我們要複製,只能一個元素一個元素地複製。不 過這樣實在太麻煩了,有沒有簡單的方法?是有的。 int array1[] = {56, 58, 59}; int array2[3]; memcpy(array2, array1, sizeof(int) * 3); array2[0] == 56, array2[1] == 58, array2[2] == 59 稍微講解一下,sizeof(int) 是指一個 int 要多少個 byte,我們 有空再談。之後 memcpy 會幫我們複製 sizeof(int) * 3 個 bytes,所以 array2 的那一段記憶體空間會和 array1 的一樣。 最後我們就會有和 array1 一樣的 array2。 提醒一下: [dest, dest + n), [src, src + n) 最好不要重疊, 不然結果不被定義(EDIT: ckclark學長說,可以改用 memmove 函 式)。 memset(dest, ch, n); 這一個函式可以從 dest 這一個 byte 開始,把 n 個 bytes 全部 設為 ch 這一個字元。例如: char str[] = "ABCDEF"; memset(str, 'a', 3); printf("%s", str); /* 輸出 "aaaDEF" */ 當然,只有這一種功能當然不夠看。memset 最強大的功能就是可 以初始化陣列。不過我要先聲明,這不是一個 Portable (可移植) 的方法 int array1[3]; memset(array1, 0, sizeof(int) * 3); /* 所有 array1 的元素是 0 */ int array2[3]; memset(array2, 0xFF, sizeof(int) * 3); /* 所有 array2 的元素是 -1 */ unsigned int array3[3]; memset(array3, 0xFF, sizeof(unsigned int) * 3); /* 所有 array3 的元素是 UINT_MAX */ 不過這一個方法,不是一個 Portable 的方法,它依賴平台的特性。 例如:如果有一個平台一個 9-bits 等於 1byte,array2 的元素 就不一定會是都是 -1。雖然,這一個方法很快速、簡潔、好用, 不過,切記:跨平台的時候不要忘了! 備註:(在 C99, 也許 C89 也對) 如果要把陣列初始化為 0,大可 不用這種方法,還有更簡單的方法,有空我們再談。 結語 結語要說啥?我不知道....。打那麼多字,手好酸。總之,字串很重 要,看不懂的人,歡迎來信(MSN 的帳號,GMAIL結尾)。下集預 告:再看看.....(陣列III?)。 PS. 這就是 鋼彈 嗎?我可沒有自信,第一次碰鋼彈就可以重寫 OS, 並幹掉 Rusty.....。眾人噓:還不快滾去救生艙! PS2. 你的推文是我再寫的原動力,你的噓文是我自 d 的原動力。 --



※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 140.112.241.166 ※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 00:44) ※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 00:44)
1F:推 ming1053:用gets就好了吧? 10/08 00:47
2F:推 ming1053:再推一個 這篇好文 10/08 00:51
3F:→ LoganChien:其實不說 gets 是故意的,連 strcpy, strcat 我都有一 10/08 00:54
4F:→ LoganChien:點猶豫,原因是... 不是很安全。 10/08 00:55
5F:推 iForests:彩色好文推 10/08 01:26
6F:推 drazi:科科 等到脫離scanf printf的時候 才會知道這些的珍貴... 10/08 02:03
7F:推 drazi:你要知道windows程式設計...要讓東西出現在螢幕上 10/08 02:03
8F:推 drazi:跟讀取Input是多困難的事情= = 噢還有...... 10/08 02:04
9F:推 drazi:匈牙利命名法有它存在的價值及必要......絕對有...... 10/08 02:04
10F:推 haoto:傷心TA推 目前AC的全都給我用array.....明明就不用...T_T 10/08 02:08
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 02:19)
11F:→ LoganChien:感謝 ckclark 助教/學長 的勘誤。(204.) 10/08 02:22
12F:推 benck:太認真了 推一個 10/08 02:33
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 07:39)
13F:推 anfranion:不是很安全是指會不會用而造成的錯誤嗎@@ 10/08 08:11
14F:→ anfranion:gets()比fgets基礎很多,有f都是涉及開檔讀檔了 10/08 08:12
15F:→ anfranion:雖然好像也是有人這樣用啦囧 10/08 08:12
16F:推 sa072686:好文推XD 真是太詳細了! 10/08 09:53
17F:推 godgunman:推好文XD 10/08 12:31
18F:→ hrs113355:fgets比gets安全... 10/08 12:52
19F:→ LoganChien:To drazi: 我覺得匈牙利命名法在那一個 type check 不 10/08 15:55
20F:→ LoganChien:是很好的年代,確實有它的好處。不過,我覺得現在的編 10/08 15:56
21F:→ LoganChien:譯器都可以正確地檢查型別,使用匈牙利命名法的理由有 10/08 15:57
22F:→ LoganChien:很大一部分被消滅了。除了像文中所提到的 sz 我覺得我 10/08 15:58
23F:→ LoganChien:不會想要用這一個命名法。因為太繁鎖了,而且有一些現 10/08 16:00
24F:→ LoganChien:在來說不太必要(例如: lp 的 l 是指 long pointer)。 10/08 16:01
25F:→ LoganChien:除非要 Windows Programming 我現在都不會推薦別人用 10/08 16:02
26F:→ LoganChien:匈牙利命名法。不過話說回來,匈牙利命名法也已經是十 10/08 16:03
27F:→ LoganChien:多年的產物,以現今的角度看可能有失公允。 10/08 16:04
28F:→ LoganChien:To anfranion: 不是很安全指得是「你沒有辦法控制輸入 10/08 16:06
29F:→ LoganChien:的大小」。如果你用 gets,然後,有心人士故意輸入一個 10/08 16:07
30F:→ LoganChien:很長的字串,想要不溢位也難。如果溢位的是一般的字元 10/08 16:08
31F:→ LoganChien:還好,如果是精心設計,還可以執行你輸入的字串(程式) 10/08 16:09
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 16:16)
32F:推 drazi:你說到重點了XD windows programming XD 10/08 18:05
33F:推 drazi:好啦我必須說有windows programming經驗的就知道~ 沒有的就. 10/08 18:05
34F:推 drazi:.....(攤手~) 10/08 18:05
35F:推 jimmycool:匈牙利命名法會讓我想到這篇文章 http://0rz.tw/9e1op 10/08 19:03
36F:→ jimmycool:說得還滿有道理的,可以參考看看:D 10/08 19:03
37F:推 drazi:大推XD 10/08 19:22
38F:推 dennis2030:樓主好文我頂 10/09 00:53
39F:→ LoganChien:To jimmycool: 那一篇我也看過,真得寫得不錯,不過他 10/09 00:59
40F:→ LoganChien:好像比較著重在型別之外加上抽象意義,而非把型別直接 10/09 01:04
41F:→ LoganChien:寫上去。我覺得那一篇真得值得推薦! 10/09 01:05
※ 編輯: LoganChien 來自: 140.112.241.166 (10/09 01:06)
42F:推 anfranion:噢嗯原來是指這個方向的實作 樓主真強大XD 10/09 08:16
※ 編輯: LoganChien 來自: 140.112.241.166 (10/10 07:53)







like.gif 您可能會有興趣的文章
icon.png[問題/行為] 貓晚上進房間會不會有憋尿問題
icon.pngRe: [閒聊] 選了錯誤的女孩成為魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一張
icon.png[心得] EMS高領長版毛衣.墨小樓MC1002
icon.png[分享] 丹龍隔熱紙GE55+33+22
icon.png[問題] 清洗洗衣機
icon.png[尋物] 窗台下的空間
icon.png[閒聊] 双極の女神1 木魔爵
icon.png[售車] 新竹 1997 march 1297cc 白色 四門
icon.png[討論] 能從照片感受到攝影者心情嗎
icon.png[狂賀] 賀賀賀賀 賀!島村卯月!總選舉NO.1
icon.png[難過] 羨慕白皮膚的女生
icon.png閱讀文章
icon.png[黑特]
icon.png[問題] SBK S1安裝於安全帽位置
icon.png[分享] 舊woo100絕版開箱!!
icon.pngRe: [無言] 關於小包衛生紙
icon.png[開箱] E5-2683V3 RX480Strix 快睿C1 簡單測試
icon.png[心得] 蒼の海賊龍 地獄 執行者16PT
icon.png[售車] 1999年Virage iO 1.8EXi
icon.png[心得] 挑戰33 LV10 獅子座pt solo
icon.png[閒聊] 手把手教你不被桶之新手主購教學
icon.png[分享] Civic Type R 量產版官方照無預警流出
icon.png[售車] Golf 4 2.0 銀色 自排
icon.png[出售] Graco提籃汽座(有底座)2000元誠可議
icon.png[問題] 請問補牙材質掉了還能再補嗎?(台中半年內
icon.png[問題] 44th 單曲 生寫竟然都給重複的啊啊!
icon.png[心得] 華南紅卡/icash 核卡
icon.png[問題] 拔牙矯正這樣正常嗎
icon.png[贈送] 老莫高業 初業 102年版
icon.png[情報] 三大行動支付 本季掀戰火
icon.png[寶寶] 博客來Amos水蠟筆5/1特價五折
icon.pngRe: [心得] 新鮮人一些面試分享
icon.png[心得] 蒼の海賊龍 地獄 麒麟25PT
icon.pngRe: [閒聊] (君の名は。雷慎入) 君名二創漫畫翻譯
icon.pngRe: [閒聊] OGN中場影片:失蹤人口局 (英文字幕)
icon.png[問題] 台灣大哥大4G訊號差
icon.png[出售] [全國]全新千尋侘草LED燈, 水草

請輸入看板名稱,例如:e-shopping站內搜尋

TOP