作者LoganChien (簡子翔)
看板b97902HW
標題[計程] 陣列簡介 II (來連載吧,繼續害人...
時間Wed Oct 8 00:43:44 2008
前言
上一篇文章我們簡單地介紹了陣列的基本概念,我們可以把陣列想像
成一個表格,每一個表格可以有若干個儲存格,每一個儲存格有一個
索引(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 │
\0 │
0x00 │
空字元(字串終止字元)│
├───┼───┼───┼──────────┤
│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
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)