MFC 中 CString 与 WPARAM 之间的转换

详解 MFC 消息传递中 CString 无法直接强转为 WPARAM 的原因,以及两种正确的转换方案,并介绍结构体指针传递的正确姿势。

$1.5k 字/约 7 min👁— views

MFC 中 CString 与 WPARAM 之间的转换

在 MFC 编程中,经常需要通过 Windows 消息(SendMessage/PostMessage)传递字符串数据。CString 是 MFC 的核心字符串类,而 WPARAMLPARAM)本质上是整数类型,两者之间的转换有很多细节需要注意。

MFC CString 内存布局

CStringCStringT 模板的特化)的内存布局经过精心设计,支持引用计数和写时复制(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()

内存安全总结

  1. PostMessage 异步,不能传局部变量或栈上数据——使用 new 在堆上分配,接收方负责 delete

  2. SendMessage 同步,可以传局部变量——但仅限同线程或跨线程 SendMessage 不会死锁的场景。

  3. 所有权协议new 出来的数据,如果通过 PostMessage 发出,所有权就转移了,发送方不再访问也不再释放。

  4. PostMessage 失败时必须自己 delete——检查返回值!

  5. 跨进程用 WM_COPYDATA——进程边界两侧指针指向不同地址空间,唯一安全的传字符串方式是 WM_COPYDATA

  6. 优先考虑 WM_COPYDATA——它由系统管理数据复制,无需手动 new/delete,是最健壮的方式(但只能用 SendMessage)。