MFC 中 CString 与 WPARAM 之间的转换
在 MFC 编程中,经常需要通过 Windows 消息(SendMessage/PostMessage)传递字符串数据。CString 是 MFC 的核心字符串类,而 WPARAM(LPARAM)本质上是整数类型,两者之间的转换有很多细节需要注意。
MFC CString 内存布局
CString(CStringT 模板的特化)的内存布局经过精心设计,支持引用计数和写时复制(COW,Copy-On-Write)。
在内存中,CString 对象本身只是一个指针,指向一个带有头部信息的字符缓冲区:
CString 对象(4/8 字节指针)
│
▼
[StringData 头部]
nRefs: 引用计数(负数表示锁定)
nDataLength: 实际字符串长度
nAllocLength: 分配的缓冲区长度
nCrtAllocBytes: CRT 分配大小
[字符数据区]
字符 0, 1, 2, ... <── m_pszData 指向这里
NUL(终止符)
关键点:CString::GetString()(或强制转换 (LPCTSTR)str)返回的是字符数据区的起始地址,而不是 CString 对象本身的地址。
CString str = _T("Hello");
LPCTSTR p1 = str.GetString(); // 指向字符数据
LPCTSTR p2 = (LPCTSTR)str; // 等价
// p1 == p2 == 字符数据的起始地址
WPARAM 的本质(UINT_PTR)
WPARAM 定义为:
typedef UINT_PTR WPARAM;
typedef LONG_PTR LPARAM;
其中 UINT_PTR 在 32 位系统上是 unsigned int(4 字节),在 64 位系统上是 unsigned __int64(8 字节)。
它足够宽以容纳一个指针,这是 Windows API 兼容性设计的一部分。
直接转换的危险
危险 1:传递临时对象的指针
// ❌ 极度危险!
void BadSend(HWND hwnd) {
CString str = _T("Hello");
// str 在函数栈上,返回后销毁
// 但 SendMessage 是同步的,这里可能侥幸可以
SendMessage(hwnd, WM_USER_STR, 0, (LPARAM)(LPCTSTR)str);
}
// ❌ 更危险:PostMessage(异步)
void VeryBadSend(HWND hwnd) {
CString str = _T("Hello");
PostMessage(hwnd, WM_USER_STR, 0, (LPARAM)(LPCTSTR)str);
// 函数返回,str 销毁,接收方处理时是悬空指针!
}
危险 2:直接传 CString 对象指针
// ❌ 绕过 CString 接口直接传对象指针,接收方用法不统一
CString* pStr = new CString(_T("Hello"));
PostMessage(hwnd, WM_USER_STR, 0, (LPARAM)pStr);
// 接收方需要知道这是 CString*,而非 LPCTSTR
// 且必须 delete,否则内存泄漏
虽然传 CString* 技术上可行,但混用裸指针和智能指针会增加维护负担。
危险 3:COW 引发的意外
// ❌ COW 潜在问题
CString original = _T("Hello");
CString copy = original; // 浅拷贝,共享缓冲区
// 传递 copy 的 LPCTSTR
SendMessage(hwnd, WM_USER_STR, 0, (LPARAM)(LPCTSTR)copy);
// 如果接收方在另一个线程修改了 copy(通过某种方式),
// COW 的写时拷贝可能在错误时机发生
正确的临时缓冲方案
方案一:SendMessage + 接收方提供缓冲区
这是最传统的 Win32 风格,接收方分配缓冲区,发送方填充:
#define WM_GET_TITLE (WM_USER + 1)
// 发送方:提供缓冲区,SendMessage 等待填充
TCHAR buffer[256] = {0};
SendMessage(hwnd, WM_GET_TITLE, (WPARAM)256, (LPARAM)buffer);
CString title = buffer;
// 接收方:填充缓冲区
case WM_GET_TITLE: {
UINT bufSize = (UINT)wParam;
LPTSTR pBuf = (LPTSTR)lParam;
CString myTitle = _T("My Application");
_tcsncpy_s(pBuf, bufSize, myTitle, _TRUNCATE);
return 0;
}
方案二:动态分配 + 所有权转移(PostMessage)
#define WM_PUSH_STRING (WM_USER + 2)
// 发送方:在堆上分配 CString,转移所有权
void SendStringAsync(HWND hwnd, const CString& str) {
CString* pStr = new CString(str); // 堆上副本
if (!PostMessage(hwnd, WM_PUSH_STRING, 0, (LPARAM)pStr)) {
delete pStr; // 入队失败,自己释放
// 处理错误
}
// 所有权已转移,不再访问 pStr
}
// 接收方:接管所有权,处理完 delete
case WM_PUSH_STRING: {
CString* pStr = reinterpret_cast<CString*>(lParam);
if (!pStr) return 0;
// 使用字符串
SetWindowText(*pStr);
delete pStr; // 释放内存
return 0;
}
方案三:WM_COPYDATA(最安全,适合跨进程)
#define MY_WM_COPYDATA_STRING 1
// 发送方
void SendStringViaCopyData(HWND hwndTarget, HWND hwndSelf, const CString& str) {
// 准备 COPYDATASTRUCT
COPYDATASTRUCT cds;
cds.dwData = MY_WM_COPYDATA_STRING;
cds.cbData = (str.GetLength() + 1) * sizeof(TCHAR);
cds.lpData = (PVOID)(LPCTSTR)str;
// SendMessage 是同步的,lpData 在函数返回前安全
SendMessage(hwndTarget, WM_COPYDATA, (WPARAM)hwndSelf, (LPARAM)&cds);
// 注意:WM_COPYDATA 只能用 SendMessage,不能用 PostMessage
}
// 接收方
case WM_COPYDATA: {
COPYDATASTRUCT* pCds = reinterpret_cast<COPYDATASTRUCT*>(lParam);
if (pCds->dwData == MY_WM_COPYDATA_STRING) {
CString received((LPCTSTR)pCds->lpData,
pCds->cbData / sizeof(TCHAR) - 1);
// 使用 received...
}
return TRUE;
}
WM_COPYDATA 的优点:系统自动处理数据复制,不需要手动 new/delete,跨进程也安全。
SendMessage vs PostMessage 差异
| 特性 | SendMessage | PostMessage |
|---|---|---|
| 执行方式 | 同步(等待消息处理完成) | 异步(立即返回) |
| 返回值 | 消息处理函数的返回值 | 是否成功入队(BOOL) |
| 数据生命周期 | 由调用方控制(在函数返回前安全) | 必须确保接收方处理时数据有效 |
| 局部变量 | 可以传局部变量地址(同步保证安全) | 不能传局部变量地址! |
| 死锁风险 | 有(跨线程相互等待) | 无(异步) |
| 跨进程 | 支持(注意指针在对方进程无意义) | 支持有限 |
SendMessage 传局部变量的正确用法:
void SendLocalString(HWND hwnd) {
CString localStr = _T("This is local");
// ✅ OK:SendMessage 是同步的
// 函数在 SendMessage 返回之前不会结束,
// localStr 在接收方处理时依然有效
SendMessage(hwnd, WM_USER_STR, 0, (LPARAM)(LPCTSTR)localStr);
// 此时 SendMessage 已返回,localStr 销毁
}
示例代码:完整的线程间通信
// 消息定义
#define WM_UI_UPDATE_TEXT (WM_USER + 10)
// ========== 工作线程 ==========
UINT WorkerThread(LPVOID pParam) {
HWND hwndMain = (HWND)pParam;
for (int i = 0; i < 10; i++) {
// 模拟耗时操作
Sleep(1000);
// 生成结果字符串
CString result;
result.Format(_T("Step %d completed"), i + 1);
// 堆上副本,PostMessage 异步发送
CString* pResult = new CString(result);
if (!PostMessage(hwndMain, WM_UI_UPDATE_TEXT, i, (LPARAM)pResult)) {
delete pResult;
}
}
// 发送完成通知(无额外数据)
PostMessage(hwndMain, WM_UI_UPDATE_TEXT, -1, 0);
return 0;
}
// ========== 主线程消息处理 ==========
LRESULT CMainDlg::OnUpdateText(WPARAM wParam, LPARAM lParam) {
int step = (int)wParam;
if (step == -1) {
// 完成
SetDlgItemText(IDC_STATUS, _T("All done!"));
return 0;
}
// 接管 CString 所有权
CString* pText = reinterpret_cast<CString*>(lParam);
if (pText) {
SetDlgItemText(IDC_STATUS, *pText);
delete pText;
}
return 0;
}
消息映射:
BEGIN_MESSAGE_MAP(CMainDlg, CDialog)
ON_MESSAGE(WM_UI_UPDATE_TEXT, &CMainDlg::OnUpdateText)
END_MESSAGE_MAP()
内存安全总结
-
PostMessage 异步,不能传局部变量或栈上数据——使用
new在堆上分配,接收方负责delete。 -
SendMessage 同步,可以传局部变量——但仅限同线程或跨线程 SendMessage 不会死锁的场景。
-
所有权协议:
new出来的数据,如果通过PostMessage发出,所有权就转移了,发送方不再访问也不再释放。 -
PostMessage 失败时必须自己 delete——检查返回值!
-
跨进程用 WM_COPYDATA——进程边界两侧指针指向不同地址空间,唯一安全的传字符串方式是
WM_COPYDATA。 -
优先考虑
WM_COPYDATA——它由系统管理数据复制,无需手动new/delete,是最健壮的方式(但只能用SendMessage)。