目录
1、概述
2、Visual Studio中的字符编码
3、ANSI窄字节编码
4、Unicode宽字节编码
5、UTF8编码
6、如何使用字符编码
7、三种字符编码之间的相互转换(附源码)
8、Windows系统对使用ANSI窄字节字符编码的程序的兼容
9、字符编码导致程序启动失败的案例
在C++编程中,我们有时需要去处理字符串编码的相关问题,常见的字符编码有ANSI窄字节编码、Unicode宽字节编码及UTF8可变长编码。很多人在处理字符串编码问题时都会有疑惑,即便是有多年工作经验的朋友也可能搞不清楚。所以有必要讲一下这三种字符编码以及如何去使用它们。
在日常的软件开发过程中,会时不时地去处理不同编码格式的字符串,特别是在处理文件路径的相关场景中,比如我们要通过路径去读写文件、通过路径去加载库文件等。常见的字符编码格式有ANSI窄字节编码、Unicode宽字节编码以及UTF8可变长编码。在Linux系统中,主要使用UTF8编码;在Windows系统中,既支持ANSI编码,也支持Unicode编码。
通用的大小写字母和数字则使用全球统一的固定编码,即ASCII码。
ANSI编码是各个国家不同语种下的字符编码,其字符的编码值只在该语种中有效,不是全球统一编码的,比如中文的GB2312编码就是简体中文的ANSI编码。
Unicode编码则是全球统一的双字节编码,所有语种的字符在一起统一的编码,每个字符的编码都是全球唯一的。
UTF8编码是一种可变长的宽字节编码,也是一种全球统一的字符编码。
本文将以WIndows中使用Visual Studio进行C++编程时需要处理的字符编码问题为切入点,详细讲解一下字符编码的相关内容。
在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
ANSI编码是不同语种下的字符编码,比如GB2312字符编码就是简体中文的本地编码。
ANSI编码是个本地范畴,只适用于对应的语种,每个字符的编码不是全球唯一的编码,只在对应的语种中有效。对于中文GB2312编码的字符串,如果当成英文的ANSI编码来解析,则结果会是乱码!
但是对于大小写英文字母和数字的ANSI编码,是字符ASCII码,英文字母和数字的ACSII码是全球统一的,比如大写字母A的ASCII码是65(十六进制是41H),数字0的ASCII码是48(十六进制是30H)。所以在所有语种中,大小写字母及数字的ANSI编码,都是能识别的。不同语种下的本地文字字符,一般是不能相互识别的。
使用中文ANSI编码的字符串开发的程序(代码中使用的都是中文字符串,使用的是ANSI窄字节编码),拿到俄文操作系统中可能显示的都是乱码,因为在俄文的ANSI编码中只识别俄文的ANSI编码出来的字符串,无法识别中文ANSI编码的字符串。这里主要有两类字符乱码问题,一是UI界面上显示的文字是乱码;二是使用路径去创建文件或访问文件时会因为路径中的字符是乱码,导致文件创建或访问失败。
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。
UTF8编码是可变长字符编码格式,是一种紧凑型存储的编码格式,也是一种宽字节的、全球统一的编码格式。UTF8编码中的所有字符,包括不同语种下面的字符,都是全球唯一编码的,在所有的系统都能识别出来。
UTF8编码中,能用一个字节存放的,就用一个字节存放,比如大小写字母和数字,在字符串中存放的ASCII码,只需要一个字节去存放就够了。所以在UTF8编码中,大小写字母和数字只占一个字节。我们常用的中文字符,一个字符则占用3个字节。
UTF8编码之所以称之为可变长的,是因为其根据字符需要的实际存储空间大小来编码的,比如大小写字母和数字的存储只需要1个字节就够了,所以它们只占一个字节,而一个中文字符则占三个字节。
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。
有朋友曾经提出这样的疑问,是不是我在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接口实现上述三种编码之间的转换。
字符编码相互转换的源代码请查看链接:VC++中ANSI、UNICODE与UTF-8字符编码之间的转换(附源码)https://blog.csdn.net/chenlycly/article/details/123589349
现在的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程序的语言”设置成中文才行。
几天前正好排查了一例因为字符编码导致的程序启动失败的实例,在这里简单的说一下。客户将软件安装到一个包含中文字符的路径中,点击启动软件没反应,软件始终启动不了,也没有弹出什么报错的提示框。客户于是向我们反馈了这个问题。
我们使用向日葵远程到客户的机器上,经对比发现,如果我们将软件安装到默认的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了,但底层的库因为是不同开发团队开发维护的,需要再逐一排查一下了!