Windows 平台编程的字符串那些注意的东西


Unicode 与多字节

Windows 支持 Unicode 后, 所有和字符串相关的 Windows API 都有了两个版本, 以 _A 结尾的和带 _W 结尾。 比如函数 MessageBox 就有 MessageBoxA 和 MessageBoxW 两个版本。 MessageBox 只是一个宏,在编译的时候根据项目的字符集设置, 用 MessageBoxA 或者 MessageBoxW 替换。 User32.dll 导出了 MessageBoxA 和 MessageBoxW, 没有导出 MessageBox, 换而言之 MessageBox 函数并不存在。

图: Unicode 字符集设置
??? Windows 平台编程的字符串那些注意的东西

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, 虽然差一个字母功能也一样, 但却是完全不同的东西。



Windows 平台编程的字符串那些注意的东西

上一篇:Windows 7安装Apache FtpServer 1.0.0


下一篇:关于Windows 10上MarkdownPad2无法预览的解决办法