[計程] 陣列簡介 II (來連載吧,繼續害人...

看板b97902HW作者 (簡子翔)時間15年前 (2008/10/08 00:43), 編輯推噓19(19023)
留言42則, 12人參與, 最新討論串1/1
前言 上一篇文章我們簡單地介紹了陣列的基本概念,我們可以把陣列想像 成一個表格,每一個表格可以有若干個儲存格,每一個儲存格有一個 索引(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)

10/08 00:47, , 1F
用gets就好了吧?
10/08 00:47, 1F

10/08 00:51, , 2F
再推一個 這篇好文
10/08 00:51, 2F

10/08 00:54, , 3F
其實不說 gets 是故意的,連 strcpy, strcat 我都有一
10/08 00:54, 3F

10/08 00:55, , 4F
點猶豫,原因是... 不是很安全。
10/08 00:55, 4F

10/08 01:26, , 5F
彩色好文推
10/08 01:26, 5F

10/08 02:03, , 6F
科科 等到脫離scanf printf的時候 才會知道這些的珍貴...
10/08 02:03, 6F

10/08 02:03, , 7F
你要知道windows程式設計...要讓東西出現在螢幕上
10/08 02:03, 7F

10/08 02:04, , 8F
跟讀取Input是多困難的事情= = 噢還有......
10/08 02:04, 8F

10/08 02:04, , 9F
匈牙利命名法有它存在的價值及必要......絕對有......
10/08 02:04, 9F

10/08 02:08, , 10F
傷心TA推 目前AC的全都給我用array.....明明就不用...T_T
10/08 02:08, 10F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 02:19)

10/08 02:22, , 11F
感謝 ckclark 助教/學長 的勘誤。(204.)
10/08 02:22, 11F

10/08 02:33, , 12F
太認真了 推一個
10/08 02:33, 12F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 07:39)

10/08 08:11, , 13F
不是很安全是指會不會用而造成的錯誤嗎@@
10/08 08:11, 13F

10/08 08:12, , 14F
gets()比fgets基礎很多,有f都是涉及開檔讀檔了
10/08 08:12, 14F

10/08 08:12, , 15F
雖然好像也是有人這樣用啦囧
10/08 08:12, 15F

10/08 09:53, , 16F
好文推XD 真是太詳細了!
10/08 09:53, 16F

10/08 12:31, , 17F
推好文XD
10/08 12:31, 17F

10/08 12:52, , 18F
fgets比gets安全...
10/08 12:52, 18F

10/08 15:55, , 19F
To drazi: 我覺得匈牙利命名法在那一個 type check 不
10/08 15:55, 19F

10/08 15:56, , 20F
是很好的年代,確實有它的好處。不過,我覺得現在的編
10/08 15:56, 20F

10/08 15:57, , 21F
譯器都可以正確地檢查型別,使用匈牙利命名法的理由有
10/08 15:57, 21F

10/08 15:58, , 22F
很大一部分被消滅了。除了像文中所提到的 sz 我覺得我
10/08 15:58, 22F

10/08 16:00, , 23F
不會想要用這一個命名法。因為太繁鎖了,而且有一些現
10/08 16:00, 23F

10/08 16:01, , 24F
在來說不太必要(例如: lp 的 l 是指 long pointer)。
10/08 16:01, 24F

10/08 16:02, , 25F
除非要 Windows Programming 我現在都不會推薦別人用
10/08 16:02, 25F

10/08 16:03, , 26F
匈牙利命名法。不過話說回來,匈牙利命名法也已經是十
10/08 16:03, 26F

10/08 16:04, , 27F
多年的產物,以現今的角度看可能有失公允。
10/08 16:04, 27F

10/08 16:06, , 28F
To anfranion: 不是很安全指得是「你沒有辦法控制輸入
10/08 16:06, 28F

10/08 16:07, , 29F
的大小」。如果你用 gets,然後,有心人士故意輸入一個
10/08 16:07, 29F

10/08 16:08, , 30F
很長的字串,想要不溢位也難。如果溢位的是一般的字元
10/08 16:08, 30F

10/08 16:09, , 31F
還好,如果是精心設計,還可以執行你輸入的字串(程式)
10/08 16:09, 31F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 16:16)

10/08 18:05, , 32F
你說到重點了XD windows programming XD
10/08 18:05, 32F

10/08 18:05, , 33F
好啦我必須說有windows programming經驗的就知道~ 沒有的就.
10/08 18:05, 33F

10/08 18:05, , 34F
.....(攤手~)
10/08 18:05, 34F

10/08 19:03, , 35F
匈牙利命名法會讓我想到這篇文章 http://0rz.tw/9e1op
10/08 19:03, 35F

10/08 19:03, , 36F
說得還滿有道理的,可以參考看看:D
10/08 19:03, 36F

10/08 19:22, , 37F
大推XD
10/08 19:22, 37F

10/09 00:53, , 38F
樓主好文我頂
10/09 00:53, 38F

10/09 00:59, , 39F
To jimmycool: 那一篇我也看過,真得寫得不錯,不過他
10/09 00:59, 39F

10/09 01:04, , 40F
好像比較著重在型別之外加上抽象意義,而非把型別直接
10/09 01:04, 40F

10/09 01:05, , 41F
寫上去。我覺得那一篇真得值得推薦!
10/09 01:05, 41F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/09 01:06)

10/09 08:16, , 42F
噢嗯原來是指這個方向的實作 樓主真強大XD
10/09 08:16, 42F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/10 07:53)
文章代碼(AID): #18wv716I (b97902HW)