字符(串)之间的转换

字符串之间的转换分为两类:

  1. 底层字符集之间的转换,表现为 char wchar_t 之间的转换,string wstring 之间的转换;
  2. 在此之上牵扯到 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
2
3
4
5
#include <locale.h>
setlocale(LC_ALL, "");

wcstombs(char * to,wchar_t * from,size_t _maxCount);
mbstowcs(wchar_t * to,char * from,size_t _maxCount);

char 和 wchar_t 之间的转换可以使用上述两个函数,使用的关键在于 setlocale() 和第三个参数,难点也在第三个参数:

  1. 设置本地策略集,使得编译器知晓多字节字符集使用的具体是哪一种;
  2. 第三个参数表示目标指针的字节长度,尤其是转为宽字符时需仔细辨识,一个宽字符占用的字节和平台有严格的关联。
  3. mbstowcs() 第三个参数 This is the maximum number of wchar_t characters to be interpreted.
  4. wcstombs() 第三个参数 This is the maximum number of bytes to be written to str.

第三个参数的赋值很容易弄错,而且在不同平台下还有出入。在网上找到 一篇说明,在第一个参数赋值 NULL(此时第三个参数也无意义,随便赋值) 的情况下,返回目标缓存所需的大小(不含终结符):

  1. size_t req = mbstowcs(NULL, str, 0); 返回 str 转宽字符串时 wchar_t 字符个数;
  2. size_t req = wcstombs(NULL, wstr, 0); 返回 wstr 转多字节字符串时字节数(同时也是 char 类型个数);

所以我们综合有关讲解,给出一份跨平台,省时省力搬来搬去可以直接用的 C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <string>
#include <iostream>
#include <cstdlib>

const std::wstring s2ws(const std::string& s)
{
std::locale old_loc =
std::locale::global(std::locale(""));

const char* src_str = s.c_str();
const size_t buffer_size = std::mbstowcs(NULL, src_str, 0);
wchar_t* dst_wstr = new wchar_t[buffer_size];
wmemset(dst_wstr, 0, buffer_size);
std::mbstowcs(dst_wstr, src_str, buffer_size);
std::wstring result = dst_wstr;
delete []dst_wstr;

std::locale::global(old_loc);

return result;
}

const std::string ws2s(const std::wstring& ws)
{
std::locale old_loc =
std::locale::global(std::locale(""));

const wchar_t* src_wstr = ws.c_str();
size_t buffer_size = std::wcstombs(NULL ,src_wstr, 0);;
char* dst_str = new char[buffer_size];
memset(dst_str, 0, buffer_size);
std::wcstombs(dst_str ,src_wstr, buffer_size);
std::string result = dst_str;
delete []dst_str;

std::locale::global(old_loc);

return result;
}

ps 这里主要是为了演示 mbstowcs()wcstombs() 的使用。如果是转换 stringwstring,可以参考 How to convert wstring into string?

不清楚为什么网上好多资源给出的代码都是手动给定第三个参数,/(ㄒoㄒ)/~~

为什么要有本地策略集?

在不同平台,编译器编译源代码时根据字符串前是否有 L 前缀,将其 硬编码 为不同的字节码:

os/ L 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
2
3
4
5
6
// CString和LPCTSTR不需要转化,两者是等价的;
// 但去掉常量属性呢?即 CString 向 LPTSTR
CString str("string");
LPTSTR pStr = str.GetBuffer();
str.ReleaseBuffer();
//注意:GetBuffer()和ReleaseBuffer()之间不可以调用任何的CString函数,比如GetLength()函数,因为无法预测对内存的操作,所以任何CString函数得到的结果都是不确定的。

CString 和 string

CString 是动态的 TCHAR 数组。TCHAR 是一个宏,是对 charwchar_t 的选择:

1
2
3
typedef ATL::CStringT< wchar_t, StrTraitMFC_DLL< wchar_t > > CStringW;
typedef ATL::CStringT< char, StrTraitMFC_DLL< char > > CStringA;
typedef ATL::CStringT< TCHAR, StrTraitMFC_DLL< TCHAR > > CString;
1
2
3
4
5
#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif

再次强调,Windows 的 wchar_t 对 Unicode 的支持是不完善的。

string 是动态的 char 数组;wstring 是动态的 wchar_t 数组。所以 CString 转 string 存在潜在的 wchar_t 转换为 char 类型的可能,反之亦然。

CString 转 string

1
2
3
CString cstr;
CT2CA psz(cstr);
std::string str(psz);

string 转 CString

下意识就会使用 CA2CT 宏做逆向操作,但其实我们用 char* 构造 CString 时无论是否 UNICODE 工程还是 MBCS 工程都是可以顺利编译的:-D

1
2
string str("hah");
CString cstr(str.c_str());

代码片段三 中提到一种特殊情况,即 string 字符串中存在 \0 字符时,可以如下构造 CString

1
2
3
4
std::string stdstr("foo");
stdstr += '\0';
stdstr += "bar";
CString cstr(stdstr.c_str(), stdstr.length());

CString 转 wstring