在 Windows 之中,想將標準字元 (char
) 字串轉換成寬字元 (wchar_t
) 字串,通常會使用 MultiByteToWideChar()
這個 Win32 API。這個 API 的 prototype 如下:
int MultiByteToWideChar(
UINT CodePage, // [in] 說明輸入字串的編碼。本文假設都為 UTF-8。
DWORD dwFlags, // [in] 選項旗標。本文不討論。
LPCSTR lpMultiByteStr, // [in] 指向輸入字串的指標。
int cchMultiByte, // [in] 指定要處理輸入字串 char 個數。-1 表示處理到結尾的空字元。
LPWSTR lpWideCharStr, // [out; optional] 輸出的 buffer。
int cchWideChar // [in] 描述 lpWideCharStr 的 buffer 大小,單位為 sizeof(wchar_t)。
);
比較有趣的設計是:如果 lpWideCharStr
傳入的值是 nullptr
,而 cchWideChar
是 0 的話,這個 API 的回傳值就會是「該字串完成轉換後所需要的 buffer 大小」。這是因為在不同的編碼下每個「文字」的大小可能不一樣;混用各種文字的字串,經過編碼轉換後,所需要的長度並沒有一定的公式可循,必須得真的轉換過一次,才能得知所需的長度 。
由於考慮到記憶體配置與安全性的問題,一般人在使用這個 API 時,會呼叫兩次:第一次取得所需的 buffer 大小、配置適當的記憶體後,再呼叫第二次,把轉換結果填入 buffer。把整個流程寫成函式的話,會長成這個樣子:
std::wstring WCS_from_UTF8(const char* in_str) {
size_t cch_in = strlen(in_str);
// ---- 第一次呼叫,取得所需的 buffer size
int buf_size = ::MultiByteToWideChar(
CP_UTF8, // 可以處理 ANSI 和 UTF-8 字串,已經夠用。
0, // 暫不考慮。
in_str,
-1, // 處理到結尾的空字元
nullptr, // 先不輸出到 buffer
0 // 先不輸出到 buffer
);
wchar_t* out_buf = new wchar_t[buf_size]; // buf_size 已經把空字元算進去了,所以不需要額外加 1。
// ---- 第二次呼叫,正式取得轉換後的結果
::MultiByteToWideChar(
CP_UTF8,
0,
in_str,
-1, // 以上參數都不變
out_buf, // 結果會填入 out_buf
buf_size
);
std::wstring wstr{out_buf}; // 將結果轉成 std::wstring,這樣才不怕 client 忘記歸還記憶體。
delete[] out_buf; // 記得要歸還暫存用的 buffer
return wstr;
}
同一個 API 要呼叫兩次看起來有點礙眼,但好歹是可以正常運作的,而且好像真的也沒什麼方法可以得知所需的記憶體,我也就當作這是最佳解了。
....直到我今天無意見看到一篇 MFC 的 Technical Note。
那篇文章在解釋 MFC 提供的編碼轉換巨集所使用的技巧。為了追求執行效率,這些巨集使用了兩個技巧:
- 利用
_alloca()
來取代new
,因為前者在 stack(而非後者會使用的 heap)配置記憶體。在 stack 配置記憶體的速度比 heap 快很多,而且也不需要手動釋放記憶體(因為函式 return 時 stack 的記憶體就會被自動歸還了)。 - 配置 output buffer 所需的記憶體時,配置稍微多一點的記憶體,這樣就可以省掉「取得所需的 buffer size」的那次
MultiByteToWideChar()
呼叫了。
我們先來看第二點:這個「稍微多一點」到底要多多少才夠用呢?我們可以來分析一下。
我們的來源字串,是用 UTF-8 編碼的。針對 Unicode 的碼點 (code point) 所在範圍不同,UTF-8 可能用 1 到 4 個 byte 來表示。
碼點範圍 | UTF-8 所需大小 | UTF-16 所需大小 | 轉換後大小倍數 | 說明 |
---|---|---|---|---|
0x00 ~ 0x7F | 1 byte | 2 byte | 2 | 即原本 7-bit ASCII 碼的範圍 |
0x080 ~ 0x7FF | 2 bytes | 2 bytes | 1 | |
0x07FF ~ 0xFFFF | 3 bytes | 2 bytes | 0.67 | 小於 0xFFFF 的碼點都屬於基本多文種平面(Basic Multilingual Plane, BMP),因此 UTF-16 還是只需要 1 個 wchar_t (也就是 2 bytes)就可以儲存。 |
0x10000 ~ 10FFFF | 4 bytes | 4 bytes | 1 | 超出 BMP 的碼點 UTF-16 需要使用代理對 (Surrogate Pair) 來表示,所以需要 2 個 wchar_t ,即 4 bytes。 |
經過上述的分析後,我們可以知道:長度為 N bytes(也就是 N 個 char
)的 UTF-8 字串,轉換為 UTF-16 後,長度不會超過 2N bytes(也就是 N 個 wchar_t
)。所以,我們只要用 strlen()
算出原始字串的 char
個數後,再保留「同樣個數 + 1(字串結尾空字元)」個 wchar_t
做為輸出 buffer 即可。
接下來回頭看第一點。我也是看了這個說明,才知道有 _alloca()
這個函式、可以在 stack 動態配置記憶體。不過如果我們去看 _alloca()
的說明文件,會發現現在已經不建議使用這個函式了。原因是通常程式保留的 stack 都會比 heap 小,如果不小心傳入太大的值進 _alloca()
,可能會導致 stack overflow,輕一點的話程式直接當掉,重一點可能會導致安全性問題。所以文件中建議改用 _malloca()
。
如果使用者輸入的 buffer size 不會太大,那麼 _malloca()
就會在 stack 配置記憶體;但如果 size 太大,則會改從 heap 配置。可以兼顧效率以及安全性。然而,和 _alloca()
不同的是,利用 _malloca()
配置的記憶體,一定要使用 _freea()
歸還!這會讓程式寫起來沒有那麼「優雅」...... 不過在正確性前面,優雅也只能犧牲了。
綜合上述的討論,順便加上一些保護機制(如果空間配置 or 轉換時遇到問題,就直接回傳空字串),我們可以把 WCS_from_UTF8()
改寫如下:
wstring WCS_from_UTF8(const char* in_str) {
wstring wcs;
size_t cch_in = strlen(in_str);
if (cch_in) {
wchar_t* out_buf = reinterpret_cast<wchar_t*>(_malloca(
(cch_in + 1) * sizeof(wchar_t)));
if (out_buf) {
int out_count = ::MultiByteToWideChar(
CP_UTF8,
0,
in_str,
-1,
out_buf,
static_cast<int>(cch_in + 1) // 長度必須包含結尾的空字元
);
if (out_count > 0) {
// out_count 包含了結尾空字元。
// 但使用 basic_string::assign 時,不應該包含空字元,
// 否則之後 append text 會出問題。因此這邊要減 1。
wcs.assign(out_buf, static_cast<size_t>(out_count - 1));
}
_freea(out_buf); // 別忘了釋放記憶體
}
}
return wcs;
}
透過相似的分析,也可以找出從 UTF-16 轉成 UTF-8 時,所需要的最大 buffer size 為 wchar_t
個數的 3 倍再加 1。範例程式如下:
std::string UTF8_from_WCS(const wchar_t* in_wcs) {
std::string u8str;
size_t wcs_count = wcslen(in_wcs);
if (wcs_count) {
int buf_len = wcs_count * 3 + 1;
char* out_buf = reinterpret_cast<char*>(_malloca(buf_len * sizeof(char)));
if (out_buf) {
int out_count = ::WideCharToMultiByte(
CP_UTF8,
0,
in_wcs,
-1,
out_buf,
buf_len,
NULL, // 使用 CP_UTF8 時,這個參數必須是 NULL
NULL // 使用 CP_UTF8 時,這個參數必須是 NULL
);
if (out_count > 0) {
// out_count 包含了結尾空字元。
// 但使用 basic_string::assign 時,不應該包含空字元,
// 否則之後 append text 會出問題。因此這邊要減 1。
u8str.assign(out_buf, static_cast<size_t>(out_count - 1));
}
_freea(out_buf); // 別忘了釋放記憶體
}
}
return u8str;
}