[計程] 陣列簡介 II (來連載吧,繼續害人...
前言
上一篇文章我們簡單地介紹了陣列的基本概念,我們可以把陣列想像
成一個表格,每一個表格可以有若干個儲存格,每一個儲存格有一個
索引(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)
推
10/08 00:47, , 1F
10/08 00:47, 1F
推
10/08 00:51, , 2F
10/08 00:51, 2F
→
10/08 00:54, , 3F
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
10/08 02:03, 6F
推
10/08 02:03, , 7F
10/08 02:03, 7F
推
10/08 02:04, , 8F
10/08 02:04, 8F
推
10/08 02:04, , 9F
10/08 02:04, 9F
推
10/08 02:08, , 10F
10/08 02:08, 10F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/08 02:19)
→
10/08 02:22, , 11F
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
10/08 08:12, 14F
→
10/08 08:12, , 15F
10/08 08:12, 15F
推
10/08 09:53, , 16F
10/08 09:53, 16F
推
10/08 12:31, , 17F
10/08 12:31, 17F
→
10/08 12:52, , 18F
10/08 12:52, 18F
→
10/08 15:55, , 19F
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
10/08 15:58, 22F
→
10/08 16:00, , 23F
10/08 16:00, 23F
→
10/08 16:01, , 24F
10/08 16:01, 24F
→
10/08 16:02, , 25F
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
10/08 16:06, 28F
→
10/08 16:07, , 29F
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
10/08 18:05, 32F
推
10/08 18:05, , 33F
10/08 18:05, 33F
推
10/08 18:05, , 34F
10/08 18:05, 34F
推
10/08 19:03, , 35F
10/08 19:03, 35F
→
10/08 19:03, , 36F
10/08 19:03, 36F
推
10/08 19:22, , 37F
10/08 19:22, 37F
推
10/09 00:53, , 38F
10/09 00:53, 38F
→
10/09 00:59, , 39F
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
10/09 08:16, 42F
※ 編輯: LoganChien 來自: 140.112.241.166 (10/10 07:53)