Unicode 与多字节
Windows 支持 Unicode 后, 所有和字符串相关的 Windows API 都有了两个版本, 以 _A 结尾的和带 _W 结尾。 比如函数 MessageBox 就有 MessageBoxA 和 MessageBoxW 两个版本。 MessageBox 只是一个宏,在编译的时候根据项目的字符集设置, 用 MessageBoxA 或者 MessageBoxW 替换。 User32.dll 导出了 MessageBoxA 和 MessageBoxW, 没有导出 MessageBox, 换而言之 MessageBox 函数并不存在。
图: Unicode 字符集设置
???
int WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
int WINAPI MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
#ifdef UNICODE
#define MessageBox MessageBoxW // 如果项目使用 Unicode , UNICODE 宏会被定义,MessageBox 被 MessageBoxW 替换
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
C 风格字符串
Windows API 统一使用一系列数据类型, 比如 DWORD INT32 等等, 而字符串的表示使用 CHAR, LPSTR, LPCSTR
- LPSTR 就是 char * 类型, 字符串内容允许被修改
- LPCSTR 就是 const char * 类型, 字符串内容不允许修改
CHAR filename[MAX_PATH] = "C:\\Windows\\System32\\user32.dll";
LPSTR filename2 = filename; // 正确
LPCSTR filename3 = "C:\\Windows\\System32\\user32.dll"; // 正确
//LPSTR filename4 = "C:\\Windows\\System32\\user32.dll"; // 编译错误 C2440, 无法从“const char [31]”转换为“LPSTR”
LoadLibraryA(filename);
Unicode 版本的 Windows API 使用 LPWSTR, LPCWSTR 来传递字符串参数
- LPWSTR 就是 wchar_t* 类型, 字符串内容允许被修改
- LPCWSTR 就是 const wchar_t* 类型, 字符串内容不允许修改
WCHAR filename[MAX_PATH] = L"C:\\Windows\\System32\\user32.dll";
LPWSTR filename2 = filename; // 正确
LPCWSTR filename3 = L"C:\\Windows\\System32\\user32.dll"; // 正确
//LPWSTR filename4 = L"C:\\Windows\\System32\\user32.dll"; // 编译错误 C2440, 无法从“const wchar_t [31]”转换为“LPWSTR”
LoadLibraryW(filename);
还有中性的 LPTSTR, LPCTSTR, 这里的中性的意思是, 项目字符集无论是 Unicode 还是 多字节字符集, 代码都是可以编译的
如果项目使用 Unicode 字符集
- LPTSTR 就是 LPWSTR, 也就是 wchar_t *
- LPCTSTR 就是 LPCWSTR, 也就是 const wchar_t*
如果项目使用多字节字符集
- LPTSTR 就是 LPSTR, 也就是 char*
- LPCTSTR 就是 LPCSTR, 也就是 const char*
LPTSTR 字符串允许修改
LPCTSTR 字符串不能修改
当使用中性的字符串字面量时, 要把字面量放在 _T() 宏中, _T() 宏的使用是根据字符集设置决定要不要在字面量前面加一个 L
比如 _T("user32.dll") 在 Unicode 字符集项目下展开为 L"user32.dll"
而在多字节项目下就是 "user32.dll"
也可以使用TEXT() 宏, 用法功能都和 _T() 宏一样
TCHAR filename[MAX_PATH] = _T("C:\\Windows\\System32\\user32.dll");
LPTSTR filename2 = filename; // 正确
LPCTSTR filename3 = _T("C:\\Windows\\System32\\user32.dll"); // 正确
LoadLibrary(filename); // 中性版本的 LoadLibrary
ATL/MFC 中的 CString 和 CStrBuf
注意: CString 实际上是 CStringA 或者 CStringW 的别名, 在 Unicode 字符集的项目中, CString 就是 CStringA; 在多字节项目中, CString 就是 CStringW
提示: CString 因为历史原因有 MFC 版本实现和 ATL 版本实现, 不过从 VS2008 之后的版本都统一了(VS2008 以前的没用过), 只需要包含 atlstr.h 头文件,即使在控制台应用程序也能使用, 并不是只有 MFC 项目才能用
CStringW 里有对 LPCWSTR 隐式类型转换操作重载,CStringW::operator LPCWSTR() const, CStringW 对象可以直接赋值给 LPCWSTR 指针(const wchar_t* 指针),可以在任何使用 LPCWSTR 指针的位置使用 CStringW 对象。
CStringW message = L"这是一条消息";
LPCTSTR message2 = message; // 隐式调用 message.operator LPCWSTR()
const wchar_t* message3 = message; // 隐式调用 message.operator LPCWSTR()
CStringW title = L"这是标题";
MessageBoxW(NULL, message2, title, MB_OK); // 隐式调用 title.operator LPCWSTR()
但是 CStringW 不能直接赋值给 LPCSTR 指针(const char* ), 因为 CStringW 没有对 LPCSTR 指针(const char*指针) 的类型转换重载
CStringW message = L"这是一条消息";
LPCSTR message2 = message; // 编译错误
const char* message3 = message; // 编译错误
CStringW title = L"这是标题";
MessageBoxA(NULL, message2, title, MB_OK); // 编译错误
当一个 CStringW 对象赋值给 LPCWSTR, LPCWSTR 指针指向的内存由 CStringW 对象来管理, 当 CStringW 对象被删除时, 这个指针会变成悬垂指针。所以在使用这个指针时, 必须保证 CStringW 对象是有效的
像这样的操作是错误的, GetSomePath() 返回的实际是上 悬垂指针, 像这种情况可以使用 CStringW 类型返回值
LPCWSTR GetSomePath()
{
CStringW path;
path = L"C:\\Windows\\System32\\user32.dll";
return path; // 错误
}
相对的 CStringA 也是同样的操作, 可以隐式转换为 LPCSTR , 这里不重复示例
CString 是中性的, 所以在初始化字符串应该用中性的 _T()宏,使用 C 字符串指针用中性的 LPCTSTR, API 调用也应该使用中性的, 这样可以确保在 Unicode 字符集和 多字节字符集两种字符集设置的情况都能编译通过
CString msg = _T("Welcome !");
LPCTSTR msg2 = msg;
MessageBox(NULL, msg, msg2, MB_OK);
像下面这样的操作是错误的, 因为 CString 可以是 CStringW , 也可以是 CStringA, 根据项目的字符集设置决定, 如果使用了非中性的 C 字符串指针, 或者调用非中性 API , 那肯定有一种字符集下是会编译报错的。 也许你并不要求在两种字符集都能编译, 比方说只要求在 Unicode 字符集编译通过就行了, 那是不是就可以这样呢? 如果是这样还不如直接使用 CStringW , 这样代码意思会更明确, 并且即使在 多字节下编译也没问题。
CString msg = _T("Welcome !");
LPCSTR msg2 = msg; // 错误
LPCWSTR msg3 = msg; // 错误
MessageBoxA(NULL, msg, nullptr, MB_OK); // 错误
MessageBoxW(NULL, msg, nullptr, MB_OK); // 错误
与 LPSTR 、 LPWSTR 和 LPTSTR 的转换
有时候需要从 API 获取一个字符串, 这就要求 CStringW 能够返回一个允许修改字符串内容的 LPWSTR , 而不是 LPCWSTR , 这时候可以使用 GetBuffer()和ReleaseBuffer() 方法,GetBuffer 可以接收一个字符数长度的参数, 返回一个足够大小的缓冲区指针,
在使用这个指针修改了字符串后, 调用 ReleaseBuffer 来更新 CStringW 长度, 确保 CStringW 的 GetLength() 方法返回正确长度, ReleaseBuffer 接收一个长度参数, 如果不指定参数, 内部会使用类似 wstrlen 的方式重新计算长度。
CStringW exePath;
LPWSTR buffer = exePath.GetBuffer(MAX_PATH);
DWORD pathLen = ::GetModuleFileNameW(NULL, buffer, MAX_PATH);
exePath.ReleaseBuffer(pathLen);
buffer = nullptr;
MessageBoxW(NULL, exePath, L"", MB_OK);
像下面这样的操作是错误,是非常危险,非常 SB...,
CStringW exePath;
LPWSTR buffer = (LPWSTR)(LPCWSTR)exePath; // 错误
::GetModuleFileNameW(NULL, buffer, MAX_PATH);
MessageBoxW(NULL, exePath, L"", MB_OK);
如果觉得 GetBuffer() / ReleaseBuffer() 的方式太麻烦, 你可以使用 CStrBuf
CString 利用 C++ RAII (Resource Acquisition Is Initialization programming idiom) 特性, 自动调用 GetBuffer() / ReleaseBuffer()
当然 CStrBuf 也有三版本, 另外两个是 CStrBufW 和 CStrBufA , 如果是 CStringW , 那肯定要用 CStringW,
CStringW exePath;
CStrBufW buffer(exePath, MAX_PATH);
::GetModuleFileNameW(NULL, buffer, MAX_PATH);
MessageBoxW(NULL, exePath, L"", MB_OK);
更少的代码
CStringW exePath;
::GetModuleFileNameW(NULL, CStrBufW(exePath, MAX_PATH), MAX_PATH);
MessageBoxW(NULL, exePath, L"", MB_OK);
Unicode 、 多字节 和 中性字符串之间的互转
这个问题一直困扰着一大片新手,一定有人大喊用 MultiByteToWideChar/WideCharToMultiByte 啊。
大多数情况下我们需要转换的不是 UTF-8, 而是 ANSI 编码(包括了 GB2312 ,GBK, Big5 等等) , 其实直接用 CString CStringA CStringW 就能转了
LPCSTR msg = "比方说这是一行文本"; // ANSI 编码
MessageBoxW(NULL, CStringW(msg), L"", MB_OK); // Unicode 编码
MessageBox(NULL, CString(msg), L"", MB_OK); // 中性
当然为了避免额外的转换操作, 在使用字面量初始化 CString CStringA CStringW 的时候, 最好使用指定的编码不要搞混
CStringA msg1 = "比方说这是一行文本";
CStringW msg2 = L"比方说这是一行文本";
CString msg3 = _T("比方说这是一行文本");
还可以使用这几个ATL类互转, 具体意思如其名, 这几个还有带 _EX 结尾的, 具体请参考 ATL 源码
- CW2A
- CA2W
- CW2T
- CA2T
- CT2W
- CT2A
使用示例
LPCSTR msg = "比方说这是一行文本"; // ANSI 编码
MessageBoxW(NULL, CA2W(msg), L"", MB_OK); // Unicode 编码
MessageBox(NULL, CA2T(msg), L"", MB_OK); // 中性
注意 W2A 和 CW2A 不一样, 前者 W2A 是 ATL7 版本以前提供的, 需先使用宏 USES_CONVERSION, 然后再使用 W2A 之类的宏, 每使用 W2A 转换一次就要在栈上分配空间, 而这部分空间只有在函数返回时才会得到释放, 所以 W2A 是不建议在循环中使用, 因为循环次数多会导致栈空间不够而转换失败。当然 CW2A 之类的转换并不存在这些问题,随便用 ! 总之 W2A 是有问题了,如果你能用 CW2A 就千万别用 W2A, 虽然差一个字母功能也一样, 但却是完全不同的东西。