详解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编码。
中文字符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.
**
** <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编码的字符串,在调⽤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;
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。