字符集基础知识
在计算机中,字符都是以二进制编码方式存在于存储中
编码与解码
将字符输入计算机存储的过程类似于一个”编码”的过程
而将对应的”编码”显示出来的过程类似于一个解码的过程
二进制值本身代表什么含义是可以随意定义的,在内存中用某个2进制的值代表某一个值,
比如说用8位2进制的代表单字节这些,16位,32位等等
用某个二进制值表示某个字符完全是人为设定的,最终字符集就是字符到其二进制数字值得映射
单字节字符集(SBCS) Single Byte Character Set
单字节字符集使用一个字节(8Bits)编码字符
最大可编码256个字符
有名的单字节字符集就是ASCII码
字符集的变体称为代码页,主要包括不同的特殊字符,一般为一种语言或语言组定制
用在美国的OEM代码页标识符是437
调用GetACP函数可以检索系统中ANSI代码页标识符
使用GetOEMCP可以得到OEM代码页标识符
OemToChar和OemToCharBuff函数可以把字符或串从OEM页转到ANSI代码页
反之用CharToOem或CharToOemBuff
GetCpInfo函数可以得到代码页中的详细信息:最大字符大小、缺省字符、前置符(详见CPINFO结构体)
使用MultiByteToWideChar可以把单字节字符或串转换到UNICODE反之用WideCharToMultiByte
双字节字符集(DBCS) Double Byte Character Set
双字节字符集被称作扩展的8Bits字符集,最小单位仍是一个字节
Windows亚洲语言版中一般都支持ANSI字符集和DBCS
在这些平台的程序中需要特别的算法处理DBCS
使用IsDBCSLeadByte可以判定所给字符是否是双字节字符的第一个字节
使用MultiByteToWideChar可以将双字节字符或串转换为UNICODE
反之使用WideCharToMultiByte
有些字符集编码甚至使用4Bytes表示一个字符,此时统称为多字节字符集MBCS(MultiBytes Char Set)
常用字符集和字符编码
ASCII字符集
GB2312字符集(国标码)
GB18030字符集
Unicode字符集
ASCII字符集
ASCII(AmericanStandard Code for Information Interchange,美国信息交换标准代码)
是基于拉丁字母的一套字符编码
主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统
等同于国际标准ISO/IEC 646
将ASCII字符集转换为计算机可以接受的数字系统的数的规则。使用7位(bits)表示一个字符,共128字符;
为了表示更多的欧洲常用字符对ASCII进行了扩展,ASCII扩展字符集使用8位(bits)表示一个字符,共256字符。
ASCII的最大缺点是只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。
而EASCII虽然解决了部份西欧语言的显示问题,但对更多其他语言依然无能为力。
GBXXXX字符集 (国标字符集)
因ASCII码的限制,为了显示中文,必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。
为此规定那些127号之后的奇异符号(即EASCII)取消掉,同时规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样可以组合出大约7000多个简体汉字
上述规则即为GB2312中国国家标准简体中文字符集GBO
对于人名、古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现
GB18030字符集
国家标准GB 18030-2005《信息技术 中文编码字符集》
与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个
特点:
n 与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。
n 编码空间庞大,最多可定义161万个字符。
n 支持中国国内少数民族的文字,不需要动用造字区。
n 汉字收录范围包含繁体汉字以及日韩汉字
Unicode字符集
当计算机传到世界各个国家时,为了适合当地语言设计和实现很多不同的编码方案。
在本地使用没有问题,一旦出现在网络中,由于不兼容,互相访问就出现了乱码现象。
为了解决这个问题,一个伟大的创想产生了——Unicode
UNICODE标准前身是通用字符集(Universal Character Set)UCS
现在标准由UNICODE联盟维护,官方网址:www.unicode.org 目前最新的标准版本是7.0 (2015.01.16)
最初的UNICODE使用两个字节编码字符集, 现在已被新的编码方式取代
Windows支持UNICODE字符集及全部编码方式
UNICODE详解
Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。
Unicode用数字0-0x10FFFF来映射这些字符,最多可以容纳1114112个字符,或者说有1114112个码位。
码位就是可以分配给字符的数字。UTF-8、UTF-16、UTF-32都是将数字转换到程序数据的编码方案。
通用字符集(Universal Character Set,UCS)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。UCS-2用两个字节编码,UCS-4用4个字节编码。
现在UCS和UNICODE基本合二为一
与其它的直接指定式编码字符集不同,UNICODE是字符集,UTF-x是编码方式
一般情况下:一个字符对应到一些位(bits). 如: A -> 0100 0001
而在 Unicode 中, 一个字符实际上对应一种叫做 code point 的东西。
每一个字母表中的抽象的字母,都被赋予了一个数字,比如 U+0645. 这个叫做 code point.
U+ 表示: Unicode, 数字是 16 进制的。
Unicode 中 code point 的数字的大小是没有限制的,而且也早就超过了 65535. 所以不是每个字符都能存储在两个字节中。
运行CharMap程序可以查看U+形式的字符详细信息 CMD
使用UNICODE的好处
Windows从NT内核版本开始所有平台均完全基于UNICODE打造
使用UNICODE版API节约了字符转换的额外开销,从根本上保证了程序能够最高效的利用API的性能
UNICODE函数不再考虑因字符不同而带来的字符串长不确定的问题(这曾是程序员的噩梦)
UNICODE编码的标准唯一性使得UNICODE具有非常好的可移植性
允许同时显示不同国家的不同语言的字符在一个程序中
Windows API 与 UNICODE
Windows平台提供3种关于字符操作相关API的封装形式
n 一般宏定义形式API(兼容UNICODE和非UNICODE)
n 以A结尾的非UNICODE版API
n 以W结尾的UNICODE版API
Windows内部其实仅提供了UNICODE版的API,非UNICODE版的API在内部先将参数转换成UNICODE串,然后再调用UNICODE版的API,如果返回结果仍然是UNICODE,那么就再进行反向转换
兼容版的API在编译时,如果指定了UNICODE宏,那么就替换成UNICODE版的API否则就替换成非UNICODE版的API
由此可见调用非UNICODE版的API会付出额外的代价,当字符串较长,或者调用较多时,性能损失还是比较明显的
综上本课程提倡使用兼容版API宏定义,并且使用UNICODE宏和_UNICODE宏编译VC代码
VC中默认采用的是UTF-16编码方式,因此一个UNICODE字符占用2字节
VC中非UNICODE的串统称为MBCS(多字节字符集)
Windows平台的UNICODE串函数(API)
strlen函数返回字符串中的字节数,而lstrlen返回字符数,不管字符是1字节还是2字节
与标准C库函数tolower或toupper不同,CharLower或CharUpper可以作用于字符集中的任何字符(包括非英文字符等,比如Æ和æ)
wsprintf和wvsprintf是标准C函数sprintf和vsprintf的扩展(API)
也是仅有的两个支持格式说明的UNICODE版函数
系统本地语言支持(National Language Support)
系统本地标示符,是一个语言/子语言标示符和结合到一个双字值中的保留字
通过该标示符,应用程序可以进一步得到系统安装的语言版本,系统区域设置等信息(比如中文版Windows,时区是东8区)
通过调用GetSystemDefaultLCID和GetUserDefaultLCID函数可以获得区域信息标识符
要获得本地语言标识符可以调用GetSystemDefaultLangID和GetUserDefaultLangID
得到区域信息有助于调用一些与区域信息相关的API:比如获取日期时间格式、货币格式等
这些是一些高级国际化软件必需要使用的信息
关于这组函数的调用,将在需要调用时进行介绍
如何正确使用兼容版字符集编程
- 包含头文件<tchar.h>: 对C,C++的表中字符的宏定义
- 使用宏定义类型替代一般字符或字符串类型:
TCHAR -> char or wchar_t
LPTSTR-> char* or wchar_t*
LPTCH->char* or wchar_t*
LPCTSTR->const char* or constwchar_t* or LPCSTR等
使用_T()宏包裹字符或串常量
- 使用兼容版Windows API(不带A或W后缀)
- 使用兼容版C库函数(一般带_t前缀)
- 使用兼容版C++标准类或库函数(标准类要自己宏定义)
- 在确定需要处理非UNICODE字符或串的地方调用非UNICODE函数(API或C/C++库函数)
- 在确定需要处理UNICODE字符串的地方调用UNICODE函数
- 字符串字符集需要转换时调用MultiByteToWideChar或WideCharToMultiByte
使用C标准库字符串函数时的几个问题
如果不调用setlocale方法,标准库函数对串的字符集解释可能不正确而输出乱码(TCHAR版:_tsetlocale)
对于含有非ASCII码的串,调用strlen时返回的是字符串的字节长度,而不是字符数(因为有些字符是多个字节,因此字节数不等于字符数)
使用MBCS版的库函数就可以正确侦测到含有多字节字符的串中的字符数
即使使用UNICODE和_UNICODE宏(或MBCS和_MBCS)编译的项目代码,仍然可以使用非UNICODE版或纯UNICODE版的函数,这个宏其实仅对TCHAR.h中和Windows.h中的宏定义起作用,对原函数无任何影响(对Windows API也成立)
字符集的转换
有些情况下,需要在各种不同的字符集之间转换字符串,甚至需要在不同的编码方式间进行转换
此时可以调用C库函数mbstowcs或wcstombs等进行转换
或者可以用Windows API:MultiByteToWideChar或WideCharToMultiByte进行相互转换
利用OemToChar可以将一个OEM版字符串转换成ANSI串或宽字节字符串,反之用CharToOem
传统C库函数的一些安全问题
在微软产品的安全漏洞中,有很大一部分是由于使用C标准库(C Runtime Library)的函数,特别是有关字符串处理的函数导致的;(数组下标越界)
多数是因为缓冲区溢出导致执行恶意代码,从而提升了权限,进而进行恶意攻击;
这一点并不奇怪,因为C标准库函数的设计大约是30年前的事情了;当时并没有太多安全方面的考虑;
在http://msdn.microsoft.com/en-us/library/bb288454.aspx有一个完整的不安全C库函数的列表
从VS2005起,如果C/C++代码中使用了这些函数,编译器都会提示一个C4996的不安全函数引用警告
非服务端软件可以关闭或直接忽略这个警告
安全字符串函数
作为非安全字符串的替代,可以在VS2005(及以上版本中)调用两组安全的字符串操作函数:
StringSafe Functions(微软提供的内联形式函数,可以当做API)
SafeCRT(C库函数)
其它的一些安全函数替代品
一般使用带有_s结尾的新版C库函数,替代原有的C库函数,如:用fopen_s替代fopen或wfopen_s替代wfopen等
如果是Windows平台的程序,可用类似功能的API替代C库函数,如:用CreateFile代替fopen,wfopen等;
要使用String Safe函数,只需包含strsafe.h即可
使用Safe CRT函数,需查看MSDN以确定需要包含的头文件
使用安全字符串函数的一些需考虑问题
StringSafe 函数因为使用的是内联方式,因此可以直接使用,并具备一定的平台无关性(推荐使用)
Safe CRT是正在考察的标准库函数,因此不同编译环境中细节上可能还有差异(详情见相应环境的帮助文档)
对于客户端软件或单机软件可以不考虑使用安全字符串函数,仍可使用传统的C库函数或C++字符串类
在安全性要求较高的环境中,比如服务端软件中,尽力避免使用stl中的string类或MFC/ATL中的CString类,需要使用时考虑使用安全字符串操作函数自行封装,同时避免使用非安全的库函数(Windows平台上的网络服务应用尤其要避免使用非安全的字符串函数或字符串类)
安全字符串函数因具备缓冲区安全检测机制,所以有一定性能损失,但损失不大可以忽略(详细情况可以使用VS2008自带性能分析工具进行分析比较)
ATL库中字符串转换的支持
ATL库中的字符串转换通过宏定义在头文件atlconv.h中
此头文件可以单独使用,不需要其它的ATL组件或头文件支持
使用时需要先调用一个宏USES_CONVERSION;然后就可以调用A2T,A2W,W2T,W2A等宏进行字符串转换
示例
UNICODE版API调用示例
#include <tchar.h> #include <windows.h> #include <locale.h> #include <malloc.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); //大小写转换 针对多字节字符 TCHAR chLower[] = _T("abc αβγδεζνξοπρσηθικλμτυφχψω"); TCHAR *chUpper = NULL; _tprintf(_T("Lower Char = %s\n"),chLower); //装换为大写的 chUpper = CharUpper(chLower); _tprintf(_T("Upper Char = %s\n"),chUpper); _tprintf(_T("Upper Char Point = 0x%08X,Lower Char Point = 0x%08X\n"),chUpper,chLower); //装换为小写的 CharLower(chUpper); _tprintf(_T("Convert Lower Char = %s\n"),chLower); TCHAR pString[] = _T("一二三赵钱孙李ABCabc1231234"); //字符长度 int iLen = lstrlen(pString); TCHAR* pNext = pString; TCHAR* pPrev = pString + sizeof(pString)/sizeof(pString[0]) - 1; for(int i = 0;i < iLen; i ++) { pPrev = CharPrev(pString,pPrev); _tprintf(_T("Next Char='%c'\tPrev Char='%c'"),*pNext,*pPrev); if(IsCharAlpha(*pNext)) { _tprintf(_T("\tNext Char is Alpha")); } else if(IsCharAlphaNumeric(*pNext)) { _tprintf(_T("\tNext Char is Alpha Numeric")); } else { _tprintf(_T("\tNext Char is Unknown Type"),*pNext); } if(IsCharLower(*pNext)) { _tprintf(_T("\tNext Char is Lower\n")); } else if(IsCharUpper(*pNext)) { _tprintf(_T("\tNext Char is Upper\n")); } else { _tprintf(_T("\tNext Char is no Lower or Upper\n"),*pNext); } pNext = CharNext(pNext); } _tsystem(_T("PAUSE")); return 0; }
系统本地语言支持(查看本地的语言的示例)
#include <tchar.h> #include <windows.h> #include <strsafe.h> BOOL CALLBACK EnumCodePagesProc(LPTSTR lpCodePageString) { const int iBufLen = 1024; TCHAR pBuff[iBufLen]; size_t szLen = 0; CPINFOEX ci = {}; GetCPInfoEx((UINT)_ttoi(lpCodePageString),0,&ci); StringCchPrintf(pBuff,iBufLen,_T("CP:%5s\tCP Name:%s\n\ Max Char Size:%uBytes Default Char:0x%02X%02X Lead Byte:0x%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X Unicode Default Char:%c CP:%u\n\ -------------------------------------------------------------------------------------------------------------------------------------------\n") ,lpCodePageString ,ci.CodePageName ,ci.MaxCharSize ,ci.DefaultChar[0],ci.DefaultChar[1] ,ci.LeadByte[0],ci.LeadByte[1],ci.LeadByte[2],ci.LeadByte[3],ci.LeadByte[4],ci.LeadByte[5] ,ci.LeadByte[6],ci.LeadByte[7],ci.LeadByte[8],ci.LeadByte[9],ci.LeadByte[10],ci.LeadByte[11] ,ci.UnicodeDefaultChar ,ci.CodePage); StringCchLength(pBuff,iBufLen,&szLen); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuff,szLen,NULL,NULL); return TRUE; } int _tmain() { COORD cd = {156,1024}; SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE),cd); TCHAR psInstalled[] = _T("Installed Code Pages:\n\ -------------------------------------------------------------------------------------------------------------------------------------------\n"); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),psInstalled ,sizeof(psInstalled)/sizeof(psInstalled[0]) - 1,NULL,NULL); EnumSystemCodePages(EnumCodePagesProc,CP_INSTALLED); _tsystem(_T("PAUSE")); TCHAR psAll[] = _T("All Supported Code Pages:\n\ -------------------------------------------------------------------------------------------------------------------------------------------\n"); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),psAll ,sizeof(psAll)/sizeof(psAll[0]) - 1,NULL,NULL); EnumSystemCodePages(EnumCodePagesProc,CP_SUPPORTED); _tsystem(_T("PAUSE")); return 0; }
调用C库函数的例子
#include <tchar.h> #include <windows.h> #include <stdio.h> #include <locale.h> #include <mbstring.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); TCHAR pszTest[] = _T("123Abc汉字€鴕㎜㎎㊣㎞㏄㏑㏕㏒"); _tprintf(_T("Len=%d; %s\n"),_tcslen(pszTest),pszTest); #ifdef _UNICODE wprintf(L"Len=%d; wprintf->:%s\n",wcslen(pszTest),pszTest); #else printf("Len=%d; printf->:%s\n",strlen(pszTest),pszTest); #endif char pszAString[] = "123Abc汉字€鴕㎜㎎㊣㎞㏄㏑㏕㏒"; printf("MBCS(ANSI)->:Len=%d; %s\n",strlen(pszAString),pszAString); wchar_t pszWString[] = L"123Abc汉字€鴕㎜㎎㊣㎞㏄㏑㏕㏒"; wprintf(L"UNICODE->:Len=%d; %s\n",wcslen(pszWString),pszWString); printf("MBCS Len = %d And Unicode Len = %d : %s\n" ,_mbslen((const unsigned char*)pszAString),wcslen(pszWString),pszAString); _tsystem(_T("PAUSE")); return 0; }
字符集转换示例
#include <tchar.h> #include <windows.h> #include <stdio.h> #include <locale.h> #include <mbstring.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); char pStringA[] = "123ABC汉字абвгде╔╗╚╝╠╣╦"; printf("转换前:%s\n",pStringA); wchar_t pStringW[256] = {}; MultiByteToWideChar( CP_ACP, 0, pStringA, strlen(pStringA)+1, pStringW, sizeof(pStringW)/sizeof(pStringW[0]) ); wprintf(L"转换后:%s\n",pStringW); ZeroMemory(pStringA,sizeof(pStringA)/sizeof(pStringA[0])); WideCharToMultiByte(CP_ACP,0,pStringW,wcslen(pStringW) ,pStringA,sizeof(pStringA)/sizeof(pStringA[0]) ,"",NULL); printf("转换回来后:%s\n",pStringA); printf("printf输出UNICODE:%S\n",pStringW); wprintf(L"wprintf输出MBCS:%S\n",pStringA); ZeroMemory(pStringW,sizeof(pStringW)/sizeof(pStringW[0])); mbstowcs(pStringW,pStringA,_mbslen((const unsigned char*)pStringA)); wprintf(L"mbstowcs转换后:%s\n",pStringW); ZeroMemory(pStringA,sizeof(pStringA)/sizeof(pStringA[0])); wcstombs(pStringA,pStringW,sizeof(pStringW)/sizeof(pStringW[0])); printf("wcstombs转换后:%s\n",pStringA); _tsystem(_T("PAUSE")); return 0; }
其它常用字符串转换
#include <tchar.h> #include <windows.h> #include <stdio.h> #include <locale.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); //取得UTF-8的编码信息 CPINFOEX ci = {}; GetCPInfoEx(CP_UTF8,0,&ci); _tprintf(_T("Code Page Name: UTF-8\tMax Char Size:%uBytes\tDefault Char:0x%X%X\tLead Byte:0x%X%X%X%X%X%X%X%X%X%X%X%X\n") ,ci.MaxCharSize ,ci.DefaultChar[0],ci.DefaultChar[1] ,ci.LeadByte[0],ci.LeadByte[1],ci.LeadByte[2],ci.LeadByte[3],ci.LeadByte[4],ci.LeadByte[5] ,ci.LeadByte[6],ci.LeadByte[7],ci.LeadByte[8],ci.LeadByte[9],ci.LeadByte[10],ci.LeadByte[11]); _tprintf(_T("Unicode Default Char:%c\tCode Page:%u\tCode Page Name:%s\n") ,ci.UnicodeDefaultChar ,ci.CodePage ,ci.CodePageName); //UTF-8编码的串"1234ABCxyz赵钱孙李" UCHAR pUTF8[] = "\x31\x32\x33\x34\x41\x42\x43\x78\x79\x7A\xE8\xB5\xB5\xE9\x92\xB1\xE5\xAD\x99\xE6\x9D\x8E"; //UTF-8转UNICODE INT iLen = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pUTF8, -1, NULL,0); WCHAR * pwUtf8 = (WCHAR*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,(iLen+1)*sizeof(WCHAR)); //CP_UTF8指定原串的代码页 MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pUTF8, -1, pwUtf8, iLen); wprintf(L"UTF8->Unicode = %s\n",pwUtf8); //UNICODE转ANSI,经过两次转换 UTF-8 已经变成了 GBK 编码 iLen = WideCharToMultiByte(CP_ACP, 0, pwUtf8, -1, NULL, 0, NULL, NULL); CHAR* psGBK = (CHAR*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,iLen * sizeof(CHAR)); WideCharToMultiByte (CP_ACP, 0, pwUtf8, -1, psGBK, iLen, NULL,NULL); printf("Unicode->GBK = %s\n",psGBK); HeapFree(GetProcessHeap(),0,pwUtf8); HeapFree(GetProcessHeap(),0,psGBK); _tsystem(_T("PAUSE")); return 0; }
安全字符串函数示例
#include <tchar.h> #include <windows.h> #include <strsafe.h> #include <stdio.h> #include <locale.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); const size_t szBuflen = 1024; TCHAR pAnyStr[] = _T("123ABCabc赵钱孙李"); TCHAR pBuff[szBuflen]; size_t szLen = 0; StringCchPrintf(pBuff,szBuflen,_T("WriteConsole OutPut:%s\n"),pAnyStr); StringCchLength(pBuff,szBuflen,&szLen); WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),pBuff,szLen,NULL,NULL); ZeroMemory(pBuff,szBuflen*sizeof(TCHAR)); _tcscpy_s(pBuff,szBuflen,pAnyStr); _tprintf_s(_T("_tprintf_s OutPut:%s\n"),pBuff); _tsystem(_T("PAUSE")); return 0; }
ATL库中字符串转换的支持
#include <tchar.h> #include <windows.h> #include <atlconv.h> #include <stdio.h> #include <locale.h> int _tmain() { _tsetlocale(LC_ALL,_T("chs")); USES_CONVERSION; WCHAR wStr[] = L"赵钱孙李123ABCxyz"; wprintf(L"UNICODE Str = %s\n",wStr); printf("W2A Convert = %s\n",W2A(wStr)); _tprintf(_T("W2T Convert = %s\n"),W2T(wStr)); CHAR aStr[] = "123ABCxyz赵钱孙李"; printf("MBCS Str = %s\n",aStr); wprintf(L"A2W Convert = %s\n",A2W(aStr)); _tprintf(_T("A2T Convert = %s\n"),A2T(aStr)); _tsystem(_T("PAUSE")); }