詳解C++中的ANSI與Unicode和UTF8三種字符編碼基本原理與相互轉(zhuǎn)換
1、概述
在日常的軟件開發(fā)過程中,會(huì)時(shí)不時(shí)地去處理不同編碼格式的字符串,特別是在處理文件路徑的相關(guān)場景中,比如我們要通過路徑去讀寫文件、通過路徑去加載庫文件等。常見的字符編碼格式有ANSI窄字節(jié)編碼、Unicode寬字節(jié)編碼以及UTF8可變長編碼。在Linux系統(tǒng)中,主要使用UTF8編碼;在Windows系統(tǒng)中,既支持ANSI編碼,也支持Unicode編碼。
通用的大小寫字母和數(shù)字則使用全球統(tǒng)一的固定編碼,即ASCII碼。
ANSI編碼是各個(gè)國家不同語種下的字符編碼,其字符的編碼值只在該語種中有效,不是全球統(tǒng)一編碼的,比如中文的GB2312編碼就是簡體中文的ANSI編碼。
Unicode編碼則是全球統(tǒng)一的雙字節(jié)編碼,所有語種的字符在一起統(tǒng)一的編碼,每個(gè)字符的編碼都是全球唯一的。
UTF8編碼是一種可變長的寬字節(jié)編碼,也是一種全球統(tǒng)一的字符編碼。
本文將以WIndows中使用Visual Studio進(jìn)行C++編程時(shí)需要處理的字符編碼問題為切入點(diǎn),詳細(xì)講解一下字符編碼的相關(guān)內(nèi)容。
2、Visual Studio中的字符編碼
在Visual Studio中編寫C++代碼時(shí),該如何指定字符串的編碼呢?其實(shí)很簡單,使用雙引號(hào)括住的字符串,使用的就是ANSI窄字節(jié)編碼;使用L+雙引號(hào)括住的字符串,使用的就是Unicode寬字節(jié)編碼,如下所示:
char* pStr = "This is a Test."; // ANSI編碼 WCHAR* pWStr = L"This is a Test."; // Unicode寬字節(jié)編碼
我們也可以使用_T宏定義來指定字符串的編碼格式:
TCHAR* pStr = _T("This is a Test.");
設(shè)置_T后,則由工程配置屬性中的字符集設(shè)置來確定到底是使用哪種編碼:
如果選擇多字節(jié)字符集,_T就被解釋為雙引號(hào),即使用ANSI窄字節(jié)編碼;如果選擇Unicode字符集,_T就被解釋為L,即使用Unicode寬字節(jié)編碼。
其實(shí),如果在工程配置中選擇使用Unicode字符集,工程中會(huì)添加一個(gè)_UNICODE宏,如下所示:
如果選擇多字節(jié)字符集,則沒有_UNICODE宏。代碼中正是通過這個(gè)宏來判定到底使用哪種編碼的,比如對_T的判斷:
#ifdef _UNICODE #define _T(X) L(X) #else #define _T(X) (X) #endif // _UNICODE
和字符編碼相對應(yīng)的,Windows系統(tǒng)提供兩個(gè)版本的API,比如給窗口設(shè)置文字的API函數(shù),一個(gè)是支持ANSI窄字節(jié)編碼的SetWindowTextA(ANSI窄字節(jié)版本),一個(gè)是支持Unicode寬字節(jié)編碼的SetWindowTextW(Wide寬字節(jié)版本)。我們也可以直接調(diào)用SetWindowText,然后由_UNICODE宏判斷到底使用哪個(gè)版本,如下:
#ifdef _UNICODE #define SetWindowText SetWindowTextW #else #define SetWindowText SetWindowTextA #endif // !UNICODE
3、ANSI窄字節(jié)編碼
ANSI編碼是不同語種下的字符編碼,比如GB2312字符編碼就是簡體中文的本地編碼。
ANSI編碼是個(gè)本地范疇,只適用于對應(yīng)的語種,每個(gè)字符的編碼不是全球唯一的編碼,只在對應(yīng)的語種中有效。對于中文GB2312編碼的字符串,如果當(dāng)成英文的ANSI編碼來解析,則結(jié)果會(huì)是亂碼!
但是對于大小寫英文字母和數(shù)字的ANSI編碼,是字符ASCII碼,英文字母和數(shù)字的ACSII碼是全球統(tǒng)一的,比如大寫字母A的ASCII碼是65(十六進(jìn)制是41H),數(shù)字0的ASCII碼是48(十六進(jìn)制是30H)。所以在所有語種中,大小寫字母及數(shù)字的ANSI編碼,都是能識(shí)別的。不同語種下的本地文字字符,一般是不能相互識(shí)別的。
使用中文ANSI編碼的字符串開發(fā)的程序(代碼中使用的都是中文字符串,使用的是ANSI窄字節(jié)編碼),拿到俄文操作系統(tǒng)中可能顯示的都是亂碼,因?yàn)樵诙砦牡腁NSI編碼中只識(shí)別俄文的ANSI編碼出來的字符串,無法識(shí)別中文ANSI編碼的字符串。這里主要有兩類字符亂碼問題,一是UI界面上顯示的文字是亂碼;二是使用路徑去創(chuàng)建文件或訪問文件時(shí)會(huì)因?yàn)槁窂街械淖址莵y碼,導(dǎo)致文件創(chuàng)建或訪問失敗。
4、Unicode寬字節(jié)編碼
Unicode編碼是全球統(tǒng)一的字符編碼,每個(gè)語種下的每個(gè)字符的編碼值都是全球唯一的,即在Unicode編碼集中可以識(shí)別每個(gè)語種下的所有字符。所以為了實(shí)現(xiàn)軟件對多語種(多國語言)的支持,我們在開發(fā)軟件時(shí)要選擇Unicode字符編碼,使用Unicode編碼的字符串,調(diào)用Unicode版本的API。
系統(tǒng)在提供包含字符串參數(shù)的API時(shí),都會(huì)提供兩個(gè)版本,一個(gè)是ANSI版本的,一個(gè)是Unicode版本的,主要體現(xiàn)在對字符串編碼的處理上,比如SetWindowTextA(ANSI版本)和SetWindowTextW(Wide寬字節(jié)Unicode版本)。我們可以直接調(diào)用W版本API,但一般我們調(diào)用API時(shí),我們不指定調(diào)用哪個(gè)版本,是通過設(shè)置工程屬性中的編碼格式來確定使用哪個(gè)版本:
#ifdef _UNICODE #define SetWindowText SetWindowTextW #else #define SetWindowText SetWindowTextA #endif // !UNICODE
具體情況已在上面的“Visual Studio中的字符編碼”章節(jié)中詳細(xì)講述,此處不再贅述。
在Unicode編碼中,每個(gè)字符都占兩個(gè)字節(jié)。對于大小寫字母和數(shù)字,當(dāng)他們出現(xiàn)在字符串中時(shí),對應(yīng)的內(nèi)存中存放的是它們的ASCII碼值,只占一個(gè)字節(jié),在Unicode 2字節(jié)編碼中,高位將填充0。
5、UTF8編碼
UTF8編碼是可變長字符編碼格式,是一種緊湊型存儲(chǔ)的編碼格式,也是一種寬字節(jié)的、全球統(tǒng)一的編碼格式。UTF8編碼中的所有字符,包括不同語種下面的字符,都是全球唯一編碼的,在所有的系統(tǒng)都能識(shí)別出來。
UTF8編碼中,能用一個(gè)字節(jié)存放的,就用一個(gè)字節(jié)存放,比如大小寫字母和數(shù)字,在字符串中存放的ASCII碼,只需要一個(gè)字節(jié)去存放就夠了。所以在UTF8編碼中,大小寫字母和數(shù)字只占一個(gè)字節(jié)。我們常用的中文字符,一個(gè)字符則占用3個(gè)字節(jié)。
UTF8編碼之所以稱之為可變長的,是因?yàn)槠涓鶕?jù)字符需要的實(shí)際存儲(chǔ)空間大小來編碼的,比如大小寫字母和數(shù)字的存儲(chǔ)只需要1個(gè)字節(jié)就夠了,所以它們只占一個(gè)字節(jié),而一個(gè)中文字符則占三個(gè)字節(jié)。
6、如何使用字符編碼
Windows系統(tǒng)主要使用Unicode編碼,Linux則使用UTF8編碼,后臺(tái)服務(wù)器一般使用的都是Linux系統(tǒng),而客戶端是運(yùn)行在Windows操作系統(tǒng)上的。一般客戶端與服務(wù)器交互的數(shù)據(jù)的字符串編碼統(tǒng)一使用全球統(tǒng)一編碼的UTF8編碼。
客戶端收到UTF8編碼的字符串后,需要將UTF8字符換轉(zhuǎn)換后顯示在界面上。如果客戶端使用的是Unicode編碼字符集,將UTF8編碼的字符串轉(zhuǎn)換成Unicode編碼的字符串后再顯示到界面上;如果客戶端使用的是多字節(jié)ANSI編碼,則需要再將Unicode編碼的字符串轉(zhuǎn)成ANSI編碼的字符串。
這里注意一下,UTF8編碼的字符串要轉(zhuǎn)成ANSI編碼的,不能直接將UTF8轉(zhuǎn)成ANSI,需要先將UTF8轉(zhuǎn)成Unicode,然后再將Unicode轉(zhuǎn)成ANSI。
為了實(shí)現(xiàn)軟件對多語種(多國語言)的支持,我們在開發(fā)Windows軟件時(shí)要選擇Unicode字符編碼,使用Unicode編碼的字符串,調(diào)用Unicode版本的API。
此外,對于一些開源的項(xiàng)目,提供的API接口中有字符串參數(shù)的,一般都明確指定字符串編碼為UTF8。因?yàn)橐话闱闆r下開源庫都支持跨平臺(tái),既支持Windows平臺(tái),也支持Linux平臺(tái),所以要選擇使用通用的、大家都是識(shí)別的UTF8編碼。
比如在輕便型數(shù)據(jù)庫sqlite開源庫中,用于打開數(shù)據(jù)庫文件的接口sqlite3_open,就明確指定使用UTF8編碼的字符串:
** ** ^URI hexadecimal escape sequences (%HH) are supported within the path and ** query components of a URI. A hexadecimal escape sequence consists of a ** percent sign - "%" - followed by exactly two hexadecimal digits ** specifying an octet value. ^Before the path or query components of a ** URI filename are interpreted, they are encoded using UTF-8 and all ** hexadecimal escape sequences replaced by a single byte containing the ** corresponding octet. If this process generates an invalid UTF-8 encoding, ** the results are undefined. ** ** <b>Note to Windows users:</b> The encoding used for the filename argument ** of sqlite3_open() and sqlite3_open_v2() must be UTF-8, not whatever ** codepage is currently defined. Filenames containing international ** characters must be converted to UTF-8 prior to passing them into ** sqlite3_open() or sqlite3_open_v2(). ** ** <b>Note to Windows Runtime users:</b> The temporary directory must be set ** prior to calling sqlite3_open() or sqlite3_open_v2(). Otherwise, various ** features that require the use of temporary files may fail. ** ** See also: [sqlite3_temp_directory] */ SQLITE_API int sqlite3_open( const char *filename, /* Database filename (UTF-8) */ sqlite3 **ppDb /* OUT: SQLite db handle */ ); SQLITE_API int sqlite3_open16( const void *filename, /* Database filename (UTF-16) */ sqlite3 **ppDb /* OUT: SQLite db handle */ ); SQLITE_API int sqlite3_open_v2( const char *filename, /* Database filename (UTF-8) */ sqlite3 **ppDb, /* OUT: SQLite db handle */ int flags, /* Flags */ const char *zVfs /* Name of VFS module to use */ );
對于使用Unicode編碼的Windows程序,代碼中使用的都是Unicode編碼的字符串,在調(diào)用sqlite3_open接口之前,需要將Unicode編碼的字符串轉(zhuǎn)成UTF8編碼的。如果收到開源庫中回調(diào)上來的UTF8編碼的字符串?dāng)?shù)據(jù),則需要將UTF8編碼的字符串轉(zhuǎn)成Unicode后,才能顯示到UI界面上,才能使用轉(zhuǎn)碼后的Unicode字符串去調(diào)用Windows系統(tǒng)API。
7、三種字符編碼之間的相互轉(zhuǎn)換(附源碼)
有朋友曾經(jīng)提出這樣的疑問,是不是我在Windows下把一個(gè)雙引號(hào)括起來的ANSI窄字節(jié)字符串賦值給WCHAR寬字節(jié)的指針:
WCHAR* pStr = "測試字符串";
字符串就能自動(dòng)轉(zhuǎn)換成Unicode寬字節(jié)?答案是否定的,這樣的賦值操作并不會(huì)做字符編碼轉(zhuǎn)換,右側(cè)的僅僅是字符串的首地址,作為地址,可以賦值給很多數(shù)據(jù)類型,比如int、void*、char*等等。
那可能有人會(huì)說,那為啥我在Unicode下,將一個(gè)ANSI編碼的字符串傳給MFC庫中的CString類對象時(shí)會(huì)自動(dòng)轉(zhuǎn)換成Unicode寬字符呢?這和上面的情況不一樣的,是因?yàn)镃String類重載了賦值操作符函數(shù),在函數(shù)內(nèi)部做了字符編碼的轉(zhuǎn)換,代碼如下:
const CUIString& CUIString::operator=(LPCSTR lpsz) { int nSrcLen = lpsz != NULL ? lstrlenA(lpsz) : 0; AllocBeforeWrite(nSrcLen); _ANSIToUnicode(m_pchData, lpsz, nSrcLen+1); ReleaseBuffer(); return *this; }
一般情況下,是需要我們自己去編寫字符編碼轉(zhuǎn)換的代碼的。下面來看一下,我們在進(jìn)行Windows C++編程時(shí),需要調(diào)用哪些API接口實(shí)現(xiàn)上述三種編碼之間的轉(zhuǎn)換。
7.1、ANSI編碼與Unicode編碼之間的轉(zhuǎn)換
ANSI轉(zhuǎn)成Unicode的代碼如下:
/*============================================================================= 函 數(shù) 名: AnsiToUnicode 功 能: 實(shí)現(xiàn)將char型buffer(ANSI編碼)中的內(nèi)容安全地拷貝到指定的WChar型(Unicode編碼)的buffer中 參 數(shù): char* pchSrc [in] 源字符串 WCAHR* pchDest [out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void AnsiToUnicode( const char* pchSrc, WCHAR* pchDest, int nDestLen ) { if ( pchSrc == NULL || pchDest == NULL ) { return; } int nTmpLen = MultiByteToWideChar(CP_ACP, 0, pchSrc, -1, NULL, 0); WCHAR* pWTemp = new WCHAR[nTmpLen + 1]; memset(pWTemp, 0, (nTmpLen + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_ACP, 0, pchSrc, -1, pWTemp, nTmpLen + 1); UINT nLen = wcslen(pWTemp); if (nLen + 1 > (nDestLen / sizeof(WCHAR))) { wcsncpy(pchDest, pWTemp, nDestLen / sizeof(WCHAR) - 1); pchDest[nDestLen / sizeof(WCHAR) - 1] = 0; } else { wcscpy(pchDest, pWTemp); } delete []pWTemp; }
Unicode轉(zhuǎn)成ANSI的代碼如下:
/*============================================================================= 函 數(shù) 名: UnicodeToAnsi 功 能: 實(shí)現(xiàn)將WCHAR型buffer(Unicode編碼)中的內(nèi)容安全地拷貝到指定的char型(ANSI編碼)的buffer中 參 數(shù): WCHAR* pchSrc [in] 源字符串 char* pchDest[out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void UnicodeToAnsi(const WCHAR* pchSrc, char* pchDest, int nDestLen ) { if ( pchDest == NULL || pchSrc == NULL ) { return; } const WCHAR* pWStrSRc = pchSrc; int nTmplen = WideCharToMultiByte(CP_ACP, 0, pWStrSRc, -1, NULL, 0, NULL, NULL); char* pTemp = new char[nTmplen + 1]; memset(pTemp, 0, nTmplen + 1); WideCharToMultiByte(CP_ACP, 0, pWStrSRc, -1, pTemp, nTmplen + 1, NULL, NULL); int nLen = strlen(pTemp); if (nLen + 1 > nDestLen) { strncpy(pchDest, pTemp, nDestLen - 1); pchDest[nDestLen - 1] = 0; } else { strcpy(pchDest, pTemp); } delete []pTemp; }
7.2、UTF8編碼與Unicode編碼之間的轉(zhuǎn)換
UTF8轉(zhuǎn)成Unicode的代碼如下:
/*============================================================================= 函 數(shù) 名: Utf8ToUnicode 功 能: 實(shí)現(xiàn)將char型的buffer(utf8編碼)中的內(nèi)容安全地拷貝到指定的WCHAR型buffer(Unicode編碼)中 參 數(shù): char* pchSrc [in] 源字符串 WCHAR* pchDest [out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void Utf8ToUnicode( const char* pchSrc, WCHAR* pchDest, int nDestLen ) { if ( pchSrc == NULL || pchDest == NULL ) { return; } int nTmpLen = MultiByteToWideChar(CP_UTF8, 0, pchSrc, -1, NULL, 0); WCHAR* pWTemp = new WCHAR[nTmpLen + 1]; memset(pWTemp, 0, (nTmpLen + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_UTF8, 0, pchSrc, -1, pWTemp, nTmpLen + 1); UINT nLen = wcslen(pWTemp); if (nLen + 1 > (nDestLen / sizeof(WCHAR))) { wcsncpy(pchDest, pWTemp, nDestLen / sizeof(WCHAR) - 1); pchDest[nDestLen / sizeof(WCHAR) - 1] = 0; } else { wcscpy(pchDest, pWTemp); } delete []pWTemp; }
Unicode轉(zhuǎn)成UTF8的代碼如下:
/*============================================================================= 函 數(shù) 名: UnicodeToUtf8 功 能: 實(shí)現(xiàn)將WCHAR型buffer(Unicode編碼)中的內(nèi)容安全地拷貝到指定的char型的buffer(utf8編碼)中 參 數(shù): WCAHR* pchSrc [in] 源字符串 char* pchDest [out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void UnicodeToUtf8(const WCHAR* pchSrc, char* pchDest, int nDestLen ); { if ( pchDest == NULL || pchSrc == NULL ) { return; } const WCHAR* pWStrSRc = pchSrc; int nTmplen = WideCharToMultiByte(CP_UTF8, 0, pWStrSRc, -1, NULL, 0, NULL, NULL); char* pTemp = new char[nTmplen + 1]; memset(pTemp, 0, nTmplen + 1); WideCharToMultiByte(CP_UTF8, 0, pWStrSRc, -1, pTemp, nTmplen + 1, NULL, NULL); int nLen = strlen(pTemp); if (nLen + 1 > nDestLen) { strncpy(pchDest, pTemp, nDestLen - 1); pchDest[nDestLen - 1] = 0; } else { strcpy(pchDest, pTemp); } delete []pTemp; }
7.3、ANSI編碼與UTF8編碼之間的轉(zhuǎn)換
ANSI與UTF8之間是不能直接轉(zhuǎn)換的,需要先轉(zhuǎn)成Unicode之后才能轉(zhuǎn)到目標(biāo)編碼。
ANSI轉(zhuǎn)成UTF8的代碼如下:
/*============================================================================= 函 數(shù) 名: AnsiToUtf8 功 能: 實(shí)現(xiàn)將char型buffer(ANSI編碼)中的內(nèi)容安全地拷貝到指定的char型的buffer(utf8編碼)中 參 數(shù): char* pchSrc [in] 源字符串 char* pchDest [out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void AnsiToUtf8( const char* pchSrc, char* pchDest, int nDestLen ) { if (pchSrc == NULL || pchDest == NULL) { return; } // 先將ANSI轉(zhuǎn)成Unicode int nUnicodeBufLen = MultiByteToWideChar(CP_ACP, 0, pchSrc, -1, NULL, 0); WCHAR* pUnicodeTmpBuf = new WCHAR[nUnicodeBufLen + 1]; memset(pUnicodeTmpBuf, 0, (nUnicodeBufLen + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_ACP, 0, pchSrc, -1, pUnicodeTmpBuf, nUnicodeBufLen + 1); // 再將Unicode轉(zhuǎn)成utf8 int nUtf8BufLen = WideCharToMultiByte(CP_UTF8, 0, pUnicodeTmpBuf, -1, NULL, 0, NULL, NULL); char* pUtf8TmpBuf = new char[nUtf8BufLen + 1]; memset(pUtf8TmpBuf, 0, nUtf8BufLen + 1); WideCharToMultiByte(CP_UTF8, 0, pUnicodeTmpBuf, -1, pUtf8TmpBuf, nUtf8BufLen + 1, NULL, NULL); int nLen = strlen(pUtf8TmpBuf); if (nLen + 1 > nDestLen) { strncpy(pchDest, pUtf8TmpBuf, nDestLen - 1); pchDest[nDestLen - 1] = 0; } else { strcpy(pchDest, pUtf8TmpBuf); } delete[]pUtf8TmpBuf; delete[]pUnicodeTmpBuf; }
UTF8轉(zhuǎn)成ANSI的代碼如下:
/*============================================================================= 函 數(shù) 名: Utf8ToAnsi 功 能: 實(shí)現(xiàn)將char型buffer(utf8編碼)中的內(nèi)容安全地拷貝到指定的char型的buffer(ANSI編碼)中 參 數(shù): char* pchSrc [in] 源字符串 char* pchDest [out] 目標(biāo)buf int nDestLen [in] 目標(biāo)buf長度(注意:以字節(jié)為單位,不是以字符個(gè)數(shù)為單位) 注 意: 無 返 回 值: 無 =============================================================================*/ void Utf8ToAnsi(const char* pchSrc, char* pchDest, int nDestLen) { if (pchSrc == NULL || pchDest == NULL) { return; } // 先將utf8轉(zhuǎn)成Unicode int nUnicdeBufLen = MultiByteToWideChar(CP_UTF8, 0, pchSrc, -1, NULL, 0); WCHAR* pUnicodeTmpBuf = new WCHAR[nUnicdeBufLen + 1]; memset(pUnicodeTmpBuf, 0, (nUnicdeBufLen + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_UTF8, 0, pchSrc, -1, pUnicodeTmpBuf, nUnicdeBufLen + 1); // 再將Unicode轉(zhuǎn)成utf8 int nAnsiBuflen = WideCharToMultiByte(CP_ACP, 0, pUnicodeTmpBuf, -1, NULL, 0, NULL, NULL); char* pAnsiTmpBuf = new char[nAnsiBuflen + 1]; memset(pAnsiTmpBuf, 0, nAnsiBuflen + 1); WideCharToMultiByte(CP_ACP, 0, pUnicodeTmpBuf, -1, pAnsiTmpBuf, nAnsiBuflen + 1, NULL, NULL); int nLen = strlen(pAnsiTmpBuf); if (nLen + 1 > nDestLen) { strncpy(pchDest, pAnsiTmpBuf, nDestLen - 1); pchDest[nDestLen - 1] = 0; } else { strcpy(pchDest, pTemp); } delete []pAnsiTmpBuf; delete []pUnicodeTmpBuf; }
8、Windows系統(tǒng)對使用ANSI窄字節(jié)字符編碼的程序的兼容
現(xiàn)在的Windows程序基本都用Unicode字符編碼了,工程屬性中將字符集都設(shè)置成了Unicode字符集,代碼中都使用Unicode編碼的字符串。但是還有一些老的程序使用的還是ANSI窄字節(jié)的字符。那這些老的程序如何才能在外文的操作系統(tǒng)中正常運(yùn)行呢?微軟提供了一種兼容這些老程序的辦法。
可以到Windows控制面板的區(qū)域語言設(shè)置中將非Unicode語言設(shè)置成程序中使用的字符語種即可,相關(guān)設(shè)置的操作步驟截圖如下:
在上圖中選擇程序中字符使用的語種即可。
下面我們來看看使用ANSI編碼的程序放到外文操作系統(tǒng)中運(yùn)行為什么會(huì)出現(xiàn)亂碼。假設(shè)將某程序中使用的是中文ANSI窄字節(jié)編碼的字符串,放到英文操作系統(tǒng)中運(yùn)行,默認(rèn)情況下,UI界面上會(huì)顯示亂碼。至于為什么會(huì)顯示亂碼,是因?yàn)橛⑽牟僮飨到y(tǒng)中默認(rèn)情況下設(shè)置的非Unicode語言是英語(美國):
這個(gè)非Unicode語言設(shè)置直接影響我們調(diào)用MultiByteToWideChar和WideCharToMultiByte接口中的CP_ACP標(biāo)記對應(yīng)的本地ANSI字符集編碼庫。在上面界面中如果將非Unicode語言設(shè)置成英語(美國),則使用英文的ANSI字符編碼庫;如果設(shè)置成中文簡體,則使用中文簡體的ANSI字符集編碼庫。
程序中調(diào)用API函數(shù)SetWindowTextA給程序中的窗口設(shè)置文字或標(biāo)題時(shí),傳入的字符串是ANSI窄字節(jié)編碼的,而SetWindowTextA函數(shù)內(nèi)部及底層的流程中會(huì)使用本地設(shè)置的ANSI字符集編碼庫將ANSI編碼的字符串轉(zhuǎn)成Unicode編碼的字符串后再設(shè)置到窗口中,最終界面上看到的文字是Unicode編碼的文字。所以在將中文字符轉(zhuǎn)換成Unicode時(shí),如果使用的是本地設(shè)置的英文字符集編碼進(jìn)行轉(zhuǎn)換,則會(huì)出現(xiàn)亂碼;如果使用中文簡體的字符集編碼進(jìn)行轉(zhuǎn)換,則能正常顯示。
所以,要讓使用中文ANSI編碼字符的程序能在英文操作系統(tǒng)中正常顯示并運(yùn)行,需要將英文操作系統(tǒng)中區(qū)域語言設(shè)置項(xiàng)中的“非Unicode程序的語言”設(shè)置成中文才行。
9、字符編碼導(dǎo)致程序啟動(dòng)失敗的案例
幾天前正好排查了一例因?yàn)樽址幋a導(dǎo)致的程序啟動(dòng)失敗的實(shí)例,在這里簡單的說一下。客戶將軟件安裝到一個(gè)包含中文字符的路徑中,點(diǎn)擊啟動(dòng)軟件沒反應(yīng),軟件始終啟動(dòng)不了,也沒有彈出什么報(bào)錯(cuò)的提示框。客戶于是向我們反饋了這個(gè)問題。
我們使用向日葵遠(yuǎn)程到客戶的機(jī)器上,經(jīng)對比發(fā)現(xiàn),如果我們將軟件安裝到默認(rèn)的C:\Program Files(X86)的英文路徑下,程序是能正常啟動(dòng)的,所以我們初步懷疑可能是字符編碼引起的問題。重新將軟件安裝到D盤包含中文字符的路徑后,我們用windbg啟動(dòng)軟件,剛啟動(dòng)windbg中就檢測到看異常,異常發(fā)生在加載主程序依賴庫的過程中。
啟動(dòng)軟件的exe主程序時(shí),會(huì)將該exe依賴的所有庫依次加載到進(jìn)程空間中,待所有的庫都加載起來后,才會(huì)將exe主程序模塊啟動(dòng)起來,才能看到軟件的主界面。
如果在加載庫時(shí)產(chǎn)生了異常,整個(gè)啟動(dòng)過程將被終止,軟件也就無法啟動(dòng)了。
異常發(fā)生在加載音視頻編解碼庫mediaproc.dll中,于是在windbg中輸入kn命令,查看異常時(shí)的函數(shù)調(diào)用堆棧(事先已經(jīng)取來了pdb符號(hào)文件)。調(diào)用堆棧顯示時(shí)崩潰在mediaproc.dll庫的DllMain函數(shù)中,加載dll庫時(shí)都會(huì)調(diào)用到該接口。
根據(jù)調(diào)用堆棧中顯示的代碼行號(hào),到編解碼庫的源代碼中查看,發(fā)現(xiàn)是崩潰在一個(gè)函數(shù)接口指針的調(diào)用上,有可能是遇到空指針了。一般情況下,使用windbg實(shí)時(shí)調(diào)試時(shí)是能看到函數(shù)中的局部變量及類對象內(nèi)存中的值,但這次有點(diǎn)特殊,看不到內(nèi)存中的值。
于是和負(fù)責(zé)維護(hù)音視頻編解碼庫的同事溝通了一下, 編解碼庫mediaproc.dll在DllMain中會(huì)使用絕對路徑(當(dāng)前exe主程序的路徑)去調(diào)用LoadLibrary去動(dòng)態(tài)加載更底層的庫,然后調(diào)用GetProcAddress把底層庫的接口都拿出來保存到指針變量中。編解碼庫mediaproc.dll是調(diào)用ANSI版本的API函數(shù)GetModuleFileNameA獲取exe主程序的路徑,問題就出在這個(gè)函數(shù)的調(diào)用上,這個(gè)函數(shù)獲取的路徑中包含亂碼。
D盤包含中文字符的文件夾在系統(tǒng)中是能正常顯示的,為啥獲取的路徑中會(huì)包含亂碼呢?于是查看了客戶Windows操作系統(tǒng)版本,是Windows10 IOT版本,經(jīng)常見到旗艦版、專業(yè)版和教育版,這個(gè)IOT版本還是第一次遇到!于是又去查看控制面板區(qū)域語言中的非Unicode語言選項(xiàng)設(shè)置:
系統(tǒng)中設(shè)置的非Unicode語言為英語(美國),這樣系統(tǒng)指向的本地ANSI字符編碼庫就是英語(美國)的ANSI字符編碼庫。
D盤中包含中文字符的文件夾在系統(tǒng)中能正常顯示的,為啥調(diào)用GetModuleFileNameA獲取到的路徑中會(huì)有亂碼呢?系統(tǒng)中顯示的中文字符是Unicode編碼的,當(dāng)我們調(diào)用ANSI版本的GetModuleFileNameA獲取路徑時(shí),GetModuleFileNameA函數(shù)內(nèi)部會(huì)將Unicode編碼的字符串轉(zhuǎn)成ANSI編碼的,轉(zhuǎn)換時(shí)使用的是系統(tǒng)指向的本地ANSI字符編碼庫,也就是英語(美國)的ANSI字符編碼庫,而英語(美國)的ANSI字符編碼庫根本不識(shí)別中文字符,所以出現(xiàn)了亂碼!
GetModuleFileNameA返回的路徑中包含亂碼,導(dǎo)致LoadLibrary失敗,導(dǎo)致GetProcAddress返回NULL值,從而導(dǎo)致call這個(gè)NULL地址產(chǎn)生了異常!
對于當(dāng)前出問題的編解碼庫,需要修改一下代碼,需要調(diào)用Unicode版本的接口。目前臨時(shí)的解決辦法有兩個(gè):
1)將軟件安裝在英文路徑中;
2)在控制面板的區(qū)域語言中將非Unicode語言改成簡體中文。
我們的軟件已經(jīng)聲稱做到了對多語種的支持,雖然UI層已經(jīng)支持Unicode了,但底層的庫因?yàn)槭遣煌_發(fā)團(tuán)隊(duì)開發(fā)維護(hù)的,需要再逐一排查一下了!
以上就是詳解C++中的ANSI與Unicode和UTF8三種字符編碼基本原理與相互轉(zhuǎn)換的詳細(xì)內(nèi)容,更多關(guān)于C++ 字符編碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Qt連接數(shù)據(jù)庫并實(shí)現(xiàn)增刪改查操作
這篇文章主要為大家詳細(xì)介紹了Qt如何連接數(shù)據(jù)庫并實(shí)現(xiàn)增刪改查等基本操作,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09LeetCode題解C++生成每種字符都是奇數(shù)個(gè)的字符串
這篇文章主要為大家介紹了LeetCode題解C++生成每種字符都是奇數(shù)個(gè)的字符串示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10C++ opencv ffmpeg圖片序列化實(shí)現(xiàn)代碼解析
這篇文章主要介紹了C++ opencv ffmpeg圖片序列化實(shí)現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08基于Matlab制作一個(gè)不良圖片檢測系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了如何基于Matlab制作一個(gè)不良圖片檢測系統(tǒng),文中的示例代碼講解詳細(xì),感興趣的可以跟隨小編一起了解一下2022-07-07