本文围绕Windows来讲解字符串处理。任何一本讲编程语言的书籍都会有涉及到字符串处理,C里面用于字符串处理的一般是一些库函数strlen、strcmp、strcat等等,C++用于字符串处理的是string类。用这些函数或者类,一般就能达到我们的字符串处理需求。当开始涉及windows编程的时候,又出现了更多跟字符串相关的类型、宏和类,这不禁让很多新手一头雾水。本文将从C/C++字符处理开始,进而深入讨论各种字符编码,再到windows的字符串处理,让读者由浅入深了解字符串处理的各个细节。
C++用于保存字符的类型是char,从数据类型来看,char同short、int相比,仅仅是长度不一样,表示的数据范围不一样,没有其它什么差别。我们知道,当给char变量赋值一个字符时,实际上char变量保存的是该字符的ASCII码,既然short比char表示的数据范围大,字符能赋给char变量,当然也就能赋给short变量。因此,下面的语句都是没问题的。
char ch1 = 'a';
short ch2 = 'a';
int ch3 = 'a';
既然上面3条语句都没问题,为什么平时都是用第一种,而不是后面的两种呢?因为对于ASCII字符来说,只用一个字节的长度就能够编码,用下面的两种方式都存在存储空间的浪费。C++的各种I/O类也就只对char定义了不一样的<<操作符。下面的几条语句效果就不一样了
cout << ch1 << endl;
cout << ch2 << endl;
cout << ch3 << endl;
对于char类型,cout会将其输出为对应的ASCII字符,short和int型变量则只是输出它们内部存储的字符的ASCII码,这取决于iostream是如何对这3种类型重载<<操作符。
同理,我们在对char指针指向的字符串和short指针指向的字符串进行输出时,前者可以正确输出字符串的内容,后者只是输出指针的值。
一开始计算机只在美国使用,用一个字节存储的ASCII字符已经够用了。但是后来,世界各地都开始使用计算机了,而且他们国家使用的不是英文,他们的字符在ASCII里面是没有的,为了可以在计算机保存他们的文字,它们决定采用127号之后的空位来表示这些新的字符,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这段范围的字符称为“扩展字符集”。
等到中国人开始使用计算机时,已经没有多余的字节状态来表示汉字了。于是中国人想出了一套新的编码方案,将127号之后的符号都取消,重新规定:
一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。这种汉字方案叫做“GB2312”。unicode,ansi,utf-8,unicode big endian编码的区别
之所以要有半角和全角之分,是因为英文字符的宽度比汉字要窄很多,这样的英文字符和汉字放在一起会显得不协调。因此重新定义了字符的宽距版本,以便符合汉字的宽度。
但是中国的汉字太多了,很快就发现很多人的人名没办法打出来。于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准。
前面两种编码方案,对于原有的ASCII字符还是使用一个字节编码,对于我们新增加的字符使用两个字节。这就是变长多字节字符集。GB18060是我国目前最新的变长多字节字符集,兼容GB2312、GBK以及Unicode3.1。每个字符可以由1个、2个或4个字节组成,支持国内少数民族文字,包含繁体汉字以及日韩汉字。它包括单字节的ASCII、双字节的GBK、以及用于填补所有Unicode码位的四字节UTF区段。
不同的国家和地区制定了不同的编码标准,使用1至4个字节来表示各种字符延伸编码方式,由此产生了GB2312、GBK、Shift_JSI等编码方式,这些统称为ANSI编码。在简体中文Windows操作系统中,ANSI编码代表GB2312编码;在日文Windows操作系统中,ANSI编码代表Shift_JIS编码。不同ANSI编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
随着计算机科学和互联网的不断发展,软件国际化逐渐成为了必然的趋势。在此背景下,一种包含了世界各地绝大部分文字字符的通用字符集就应运而生了-Unicode字符集。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode的最新版本包括136755个字符。
Unicode中的每一个编码称为代码点,所有的代码点构成代码空间。Unicode的代码空间被分成17个平面,编号从0到16,每个平面包含的范围是0000-FFFF。平面0包含的字符又称为BMP(Basic Multilingual Plane)。要指定一个代码点,通常在十六进制表示的代码点前面添加”U+”。对于BMP内的代码点,使用4个数字表示(如U+0058表示“X”)。https://en.wikipedia.org/wiki/Unicode
至少需要21位才能表示所有Unicode代码点,如果只针对BMP,由于其高5位(指明位于哪一个平面)为0,用2个字节就可以表示。BMP中属于原ASCII字符的部分,高字节都是0。在网络间传输的字符大部分都是英文字符,如果直接传输Unicode字符,有一半的空间都是0,这对于寸土寸金的带宽资源是一种极大的浪费。因此出现了几种用于传输的实现方式,目前主要有UTF-8、UTF-16、UTF-32。UTF-8是使用的最为广泛的一种。Unicode的编码方式是唯一的,UTF是Unicode的不同实现方式。直接将Unicode的编码方式作为实现方式的,称为UCS,主要有UCS-2和UCS-4。Unicode实现方式中的每个编码称为代码单元。
UCS-2是以2个字节位单位的实现方式,只包含BMP部分的Unicode字符,不能包含全部Unicode字符,因此被Unicode委员会认定位应当遗弃。由于BMP字符基本能涵盖日常中用到的所有字符,所以UCS-2依然在被广泛地使用。
UTF-8是以一个字节为单位的实现方式。BMP的字符在UTF-8中可以是1、2、4个字节,BMP以外的字符是4个字节,原有的ASCII字符占用一个字节。所以用UTF-8传输文本既不会造成传输带宽的浪费,又能表示所有的Unicode字符。
UCS-2是UTF-16的子集,对于BMP部分字符,UCS-2与UTF-16是一样的,同时UTF-16还包含BMP以外的字符。UTF-16将Unicode代码空间分为3部分:https://en.wikipedia.org/wiki/UTF-16
U+0000至U+D7FF和U+E000至U+FFFF
这区间的代码点和代码单元数值是一样的
U+10000至U+10FFFF
来自于BMP以外(补充平面)的这部分代码点被编码为两个16位的代码单元,称为surrogate pairs。其实现方案为:
- 代码点数值减去0x010000,剩下区间为0x000000至0x0FFFFF的20位数。
- 高10位(范围为0x0000至0x03FF)加上0xD800作为第一个16位的代码单元(high surrogate),范围在0xD800至0xDBFF。
- 低10位(范围位0x0000至0x03FF)加上0xDC00最为第二个16位的代码单元(low surrogate),范围在0xDC00至0xDFFF。
U+D800至U+DFFF
Unicode标准暂时将这区间的代码点保留用于区分UTF-16的high surrogate和low surrogate。
不像其它UTF使用变长编码方式,UTF-32的每个代码单元都是4个字节,代码单元和Unicode的代码点的数值是一样的。由于UTF-32有更多的空间浪费,因此使用的比较少。
UCS-4和UTF-32是一样的。
Unicode作为一种能够覆盖所有国家文字的编码方式,被大力推从,我们平时在写程序的时候也应该养成使用Unicode的习惯,如果所有人都使用Unicode,那么在世界范围内传输字符串就不会有不兼容的情况。
由于我们平时使用的字符几乎都是BMP部分的字符,而这部分字符用两个字节就可以表示,在C++中的wchar_t类型就是用来存储Unicode字符的,也叫宽字符。为了支持Unicode,C++中跟字符相关的函数都有两个版本,分别针对ANSI字符和Unicode字符。如string和wstring,fstream和wfstream。C库函数也有两个版本,如strlen和wcslen。由于ANSI字符的编码长度可变,因此也叫多字节(MBCS)字符,对于最长编码长度只有2个字节的字符集也可以叫做双字节(DBCS)字符。
在给wchar_t变量赋值字符时,需要在字符前面加上L,以表示该字符要以Unicode编码,否则会是默认的ANSI编码,编译器会报错不能把char型字符赋给wchar_t变量。
wchar_t str[15] = L"This is test.";
在Windows环境下,输入的字符都是用的本地编码方式,我们平常用的简体中文系统就是GB2312,当我们在VS中敲入中文字符串的时候,内存中存放的就是中文的GB2312编码。
char str[15] = "中国Chinese";
上面的语句执行后,str中的内容是“D6 D0 B9 FA 43 68 69 6E 65 73 65”。其中D6D0是“中”的GB2312编码,B9FA是“国”的GB2312编码。剩余的英文字符,GB2312的编码和ASCII码是一样的。
虽然使用char数组可以保存ANSI字符的编码,但是当我们想知道数组里面保存有多少字符时却有问题了。用strlen来求str的长度时,得到的结果是11,而我们的str的字符是9个。由于一个汉字包含两个字节,strlen将其当做2个字符。
一个ANSI字符的第一个字节称为“lead byte”,最后一个字节称为“trail byte”。在Windows下,我们可以借助函数IsDBCSLeadByte来判断一个字节是不是lead byte。如果是lead byte,那么当前字节和下一个字节构成一个字符,否则当前字节就是一个字符。
char str[15] = "中国Chinese";
int len = 0;
for (int i = 0; i < strlen(str);)
{
if (!IsDBCSLeadByte(str[i]))
{
len++;
i += 1;
}
else
{
i += 2;
}
}
上面的程序执行后可以计算出该段字符的长度是7。
wchar_t str[15] = L"中国Chinese";
上面的语句执行后,str中的内容时“2D4E FD56 4300 6800 6900 6E00 6500 7300 6500”。因为Unicode的每个字符都是2个字节,所以str中每个字符都对应2个字节的编码。
当然,使用Unicode也有缺点,其中最重要的是程序中的每一个英文字符会占用两倍的空间。上面的UTF-8对于英文字符是一个字节,但是它是用于传输的,在计算机内部表示都是Unicode。有时我们可能希望创建两个版本的程序,一个使用ASCII字符串而另一个使用Unicode字符串。最好的办法是维护一个单一的源码文件,但可以编译成ASCI或Unicode。
但这是有问题的,因为运行库函数具有不同名称,字符变量的定义也不同,还有Unicode字符串前面还必须加L。一个答案是使用Microsoft Visual C++中的TCHAR.H头文件,其中的每一个函数和宏都有一个下划线前缀。TCHAR.H为那些需要字符串参数的普通库函数提供了一系列的替代名称,它们可以指Unicode或非Unicode版本的函数。
如果一个名为_UNICODE的标识符被定义了,_tcslen就被定义为wcslen,如果_UNICODE没有别定义,那么_tcslen就被定义为strlen。
#define _tcslen strlen
以此类推,所有的函数都有这样的一个通用版本。对于两个字符类型,如果_UNICODE被定义了,TCHAR就是wchar_t,否则的话TCHAR就是char。
对于字符串文字中L这一问题,如果_UNICODE被定义了,一个叫_T的宏是如下定义的:
#define _T(x) L##x
在ANSI C标准的C预处理器中,那一对“#”被称为令牌粘贴,使得字母L和宏参数拼接在一起,因此,如果宏参数是“Hello”,那么L##x就是L”Hello”。如果_UNICODE没有被定义,_T宏就简单地如下定义:
#define _T(x) x
_TEXT(x)也是同样的定义的。通过TCHAR.H中提供的这些宏,就可以写出同时兼容ANSI和Unicode的代码了,只需要定义或者不定义_UNICODE就可以完成两个版本的转换,而不用分别写两份代码。
Windows中所有的函数都有ANSI和宽字符两种版本。接触过Windows编程的读者会发现涉及到字符串参数的函数,其参数都是LPCTSTR。其中的P表示是一个指针类型,C表示指向常量,STR表示指向的是字符串,T表示是一个通用类型,可以表示ANSI或Unicode版本,因此这就是一个指向常量字符串的指针。如果定义了UNICODE(注意不是_UNICODE),它就是LPCSTR,而LPCTSTR是:
typedef const char *LPCSTR;
如果没有定义UNICODE,它就是LPCWSTR,而LPCWSTR是:
typedef const wchar_t *LPCWSTR;
Windows中还定义了很多这种类似的字符串指针,下面是截的一张这些定义的图,总之它们就是指向字符串的指针。不要被这一大堆的字符串类型给吓到。因此在需要LPCSTR参数的地方,我们传一个TCHAR指针就行。
Windows也定义了一组字符串函数,这些函数与C运行库中对应的函数功能是相同的,并且都是通用版本的。
ILength = lstrlen(pString);
pString = lstrcpy(pString1, pString2);
pString = lstrcpyn(pString1, pString2, iCount);
pString = lstrcat(pString1, pString2);
iComp = lstrcmpi(pString1, pString2);
在Visual Studio的Property->Configuration Properties->Character Set中可以设置使用ANSI还是Unicode版本的字符。Multi-Byte类型就是ANSI版本。这个设置项的本质就是是否定义UNICODE,因此使用哪个版本的字符可以在这里设置。
CString类是MFC中的字符串类,比直接使用字符串指针要方便很多。这个类也是一个通用版本,在不同情况下分别是CStringA和CStringW的宏定义。在需要用到LPCTSTR参数的地方,我们都可以传递CString的对象,这是为什么呢?
CString的基类CSimpleStringT定义了一个类型转换运算符CSimpleStringT::operator PCXSTR,在需要PCXSTR的地方该类都会返回一个指向内部字符串缓存的指针,而PCXSTR就是LPCTSTR的typedef。在返回LPCTSTR地方,我们也可以用CString对象类接收,因为CString有针对LPCTSTR的构造函数。
根据上面的知识,我们知道ANSI字符与Unicode字符是不兼容的,不能将ANSI字符赋给wchar_t变量,反之亦然。但有时我们得到的字符是其中一个版本,而我们对字符处理的函数又是另一个版本(比如从一个ANSI编码的文件读取的字符串,而我们的程序又是针对Unicode编写的),那我们就没法处理了吗?Windows提供了两个函数分别执行两个方向的转换:WideCharToMultiByte
int MultiByteToWideChar(UINT CodePage, DWORD dwFlags,
LPCSTR lpMultiByteStr, int cbMultiByte, LPWSTR lpWideCharStr, int cchWideChar);
int WideCharToMultiByte(UINT CodePage, DWORD dwFlags,
LPWSTR lpWideCharStr, int cchWideChar, LPCSTR lpMultiByteStr, int cbMultiByte);
这两个函数中比较关键的是第一个参数,代码页用来指定转换的时候使用哪种编码方式,Windows支持的编码方式都在这里。
char str[15] = "中国Chinese";
int len = MultiByteToWideChar(936, 0, str, strlen(str), NULL, 0);
wchar_t *wstr = new wchar_t[len];
wstr[len] = L'\0';
MultiByteToWideChar(936, 0, str, strlen(str), wstr, len);
上面的代码可以将ANSI版本的“中国Chinese”转换为Unicode版本,其中第一个调用MultiByteToWideChar的时候,将最后一个参数设置为0,该函数会返回为了保存转换后的字符所需要的空间,第二次调用将转换后的字符保存在了相应的缓存中。936是GB2312的代码页编号。
可能有的读者会说,上面这两个函数的使用方式也太麻烦了吧?就不能简单一点吗?确实,微软在ATL中提供了几个更方便的宏和类来进行转换,它们的命名规则是:C SourceType 2 DestinationType
SourceType/DestinationType | Description |
---|---|
A | ANSI字符 |
W | Unicode字符 |
T | 通用字符(根据_UNICODE是否定义而不同) |
根据这个命名规则,CA2W的作用是将ANSI字符转换位Unicode字符。因此下面的代码完成的功能跟上面的一样。
char str[15] = "中国Chinese";
CStringW wstr = CA2W(str, 936);
第一次写博客,有哪里写得有误的地方,欢迎各位大佬不吝赐教!