您的当前位置:首页详解C++中的ANSI与Unicode和UTF8三种字符编码基本原理与相互转换

详解C++中的ANSI与Unicode和UTF8三种字符编码基本原理与相互转换

2021-06-10 来源:乌哈旅游
详解C++中的ANSI与Unicode和UTF8三种字符编码基本原

理与相互转换

⽬录

1、概述2、Visual Studio中的字符编码3、ANSI窄字节编码4、Unicode宽字节编码5、UTF8编码6、如何使⽤字符编码7、三种字符编码之间的相互转换(附源码)7.1、ANSI编码与Unicode编码之间的转换7.2、UTF8编码与Unicode编码之间的转换7.3、ANSI编码与UTF8编码之间的转换8、Windows系统对使⽤ANSI窄字节字符编码的程序的兼容9、字符编码导致程序启动失败的案例1、概述

在⽇常的软件开发过程中,会时不时地去处理不同编码格式的字符串,特别是在处理⽂件路径的相关场景中,⽐如我们要通过路径去读写⽂件、通过路径去加载库⽂件等。常见的字符编码格式有ANSI窄字节编码、Unicode宽字节编码以及UTF8可变长编码。在Linux系统中,主要使⽤UTF8编码;在Windows系统中,既⽀持ANSI编码,也⽀持Unicode编码。

通⽤的⼤⼩写字母和数字则使⽤全球统⼀的固定编码,即ASCII码。

ANSI编码是各个国家不同语种下的字符编码,其字符的编码值只在该语种中有效,不是全球统⼀编码的,⽐如中⽂的GB2312编码就是简体中⽂的ANSI编码。

Unicode编码则是全球统⼀的双字节编码,所有语种的字符在⼀起统⼀的编码,每个字符的编码都是全球唯⼀的。UTF8编码是⼀种可变长的宽字节编码,也是⼀种全球统⼀的字符编码。

本⽂将以WIndows中使⽤Visual Studio进⾏C++编程时需要处理的字符编码问题为切⼊点,详细讲解⼀下字符编码的相关内容。

2、Visual Studio中的字符编码

在Visual Studio中编写C++代码时,该如何指定字符串的编码呢?其实很简单,使⽤双引号括住的字符串,使⽤的就是ANSI窄字节编码;使⽤L+双引号括住的字符串,使⽤的就是Unicode宽字节编码,如下所⽰:

char* pStr = \"This is a Test.\"; // ANSI编码

WCHAR* pWStr = L\"This is a Test.\"; // Unicode宽字节编码

我们也可以使⽤_T宏定义来指定字符串的编码格式:

TCHAR* pStr = _T(\"This is a Test.\");

设置_T后,则由⼯程配置属性中的字符集设置来确定到底是使⽤哪种编码:

如果选择多字节字符集,_T就被解释为双引号,即使⽤ANSI窄字节编码;如果选择Unicode字符集,_T就被解释为L,即使⽤Unicode宽字节编码。

其实,如果在⼯程配置中选择使⽤Unicode字符集,⼯程中会添加⼀个_UNICODE宏,如下所⽰:

如果选择多字节字符集,则没有_UNICODE宏。代码中正是通过这个宏来判定到底使⽤哪种编码的,⽐如对_T的判断:

#ifdef _UNICODE#define _T(X) L(X)#else

#define _T(X) (X)#endif // _UNICODE

和字符编码相对应的,Windows系统提供两个版本的API,⽐如给窗⼝设置⽂字的API函数,⼀个是⽀持ANSI窄字节编码的SetWindowTextA(ANSI窄字节版本),⼀个是⽀持Unicode宽字节编码的SetWindowTextW(Wide宽字节版本)。我们也

可以直接调⽤SetWindowText,然后由_UNICODE宏判断到底使⽤哪个版本,如下:

#ifdef _UNICODE

#define SetWindowText SetWindowTextW#else

#define SetWindowText SetWindowTextA#endif // !UNICODE

3、ANSI窄字节编码

ANSI编码是不同语种下的字符编码,⽐如GB2312字符编码就是简体中⽂的本地编码。

ANSI编码是个本地范畴,只适⽤于对应的语种,每个字符的编码不是全球唯⼀的编码,只在对应的语种中有效。对于中⽂GB2312编码的字符串,如果当成英⽂的ANSI编码来解析,则结果会是乱码!

但是对于⼤⼩写英⽂字母和数字的ANSI编码,是字符ASCII码,英⽂字母和数字的ACSII码是全球统⼀的,⽐如⼤写字母A的ASCII码是65(⼗六进制是41H),数字0的ASCII码是48(⼗六进制是30H)。所以在所有语种中,⼤⼩写字母及数字的ANSI编码,都是能识别的。不同语种下的本地⽂字字符,⼀般是不能相互识别的。

使⽤中⽂ANSI编码的字符串开发的程序(代码中使⽤的都是中⽂字符串,使⽤的是ANSI窄字节编码),拿到俄⽂操作系统中可能显⽰的都是乱码,因为在俄⽂的ANSI编码中只识别俄⽂的ANSI编码出来的字符串,⽆法识别中⽂ANSI编码的字符串。这⾥主要有两类字符乱码问题,⼀是UI界⾯上显⽰的⽂字是乱码;⼆是使⽤路径去创建⽂件或访问⽂件时会因为路径中的字符是乱码,导致⽂件创建或访问失败。

4、Unicode宽字节编码

Unicode编码是全球统⼀的字符编码,每个语种下的每个字符的编码值都是全球唯⼀的,即在Unicode编码集中可以识别每个语种下的所有字符。所以为了实现软件对多语种(多国语⾔)的⽀持,我们在开发软件时要选择Unicode字符编码,使⽤Unicode编码的字符串,调⽤Unicode版本的API。

系统在提供包含字符串参数的API时,都会提供两个版本,⼀个是ANSI版本的,⼀个是Unicode版本的,主要体现在对字符串编码的处理上,⽐如SetWindowTextA(ANSI版本)和SetWindowTextW(Wide宽字节Unicode版本)。我们可以直接调⽤W版本API,但⼀般我们调⽤API时,我们不指定调⽤哪个版本,是通过设置⼯程属性中的编码格式来确定使⽤哪个版本:

#ifdef _UNICODE

#define SetWindowText SetWindowTextW#else

#define SetWindowText SetWindowTextA#endif // !UNICODE

具体情况已在上⾯的“Visual Studio中的字符编码”章节中详细讲述,此处不再赘述。

在Unicode编码中,每个字符都占两个字节。对于⼤⼩写字母和数字,当他们出现在字符串中时,对应的内存中存放的是它们的ASCII码值,只占⼀个字节,在Unicode 2字节编码中,⾼位将填充0。

5、UTF8编码

UTF8编码是可变长字符编码格式,是⼀种紧凑型存储的编码格式,也是⼀种宽字节的、全球统⼀的编码格式。UTF8编码中的所有字符,包括不同语种下⾯的字符,都是全球唯⼀编码的,在所有的系统都能识别出来。

UTF8编码中,能⽤⼀个字节存放的,就⽤⼀个字节存放,⽐如⼤⼩写字母和数字,在字符串中存放的ASCII码,只需要⼀个字节去存放就够了。所以在UTF8编码中,⼤⼩写字母和数字只占⼀个字节。我们常⽤的中⽂字符,⼀个字符则占⽤3个字节。

UTF8编码之所以称之为可变长的,是因为其根据字符需要的实际存储空间⼤⼩来编码的,⽐如⼤⼩写字母和数字的存储只需要1个字节就够了,所以它们只占⼀个字节,⽽⼀个中⽂字符则占三个字节。

6、如何使⽤字符编码

Windows系统主要使⽤Unicode编码,Linux则使⽤UTF8编码,后台服务器⼀般使⽤的都是Linux系统,⽽客户端是运⾏在Windows操作系统上的。⼀般客户端与服务器交互的数据的字符串编码统⼀使⽤全球统⼀编码的UTF8编码。

客户端收到UTF8编码的字符串后,需要将UTF8字符换转换后显⽰在界⾯上。如果客户端使⽤的是Unicode编码字符集,将UTF8编码的字符串转换成Unicode编码的字符串后再显⽰到界⾯上;如果客户端使⽤的是多字节ANSI编码,则需要再将Unicode编码的字符串转成ANSI编码的字符串。

这⾥注意⼀下,UTF8编码的字符串要转成ANSI编码的,不能直接将UTF8转成ANSI,需要先将UTF8转成Unicode,然后再将Unicode转成ANSI。

为了实现软件对多语种(多国语⾔)的⽀持,我们在开发Windows软件时要选择Unicode字符编码,使⽤Unicode编码的字符串,调⽤Unicode版本的API。

此外,对于⼀些开源的项⽬,提供的API接⼝中有字符串参数的,⼀般都明确指定字符串编码为UTF8。因为⼀般情况下开源库都⽀持跨平台,既⽀持Windows平台,也⽀持Linux平台,所以要选择使⽤通⽤的、⼤家都是识别的UTF8编码。⽐如在轻便型数据库sqlite开源库中,⽤于打开数据库⽂件的接⼝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.**

** Note to Windows users: 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().**

** Note to Windows Runtime users: 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编码的字符串,在调⽤sqlite3_open接⼝之前,需要将Unicode编码的字符串转成UTF8编码的。如果收到开源库中回调上来的UTF8编码的字符串数据,则需要将UTF8编码的字符串转成Unicode后,才能显⽰到UI界⾯上,才能使⽤转码后的Unicode字符串去调⽤Windows系统API。

7、三种字符编码之间的相互转换(附源码)

有朋友曾经提出这样的疑问,是不是我在Windows下把⼀个双引号括起来的ANSI窄字节字符串赋值给WCHAR宽字节的指针:

WCHAR* pStr = \"测试字符串\";

字符串就能⾃动转换成Unicode宽字节?答案是否定的,这样的赋值操作并不会做字符编码转换,右侧的仅仅是字符串的⾸地址,作为地址,可以赋值给很多数据类型,⽐如int、void*、char*等等。

那可能有⼈会说,那为啥我在Unicode下,将⼀个ANSI编码的字符串传给MFC库中的CString类对象时会⾃动转换成Unicode宽字符呢?这和上⾯的情况不⼀样的,是因为CString类重载了赋值操作符函数,在函数内部做了字符编码的转换,代码如下:

const CUIString& CUIString::operator=(LPCSTR lpsz){

int nSrcLen = lpsz != NULL ? lstrlenA(lpsz) : 0; AllocBeforeWrite(nSrcLen);

_ANSIToUnicode(m_pchData, lpsz, nSrcLen+1); ReleaseBuffer();

return *this;}

⼀般情况下,是需要我们⾃⼰去编写字符编码转换的代码的。下⾯来看⼀下,我们在进⾏Windows C++编程时,需要调⽤哪些API接⼝实现上述三种编码之间的转换。

7.1、ANSI编码与Unicode编码之间的转换

ANSI转成Unicode的代码如下:

/*=============================================================================函 数 名: AnsiToUnicode

功 能: 实现将char型buffer(ANSI编码)中的内容安全地拷贝到指定的WChar型(Unicode编码)的buffer中参 数: char* pchSrc [in] 源字符串 WCAHR* pchDest [out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/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转成ANSI的代码如下:

/*=============================================================================函 数 名: UnicodeToAnsi

功 能: 实现将WCHAR型buffer(Unicode编码)中的内容安全地拷贝到指定的char型(ANSI编码)的buffer中参 数: WCHAR* pchSrc [in] 源字符串 char* pchDest[out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/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编码之间的转换

UTF8转成Unicode的代码如下:

/*=============================================================================函 数 名: Utf8ToUnicode

功 能: 实现将char型的buffer(utf8编码)中的内容安全地拷贝到指定的WCHAR型buffer(Unicode编码)中参 数: char* pchSrc [in] 源字符串 WCHAR* pchDest [out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/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转成UTF8的代码如下:

/*=============================================================================函 数 名: UnicodeToUtf8

功 能: 实现将WCHAR型buffer(Unicode编码)中的内容安全地拷贝到指定的char型的buffer(utf8编码)中参 数: WCAHR* pchSrc [in] 源字符串 char* pchDest [out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/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编码之间的转换

ANSI与UTF8之间是不能直接转换的,需要先转成Unicode之后才能转到⽬标编码。ANSI转成UTF8的代码如下:

/*=============================================================================函 数 名: AnsiToUtf8

功 能: 实现将char型buffer(ANSI编码)中的内容安全地拷贝到指定的char型的buffer(utf8编码)中参 数: char* pchSrc [in] 源字符串 char* pchDest [out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/void AnsiToUtf8( const char* pchSrc, char* pchDest, int nDestLen ) {

if (pchSrc == NULL || pchDest == NULL) {

return; }

// 先将ANSI转成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转成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转成ANSI的代码如下:

/*=============================================================================

函 数 名: Utf8ToAnsi

功 能: 实现将char型buffer(utf8编码)中的内容安全地拷贝到指定的char型的buffer(ANSI编码)中参 数: char* pchSrc [in] 源字符串 char* pchDest [out] ⽬标buf

int nDestLen [in] ⽬标buf长度(注意:以字节为单位,不是以字符个数为单位)注 意: ⽆返 回 值: ⽆

=============================================================================*/void Utf8ToAnsi(const char* pchSrc, char* pchDest, int nDestLen){

if (pchSrc == NULL || pchDest == NULL) {

return; }

// 先将utf8转成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转成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系统对使⽤ANSI窄字节字符编码的程序的兼容

现在的Windows程序基本都⽤Unicode字符编码了,⼯程属性中将字符集都设置成了Unicode字符集,代码中都使⽤Unicode编码的字符串。但是还有⼀些⽼的程序使⽤的还是ANSI窄字节的字符。那这些⽼的程序如何才能在外⽂的操作系统中正常运⾏呢?微软提供了⼀种兼容这些⽼程序的办法。

可以到Windows控制⾯板的区域语⾔设置中将⾮Unicode语⾔设置成程序中使⽤的字符语种即可,相关设置的操作步骤截图如下:

在上图中选择程序中字符使⽤的语种即可。

下⾯我们来看看使⽤ANSI编码的程序放到外⽂操作系统中运⾏为什么会出现乱码。假设将某程序中使⽤的是中⽂ANSI窄字节编码的字符串,放到英⽂操作系统中运⾏,默认情况下,UI界⾯上会显⽰乱码。⾄于为什么会显⽰乱码,是因为英⽂操作系统中默认情况下设置的⾮Unicode语⾔是英语(美国):

这个⾮Unicode语⾔设置直接影响我们调⽤MultiByteToWideChar和WideCharToMultiByte接⼝中的CP_ACP标记对应的本地ANSI字符集编码库。在上⾯界⾯中如果将⾮Unicode语⾔设置成英语(美国),则使⽤英⽂的ANSI字符编码库;如果设置成中⽂简体,则使⽤中⽂简体的ANSI字符集编码库。

程序中调⽤API函数SetWindowTextA给程序中的窗⼝设置⽂字或标题时,传⼊的字符串是ANSI窄字节编码的,⽽

SetWindowTextA函数内部及底层的流程中会使⽤本地设置的ANSI字符集编码库将ANSI编码的字符串转成Unicode编码的字符串后再设置到窗⼝中,最终界⾯上看到的⽂字是Unicode编码的⽂字。所以在将中⽂字符转换成Unicode时,如果使⽤的是本地设置的英⽂字符集编码进⾏转换,则会出现乱码;如果使⽤中⽂简体的字符集编码进⾏转换,则能正常显⽰。所以,要让使⽤中⽂ANSI编码字符的程序能在英⽂操作系统中正常显⽰并运⾏,需要将英⽂操作系统中区域语⾔设置项中的“⾮Unicode程序的语⾔”设置成中⽂才⾏。

9、字符编码导致程序启动失败的案例

⼏天前正好排查了⼀例因为字符编码导致的程序启动失败的实例,在这⾥简单的说⼀下。客户将软件安装到⼀个包含中⽂字符的路径中,点击启动软件没反应,软件始终启动不了,也没有弹出什么报错的提⽰框。客户于是向我们反馈了这个问题。我们使⽤向⽇葵远程到客户的机器上,经对⽐发现,如果我们将软件安装到默认的C:\\Program Files(X86)的英⽂路径下,程序是能正常启动的,所以我们初步怀疑可能是字符编码引起的问题。重新将软件安装到D盘包含中⽂字符的路径后,我们⽤windbg启动软件,刚启动windbg中就检测到看异常,异常发⽣在加载主程序依赖库的过程中。

启动软件的exe主程序时,会将该exe依赖的所有库依次加载到进程空间中,待所有的库都加载起来后,才会将exe主程序模块启动起来,才能看到软件的主界⾯。

如果在加载库时产⽣了异常,整个启动过程将被终⽌,软件也就⽆法启动了。

异常发⽣在加载⾳视频编解码库mediaproc.dll中,于是在windbg中输⼊kn命令,查看异常时的函数调⽤堆栈(事先已经取来了pdb符号⽂件)。调⽤堆栈显⽰时崩溃在mediaproc.dll库的DllMain函数中,加载dll库时都会调⽤到该接⼝。

根据调⽤堆栈中显⽰的代码⾏号,到编解码库的源代码中查看,发现是崩溃在⼀个函数接⼝指针的调⽤上,有可能是遇到空指针了。⼀般情况下,使⽤windbg实时调试时是能看到函数中的局部变量及类对象内存中的值,但这次有点特殊,看不到内存中的值。

于是和负责维护⾳视频编解码库的同事沟通了⼀下, 编解码库mediaproc.dll在DllMain中会使⽤绝对路径(当前exe主程序的路径)去调⽤LoadLibrary去动态加载更底层的库,然后调⽤GetProcAddress把底层库的接⼝都拿出来保存到指针变量中。编解码库mediaproc.dll是调⽤ANSI版本的API函数GetModuleFileNameA获取exe主程序的路径,问题就出在这个函数的调⽤上,这个函数获取的路径中包含乱码。

D盘包含中⽂字符的⽂件夹在系统中是能正常显⽰的,为啥获取的路径中会包含乱码呢?于是查看了客户Windows操作系统版本,是Windows10 IOT版本,经常见到旗舰版、专业版和教育版,这个IOT版本还是第⼀次遇到!于是⼜去查看控制⾯板区域语⾔中的⾮Unicode语⾔选项设置:

系统中设置的⾮Unicode语⾔为英语(美国),这样系统指向的本地ANSI字符编码库就是英语(美国)的ANSI字符编码库。D盘中包含中⽂字符的⽂件夹在系统中能正常显⽰的,为啥调⽤GetModuleFileNameA获取到的路径中会有乱码呢?系统中显⽰的中⽂字符是Unicode编码的,当我们调⽤ANSI版本的GetModuleFileNameA获取路径时,GetModuleFileNameA函数内部会将Unicode编码的字符串转成ANSI编码的,转换时使⽤的是系统指向的本地ANSI字符编码库,也就是英语(美国)的ANSI字符编码库,⽽英语(美国)的ANSI字符编码库根本不识别中⽂字符,所以出现了乱码!

GetModuleFileNameA返回的路径中包含乱码,导致LoadLibrary失败,导致GetProcAddress返回NULL值,从⽽导致call这个NULL地址产⽣了异常!

对于当前出问题的编解码库,需要修改⼀下代码,需要调⽤Unicode版本的接⼝。⽬前临时的解决办法有两个:1)将软件安装在英⽂路径中;

2)在控制⾯板的区域语⾔中将⾮Unicode语⾔改成简体中⽂。

我们的软件已经声称做到了对多语种的⽀持,虽然UI层已经⽀持Unicode了,但底层的库因为是不同开发团队开发维护的,需要再逐⼀排查⼀下了!

以上就是详解C++中的ANSI与Unicode和UTF8三种字符编码基本原理与相互转换的详细内容,更多关于C++ 字符编码的资料请关注其它相关⽂章!

因篇幅问题不能全部显示,请点此查看更多更全内容