字符(串)之间的转换
字符串之间的转换分为两类:
- 底层字符集之间的转换,表现为
char
wchar_t
之间的转换,string
wstring
之间的转换; - 在此之上牵扯到 Windows 平台下的各种字符串之间的转换;去伪存真,归根到底还是第一种转换。
为了“知其然,也要知其所以然”,我们在描述相互转换之前,先介绍一些相关的类型。上层所有的字符串类型归根到底都是对底层 char
或者 wchar_t
类型的封装,如果真有刨根问底的兴趣,就需要了解 char
wchar_t
的关联和区别。我们比较熟悉 char
,接下来看一下 wchar_t
。
wchar_t
虽然我们大多是在 windows 下开发时接触过 wchar_t
,但它其实是 C 标准(但不是内置类型)。类 Unix 环境下使用 wchar_t
并不广泛。
我们看一下 维基百科中对“宽字符”的解释:
宽字符(Wide character) 是程序设计的术语。它是一个抽象的术语(没有规定具体实现细节),用以表示比8位字符还宽的数据类型。它不同于 Unicode。
尴尬的是标准并未规定具体的实现细节,而 Windows 下的 wchar_t
是破坏(不符合)了ANSI/ISO C标准的。
wchar_t
的宽度属于编译器的特性,且可以小到8位。所以程序若需要跨过所有 C 和 C++ 编译器的可携性,就不应使用wchar_t
存储 Unicode 文字。wchar_t
类型是为存储编译器定义的宽字符,在部分编译器中,其可以是 Unicode 字符。在Windows API中,
wchar_t
是16位宽。Windows API因不使wchar_t
字符类型在单一wchar_t
单元中支持所有系统可表示的字符,而破坏了 ANSI/ISO C 标准。wchar_t
在Windows下,反而表示一个 UTF-16 小尾字符(或 UTF-16 的一部分)。在类Unix系统中,
wchar_t
是32位宽。
如果对 Unicode 字符集有足够的认识,我们就能知道 16 位无法满足“表示所有 Unicode 字符”。一般来说,UTF-x,x表示这套编码一个单位至少占用x位,因为Unicode最长达到32位,所以UTF-x通常是变长的(除了UTF-32)。
需要指出的是,C++标准中对
wchar_t
的要求是要能表示所有系统能识别的字符。Windows 自称支持 Unicode,但是其wchar_t
却不能表示所有的 Unicode,由此违背了 C++ 标准。 引用自 彻底解密C++宽字符:2、Unicode和UTF
结论:宽字符的宽度依赖于平台,局限性太高。不推荐使用。Windows 虽然广泛使用宽字符,但其有一个愚蠢的(错误的)假设:所有字符只使用两位字节就能表示。微软也有替人背锅的嫌疑,也可能是历史包袱的原因。
字符集
学习过程中难免又涉及到更底层的字符集(character set)、字符编码(character encoding)知识,可以参考 字符集和字符编码(Charset & Encoding) - cnblogs 吴秦。
这里需要强调的是:
术语字符编码(character encoding),字符映射(character map),字符集(character set)或者代码页,在历史上往往是同义概念。 引用自 维基百科
这句话适用于 ANSI 体系。后来随着 Unicode 的流行,随着其 UTF-32、UTF-16 和 UTF-8 的使用,为了更清楚的理解其中的关联
可以这样理解:Unicode是字符集,UTF-32/ UTF-16/ UTF-8是三种字符编码方案。
mbs 和 wcs
mbs:multi byte string,用char作为存储类型,一个字符可能对应1个或者多个char,不能直接确定字符边界。charset不确定。过去的程序都是采用mbs的。
wcs:wide character string,用wchar_t作为存储类型,一个字符对于一个wchar_t。使用unicode编码,charset与OS相关,在windows平台中为UTF16(UCS-2),在大多数unix平台中为UTF32(UCS-4)。
mbs 的具体表现形式有 C 中 char*
/char[]
,C++ 中的 std::string
;wcs 的具体表现形式有 C 中的 wchar_t*
/wchar_t[]
,C++ 中的 std::wstring
。
要“知其所以然”,参考 彻底解密C++宽字符:1、从char到wchar_t,讲解的很透彻。
要“知其然”,C 代码怎么写,可以参考 wchar和char之间的相互转换;C++ 代码怎么写,可以参考 彻底解密C++宽字符:3、利用C运行时库函数转换
1 |
|
char 和 wchar_t 之间的转换可以使用上述两个函数,使用的关键在于 setlocale()
和第三个参数,难点也在第三个参数:
- 设置本地策略集,使得编译器知晓多字节字符集使用的具体是哪一种;
第三个参数表示目标指针的字节长度,尤其是转为宽字符时需仔细辨识,一个宽字符占用的字节和平台有严格的关联。- mbstowcs() 第三个参数 This is the maximum number of wchar_t characters to be interpreted.
- wcstombs() 第三个参数 This is the maximum number of bytes to be written to str.
第三个参数的赋值很容易弄错,而且在不同平台下还有出入。在网上找到 一篇说明,在第一个参数赋值 NULL
(此时第三个参数也无意义,随便赋值) 的情况下,返回目标缓存所需的大小(不含终结符):
size_t req = mbstowcs(NULL, str, 0);
返回 str 转宽字符串时 wchar_t 字符个数;size_t req = wcstombs(NULL, wstr, 0);
返回 wstr 转多字节字符串时字节数(同时也是 char 类型个数);
所以我们综合有关讲解,给出一份跨平台,省时省力搬来搬去可以直接用的 C++ 代码:
1 |
|
ps 这里主要是为了演示 mbstowcs()
和 wcstombs()
的使用。如果是转换 string
和 wstring
,可以参考 How to convert wstring into string?
不清楚为什么网上好多资源给出的代码都是手动给定第三个参数,/(ㄒoㄒ)/~~
为什么要有本地策略集?
在不同平台,编译器编译源代码时根据字符串前是否有 L 前缀,将其 硬编码 为不同的字节码:
os/ | L | |
---|---|---|
Linux | UTF-32 | UTF-8 |
Windows | USC-2 | ASCII/gb2312/big5 |
- 如果字符串有前缀 L,那么在编译器确定的前提下,使用的字符编码是确定的,硬编码的内容也是确定的;
- 如果字符串无前缀 L,那么编译器 直接读取字符(串)在源文件中的编码数值,因为源文件编码不同,其硬编码的内容是不确定的;
基于上述描述可以推断,char 和 wchar_t 互转,在编译项目时(编译器确定)宽字符的硬编码映射单一,但多字节字符的硬编码(主要针对 ANSI 系的非 ASCII 部分)应该以哪种字符集编码方式解析(通俗地理解就是某几个字节究竟是翻译成简体中文、繁体中文还是日文之类的)是不确定的。所以需要本地策略集(locale)。
ps 如果要做国际化的东西,需要深入了解 locale 策略,可以学习 彻底解密C++宽字符 的系列文章作为切入点。中间两三篇涉及代码的具体实现可能略微复杂,可以暂时忽略,但推荐一定要读前三篇和 彻底解密C++宽字符:6、国际化策略(完),通过了解“硬编码的硬伤”进一步理解“硬编码”,理解编译、字符集等偏底层的知识。
LP[,C][,W,T]STR
这几个是 Windows 平台对 char wchar_t 类型的封装。如标题,我们可以组合出 6 种情况:
xx | x | W | T |
---|---|---|---|
x | LPSTR | LPWSTR | LPTSTR |
C | LPCSTR | LPCWSTR | LPCTSTR |
基于 | char | wchar_t |
- L表示
long
指针,这是为了兼容 Windows 3.1 等 16 位操作系统遗留下来的,在 win32 中以及其他的 32位操作系统中,long
指针和near
指针及far
修饰符都是为了兼容的作用,没有实际意义。即 win32 中,long
near
far
指针与普通指针没有区别,LP 与P是等效的。 - P表示这是一个指针。
- STR表示这个变量是一个字符串。
- C表示是一个常量,
const
。 - W 来源于
wchar_t
(宽字符) - T表示**_T宏**,这个宏用来表示你的字符是否使用 UNICODE, 如果你的程序定义了 UNICODE或者其他相关的宏,那么这个字符或者字符串将被作为 UNICODE字符串,否则就是标准的 ANSI字符串。
LPTSTR:如果定义了 UNICODE宏则 LPTSTR被定义为 LPWSTR,typedef LPTSTR LPWSTR
;否则LPTSTR被定义为LPSTR,typedef LPTSTR LPSTR
。
更系统、更精彩的介绍参考 What are TCHAR, WCHAR, LPSTR, LPWSTR, LPCTSTR (etc.)?
关于这些类型彼此之间转换以及和 CString 之间的转换,可以参考 懒得起名…。这里只罗列几个特殊的转换:
W2A W2CA CW2A CW2CA
A2W A2CW CA2W CA2CW
有上述这些也可以推测出 W2T A2T T2W T2A 等等的存在,这里特别提一下 [CT,CW,CA]2[CT,CW,CA] 是存在的。
1 | // CString和LPCTSTR不需要转化,两者是等价的; |
CString 和 string
CString
是动态的 TCHAR
数组。TCHAR
是一个宏,是对 char
和 wchar_t
的选择:
1 | typedef ATL::CStringT< wchar_t, StrTraitMFC_DLL< wchar_t > > CStringW; |
1 |
|
再次强调,Windows 的 wchar_t
对 Unicode 的支持是不完善的。
string
是动态的 char
数组;wstring
是动态的 wchar_t
数组。所以 CString 转 string 存在潜在的 wchar_t
转换为 char
类型的可能,反之亦然。
CString 转 string
1 | CString cstr; |
string 转 CString
下意识就会使用 CA2CT 宏做逆向操作,但其实我们用 char* 构造 CString 时无论是否 UNICODE 工程还是 MBCS 工程都是可以顺利编译的:-D
1 | string str("hah"); |
在 代码片段三 中提到一种特殊情况,即 string 字符串中存在 \0
字符时,可以如下构造 CString
1 | std::string stdstr("foo"); |