字串就是一串文字,在 C++ 談到字串的話,一個意義是指字元組成的陣列,最後加上一個空(null)字元 '\0'
,例如底下是個 "hello"
字串:
char text[] = {'h', 'e', 'l', 'l', 'o', '\0'};
之後可以直接使用 text
來代表 "hello"
文字,例如:
cout << text << endl; // 顯示 hello
也可以使用 ""
來包含文字,例如:
char text[] = "hello";
"hello"
是字串字面常量,在這個例子中,雖然沒有指定空字元 '\0'
,但是會自動加上空字元,來看看底下這個程式就可以得知:
#include <iostream>
using namespace std;
int main() {
char text[] = "hello";
for(auto ch : text) {
if(ch == '\0') {
cout << "null";
}
else {
cout << ch;
}
}
cout << endl;
return 0;
}
text
基本上還是字元陣列,空字元用來識別字元陣列單純就只是字元陣列,或者是表示字串。
執行結果:
hellonull
這種字串,其實是延續自 C 風格的字串,因此更多細節可參考我寫的 C 語言文件中〈字串(字元陣列)〉,C 標準函式庫中有一些函式可以協助此類字串的處理,像是〈字串長度、複製、串接〉、〈字串比較、搜尋〉與〈字串轉換、字元測試〉,只不過在 C++ 中,標頭檔必須引用 cstring
而不是 string.h
。
在 C++ 中並不鼓勵使用 C 風格的字串,不過在這邊倒是想藉此探討一些字元細節。例如,有辨法指定 '林'
給 char
變數嗎?這會引發編譯警訊:
// warning: multi-character character constant
// warning: overflow in conversion from 'int' to 'char' changes value
char t = '林';
文字「林」不會是一個位元組就可以儲存的資料,因此引發警訊,你需要使用以下的方式:
char text[] = "林";
若使用 strlen(text)
的話,會得到什麼數字呢?若單純使用 g++
編譯,不加上任何引數的話,答案是看你的原始碼編碼是什麼,如果使用 Big5 撰寫原始碼的話,答案會是 2,如果使用 UTF-8 撰寫原始碼的話,答案會是 3。
記得在〈資料型態〉中談過嗎?char
用來儲存字元資料,但沒有規定什麼是字元資料,若單純使用 g++
編譯,不加上任何引數的話,原始碼中字串怎麼儲存,執行時期 text
就怎麼儲存,當原始碼是 Big5 時,因為 "林"
會用上兩個位元組,strlen(text)
會是 2,當原始碼是 UTF-8 時,"林"
會用上三個位元組,因此 strlen(text)
會是是 3。
現代程式設計鼓勵使用 UTF-8,如果使用 UTF-8 撰寫原始碼,單純使用 g++
編譯,不加上任何引數的話,若是在 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 編碼字串,C++ 17 可以 UTF-8 撰寫原始碼,並在 ""
前置 u8
,指定字串使用 UTF-8 編碼:
char text[] = u8"林";
cout << strlen(text) << endl; // 顯示 3
若不使用 UTF-8 編碼的原始碼,可以使用碼點指定:
char text[] = u8"\u6797";
cout << strlen(text) << endl; // 顯示 3
如果處理中文字串時,想知道有幾個中文字怎麼辦?這要知道 wchar_t
型態,對應的字元常量是 L'林'
這樣的寫法稱為擴充字元字面常量(wide character literal),wchar_t
其實是個整數型態,用來儲存碼點,就現今來說,基本上是指 Unicode。
例如,若以 UTF-8 撰寫原始碼,底下的程式會顯示 Unicode 碼點號碼:
wchar_t ch = L'林'; // 也可以寫 L'\u6797'
cout << ch << endl; // 顯示碼點十進位 26519 或十六進位 6797(視平台而定)
對於字串,也可以使用 wchar_t
宣告陣列進行處理。例如:
#include <iostream>
#include <cstring>
using namespace std;
int main() {
wchar_t text[] = L"良葛格";
cout << wcslen(text); // 顯示 3
return 0;
}
L"良葛格"
這種寫法,稱為擴充字元字串(wide-chararater string),C 風格的字串處理函式,都有對應 wchar_t
的版本,只要將函式名稱的 str 前置改為 wcs 前置就可以了,wcs 就是 wide-chararater string 的縮寫。
wchar_t
並沒有規定大小,只要求必須容納系統中可以使用的字元,C++ 11 制定了 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"良葛格";
C++ 20 制定了 char8_t
,必須夠大到能容納 UTF-8 編碼可表現的全部字元,使用的字元常量或字串常量前要加上 u8
。
至於 char
之間與 wchar_t
、char16_t
、char32_t
間要怎麼轉換呢?這問題基本上涉及 Unicode 碼點要轉換至哪個編碼,若是 Unicode 碼點與 UTF-8 的轉換,可以參考底下的實作(修改自 C++ UTF-8 codepoint conversion):
#include <iostream>
using namespace std;
string toUTF8(int cp);
int toCodePoint(const string &u);
int main(int argc, char *argv[]) {
// 在 UTF-8 終端機下會顯示「林」
cout << toUTF8(L'林') << endl;
cout << toUTF8(u'林') << endl;
cout << toUTF8(U'林') << endl;
string utf8 = u8"林";
// 顯示 26519
cout << toCodePoint(utf8) << endl;
return 0;
}
string toUTF8(int cp) {
char ch[5] = {0x00};
if(cp <= 0x7F) {
ch[0] = cp;
}
else if(cp <= 0x7FF) {
ch[0] = (cp >> 6) + 192;
ch[1] = (cp & 63) + 128;
}
else if(0xd800 <= cp && cp <= 0xdfff) {} // 無效區塊
else if(cp <= 0xFFFF) {
ch[0] = (cp >> 12) + 224;
ch[1]= ((cp >> 6) & 63) + 128;
ch[2]= (cp & 63) + 128;
}
else if(cp <= 0x10FFFF) {
ch[0] = (cp >> 18) + 240;
ch[1] = ((cp >> 12) & 63) + 128;
ch[2] = ((cp >> 6) & 63) + 128;
ch[3]= (cp & 63) + 128;
}
return string(ch);
}
int toCodePoint(const string &u) {
int l = u.length();
if(l < 1) {
return -1;
}
unsigned char u0 = u[0];
if(u0 >=0 && u0 <= 127) {
return u0;
}
if(l < 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(l < 3) {
return -1;
}
unsigned char u2 = u[2];
if(u0 >= 224 && u0 <= 239) {
return (u0 - 224) * 4096 + (u1 - 128) * 64 + (u2 - 128);
}
if (l < 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;
}
string
是 C++ 建議使用的字串型態,也有一些現有的程式庫,可以提供編碼轉換,這之後會介紹。
若字串中包含 \
、"
等字元,會需要轉義,例如:
char text[] = "c:\\workspace\\exercise";
C++ 11 後可以使用原始字串常量 R"(...)"
的寫法,在括號中的文字無需轉義,也可以直接撰寫 "
,例如:
char text1[] = R"(c:\workspace\exercise)";
char text2[] = R"(This is a "test")";
也可以進行換行:
#include <iostream>
using namespace std;
int main() {
char text[] = R"(Your left brain has nothing right.
Your right brain has nothing left.)";
cout << text << endl;
return 0;
}
在原始字串中撰寫的內容都會保留,因此顯示結果會是:
Your left brain has nothing right.
Your right brain has nothing left.
U
、L
等前置字,都可以與原始字串結合,例如 UR"(...)"
、LR"..."
等,不過要留意的是,若結合原始字串,\u1234
這類寫法,就會如實呈現,不會作為碼點表示法的轉義。