字串就是一串文字,在 C 談到字串的話,一個意義是指字元組成的陣列,最後加上一個空(null)字元 '\0'
,例如底下是個 "hello"
字串:
char text[] = {'h', 'e', 'l', 'l', 'o', '\0'};
之後可以直接使用 text
來代表 "hello"
文字,例如:
printf("%s\n", text);
也可以使用 “” 來包含文字,例如:
char text[] = "hello";
"hello"
是字串字面常量,在這個例子中,雖然沒有指定空字元 '\0'
,但是會自動加上空字元,來看看底下這個程式就可以得知:
#include <stdio.h>
#include <string.h>
int main(void) {
char text[] = "hello";
int length = sizeof(text) / sizeof(text[0]);
for(int i = 0; i < length; i++) {
if(text[i] == '\0') {
puts("null");
} else {
printf("%c ", text[i]);
}
}
printf("陣列長度 %d\n", length);
printf("字串長度 %d", strlen(text));
return 0;
}
執行結果:
h e l l o null
陣列長度 6
字串長度 5
字串是字元陣列,可以用陣列存取方式取出每個字元,在指定 "hello"
時表面上雖然只有 5 個字元, 但是最後會加上一個空字元 '\0'
,因此 text
就陣列長度而言會是 6,不過就字串長度而言會是 5,strlen
可以取得字串長度,定義在 string.h。
由使用者輸入取得字串值時,需注意不要超過字串(字元陣列)的長度;使用 scanf
從使用者輸入取得字串值,並儲存至字元陣列,只要這麼作就可以了:
char buf[80];
printf("輸入字串:");
scanf("%s", buf);
printf("你輸入的字串為 %s\n", buf);
這個程式片段可以取得使用者的字串輸入,輸入的字串長度不得超過 80 個字元,80 個字元的上限包括空字元,因此實際上可以輸入 79 個字元;如果輸入的字元超出所宣告的上限,會發生不可預期的結果,甚至成為安全弱點,如〈printf 與 scanf〉中最後談到的,預防的方法之一是,限定 scanf
每次執行可以接受的最大字元數,或者是使用 fgets
。
在使用 scanf
取得使用者輸入的字串時,格式指定字是使用 %s
,而變數前不用再加上 &
,因為實際上,字串(字元陣列)變數名稱本身,即表示記憶體位址資訊。
要指定新的字串值給它時,不能像下面的方式指定:
char name[80];
name = "Justin";
而必須要一個字元一個字元的指定至陣列中,並在最後加上空白字元,例如:
char name[80] = {'\0'};
name[0] = 'J';
name[1] = 'u';
name[2] = 's';
name[3] = 't';
name[4] = 'i';
name[5] = 'n';
name[6] = '\0';
puts(str);
這樣的字元指定方式當然相當的不方便,所以 C 提供了字串處理的相關函式,可以協助字串處理,在之後的主題還會說明。
字串的宣告還有指標(Pointer)的宣告方式,這個留待談到指標時再來說明。
前面談到的都是僅包含英文字母的字串,那麼包含中文的字串呢?在談到這個問題前,得先探討一下,是否有辨法指定 '林'
給 char
變數?這會引發編譯警訊:
// warning: multi-character character constant
// warning: overflow in conversion from 'int' to 'char' changes value
char t = '林';
文字「林」不會是一個位元組就可以儲存的資料,因此引發警訊,你需要使用以下的方式:
char text[] = "林";
若使用 strlen(text)
的話,會得到什麼數字呢?若單純使用 gcc
編譯,不加上任何引數的話,答案是看你的原始碼編碼是什麼,如果使用 Big5 撰寫原始碼的話,答案會是 2,如果使用 UTF-8 撰寫原始碼的話,答案會是 3。
記得在〈資料型態〉中談過嗎?char
用來儲存字元資料,但沒有規定什麼是字元資料,若單純使用 gcc
編譯,不加上任何引數的話,原始碼中字串怎麼儲存,執行時期 text
就怎麼儲存,當原始碼是 Big5 時,因為 "林"
會用上兩個位元組,strlen(text)
會是 2,當原始碼是 UTF-8 時,"林"
會用上三個位元組,因此 strlen(text)
會是是 3。
現代程式設計鼓勵使用 UTF-8,如果使用 UTF-8 撰寫原始碼,單純使用 gcc
編譯,不加上任何引數的話,若是在 Windows 的文字模式執行程式,就會出現亂碼,因為 Windows 的文字模式預設採用 Big5(MS950),為了可以看到正確的文字,編譯時可以加上 -fexec-charset=BIG5
,執行時期字串使用 Big5 編碼,這時 strlen(text)
又會是 2 了。
這就要問到一個問題了,字元是什麼呢?C 的 char
又是什麼呢?C 是個歷史悠久的語言,早期用 char
儲存的文字僅需單一位元組,例如 ASCII 的文字,使用 char
代表字元是沒問題,因為 ASCII 既定義了字元集,也定義了字元編碼,在表示 ASCII 的文字時,char
確實就代表字元,然而後來為了支援更多的文字,char
就不再是代表字元了。
char
是用來儲存字元資料,至於存什麼沒有規定,對於 char text[] = "林"
的情況,應該將 text
中每個索引位置當成是碼元(code unit),而不是字元了,因為必須以多個位元組來儲存「林」,因此這類字元在 C 被稱為多位元組字元(multibyte character),技術上來說,是用數個 char
組成的一個字元,如何組成就要看採用哪種編碼了。
如果採用 Big5 編碼,那 "林"
是個 Big5 字元,如果採用 UTF-8 編碼,那 "林"
是個 Unicode 字元,現代程式設計鼓勵用 UTF-8,若要固定使用 UTF-8 編碼字串,C11 可以 UTF-8 撰寫原始碼,並在 ""
前置 u8
,指定字串使用 UTF-8 編碼:
char text[] = u8"林";
printf("字串長度 %d", strlen(text)); // 顯示 3
若不使用 UTF-8 編碼的原始碼,可以使用碼點指定:
char text[] = u8"\u6797";
printf("字串長度 %d", strlen(text)); // 顯示 3
如果處理中文字串時,想知道有幾個中文字怎麼辦?這要知道 wchar_t
型態,對應的字元常量是 L'林'
這樣的寫法稱為擴充字元字面常量(wide character literal),wchar_t
其實是個整數型態,用來儲存碼點,就現今來說,基本上是指 Unicode。
例如,若以 UTF-8 撰寫原始碼,底下的程式會顯示 Unicode 碼點號碼:
wchar_t ch = L'林'; // 也可以寫 L'\u6797'
printf("%d", ch); // 顯示 26519
對於字串,也可以使用 wchar_t
宣告陣列進行處理。例如:
#include <stdio.h>
#include <string.h>
int main(void) {
wchar_t text[] = L"良葛格";
printf("字串長度 %d", wcslen(text)); // 顯示 3
return 0;
}
L"良葛格"
這種寫法,稱為擴充字元字串(wide-chararater string),C 的字串處理函式,都有對應 wchar_t
的版本,只要將函式名稱的 str 前置改為 wcs 前置就可以了,wcs 就是 wide-chararater string 的縮寫。
wchar_t
並沒有規定大小,只要求必須容納系統中可以使用的字元,C11 在 uchar.h 中定義了 char16_t
與 char32_t
,這會讓人誤以為它們用來儲存編碼,其實它們依舊是儲存碼點。
char16_t
可儲存的碼點,必須能涵蓋 UTF-16 編碼可表現的全部字元,使用的字元常量或字串常量前要加上 u
,例如:
char16_t ch = u'林';
char16_t text[] = u"良葛格";
char32_t
可儲存的碼點,必須能涵蓋 UTF-32 編碼可表現的全部字元,使用的字元常量或字串常量前要加上 U
,例如:
char32_t ch = U'林';
char32_t text[] = U"良葛格";
至於 char
之間與 wchar_t
、char16_t
、char32_t
間要怎麼轉換呢?這問題基本上涉及 Unicode 碼點要轉換至哪個編碼,若是 Unicode 碼點與 UTF-8 的轉換,可以參考底下的實作(修改自 C++ UTF-8 codepoint conversion):
#include <stdio.h>
#include <string.h>
void toUTF8(int cp, char* str);
int toCodePoint(char* str, int len);
int main(void) {
char str1[5] = {0x00};
toUTF8(L'林', str1);
printf("%s\n", str1); // 在 UTF-8 終端機下會顯示「林」
char str2[] = u8"林"; // 「林」會使用三個位元組
printf("%d\n", toCodePoint(str2, 3)); // 顯示 26519
return 0;
}
void toUTF8(int cp, char* str) {
if(cp <= 0x7F) {
str[0] = cp;
}
else if(cp <= 0x7FF) {
str[0] = (cp >> 6) + 192;
str[1] = (cp & 63) + 128;
}
else if(0xd800 <= cp && cp <= 0xdfff) {} // 無效區塊
else if(cp <= 0xFFFF) {
str[0] = (cp >> 12) + 224;
str[1]= ((cp >> 6) & 63) + 128;
str[2]= (cp & 63) + 128;
}
else if(cp <= 0x10FFFF) {
str[0] = (cp >> 18) + 240;
str[1] = ((cp >> 12) & 63) + 128;
str[2] = ((cp >> 6) & 63) + 128;
str[3]= (cp & 63) + 128;
}
}
int toCodePoint(char* u, int len) {
if(len < 1) {
return -1;
}
unsigned char u0 = u[0];
if(u0 >=0 && u0 <= 127) {
return u0;
}
if(len < 2) {
return -1;
}
unsigned char u1 = u[1];
if (u0 >= 192 && u0 <= 223) {
return (u0 - 192) * 64 + (u1 - 128);
}
if(u[0] == 0xed && (u[1] & 0xa0) == 0xa0) {
return -1; //code points, 0xd800 to 0xdfff
}
if(len < 3) {
return -1;
}
unsigned char u2 = u[2];
if(u0 >= 224 && u0 <= 239) {
return (u0 - 224) * 4096 + (u1 - 128) * 64 + (u2 - 128);
}
if (len < 4) {
return -1;
}
unsigned char u3 = u[3];
if(u0>=240 && u0<=247) {
return (u0 - 240) * 262144 + (u1 - 128) * 4096 + (u2 - 128) * 64 + (u3 - 128);
}
return -1;
}