十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MFC类型转换实战:CString与字符集切换的坑与解法

MFC类型转换实战:CString与字符集切换的坑与解法 简介一份面向MFC开发者的类型转换知识点整理文档针对Windows C项目开发中频繁出现的类型转换需求系统汇总了CString与char*、LPCSTR、LPSTR等字符串类型之间的互转方式也覆盖了字符数组与int、float等数值类型之间的转换并给出atoi、atof、itoa、ltoa等函数的调用示例。文档还简要整理了BSTR、_bstr_t、CComBSTR以及VARIANT、_variant_t、COleVariant等封装类型的基本操作方便在COM或OLE自动化场景下快速查阅。整个资源仅1个PDF文件大小56KB体积小、内容紧凑适合开发者下载后当作速查手册使用。目前已有99人学习对于刚接触MFC或常常需要处理类型转换的开发者来说这份文档能帮助快速定位转换写法同时规避内存管理和类型安全方面的常见坑点是一份实用的参考笔记。1. 为什么 MFC 类型转换总在字符集切换时崩掉很多 MFC 项目跑了两三年Debug/Release 都稳定结果某天把工程属性里的“字符集”从“使用多字节字符集”改成“使用 Unicode 字符集”一编译能过运行起来却到处乱码、断言的弹窗一个接一个。这个时刻能逼疯人的点基本不在业务逻辑而在类型转换的边界上某个函数接收 const char*某个接口要 LPCTSTR另一段代码又用 sprintf 拼了 CString这些代码在 ANSI 时代能互相迁就切到 Unicode 后就不成立了。下面要把 MFC 类型转换里最常走的几条路排清楚给出每条路径上可抄的代码和必须调的参数也把缓冲区谁负责释放、宽窄字符在什么时候混用这类坑写明白。这篇整理适合正在维护旧 MFC 工程的开发者也适合刚把对话框程序从 VS 老版本迁到新版本的人翻着用。2. MFC 类型转换以 CString 为中转点的常用写法2.1 为什么 CString 适合做转换的中间点MFC 控件取回来的文本、路径、注册表值绝大多数都先落到 CString。CString 在 MFC 里实际是 CStringTTCHAR 的实例TCHAR 在 Unicode 工程下展开为 wchar_t在多字节工程下展开为 char。也就是说同一行 CString 代码编译出来的底层字符类型完全由工程属性里的“字符集”决定而不是由代码本身决定。所以写转换时第一件事不是翻函数签名而是确认当前工程的 _UNICODE 宏。在 VS 工程属性里“配置属性 - 常规 - 字符集”选中的是“使用 Unicode 字符集”时编译器会同时定义 UNICODE 和 _UNICODE选“使用多字节字符集”时两个宏都不存在。很多老代码编译不过或者运行时乱码就是因为在 ANSI 工程里写了 CString 到 char* 的直接赋值到了 Unicode 下双方类型不再是同一族。下面这张表是我在遇到需要从 CString 向其它类型转时优先会查的对应关系。目标类型常用手段需要注意的点const char*CStringA 构造后再取 GetString编码默认是 ANSI不一定是 UTF-8char*/TCHAR* 缓冲区GetBuffer/ReleaseBuffer不调用 ReleaseBuffer 之前别使用 CString 自身std::stringCW2A 或 WideCharToMultiByte跨平台时建议显式传 CP_UTF8BSTRAllocSysString / CComBSTR用完后必须 SysFreeStringint/DWORD_ttoi / _tcstoul / _tcstol空串和溢出都会返回 0需要看 errno2.2 CString 与 const char* 互转的常规做法最常见的错误写法是直接强转CString strText _T(abc); const char* pData (const char*)(LPCTSTR)strText; // 仅 ANSI 工程可编译这段代码在 Unicode 工程里编译报错因为 LPCTSTR 展开后是 const wchar_t*强转成 const char* 属于类型不兼容。即使编译器放行运行时会拿到第一个字节的地址字符串内容也完全是错的。如果确实需要 ANSI 字符串用 CStringA 做一次显式拷贝CString strText _T(abc); CStringA strAnsi(strText); // 用当前 CString 内容构造窄字符副本 const char* pData strAnsi.GetString(); SomeCLibraryFunction(pData); // C 接口内部会拷贝 pData可以安全使用CStringA 的构造过程会把宽字符按当前系统代码页转成窄字符默认不是 UTF-8。如果第三方库要 UTF-8应该指定代码页CString strText _T(中文); int nLen ::WideCharToMultiByte(CP_UTF8, 0, strText, -1, nullptr, 0, nullptr, nullptr); std::string strUtf8(nLen - 1, \0); ::WideCharToMultiByte(CP_UTF8, 0, strText, -1, strUtf8[0], nLen - 1, nullptr, nullptr);WideCharToMultiByte 的第三个参数传 -1表示把字符串末尾的终止符也一起转换返回值 nLen 包含终止符所以构造 std::string 时要减掉 1。第六个参数传 nullptr可以让 API 先算出需要的缓冲区长度避免手动猜大小。转换只发生在这一行CStringA 或 std::string 的缓冲区谁申请谁释放不用手动 delete比 GetBuffer 安全得多。2.3 CString 与 std::string 双向转换双向转换的麻烦不在语法而在编码一致。CString 在 Unicode 工程下存的是 UTF-16std::string 的编码则完全由代码决定默认是 char 序列。把 CString 转成 std::string 时不指定代码页就等于把宽字符交给系统默认代码页在中文系统上得到的是 GBK在英文系统上得到的是 CP1252同一个程序两边产出的字节完全不一样。我会用 ATL 提供的 CW2A 宏来压缩样板代码CString strWide _T(配置文件); std::string strUtf8 CW2A(strWide.GetString(), CP_UTF8).m_sz;CW2A 的构造参数里第一个是源字符串指针第二个是目标代码页。CP_UTF8 表示生成 UTF-8 字节如果写 CP_ACP 就是系统 ANSI 代码页。m_sz 返回转换后的 char 指针把它直接赋给 std::string 会拷贝一份数据所以临时对象生命周期的坑在这里不存在。反向转换时用 CA2Wstd::string strInput 配置文件; CString strWide CA2W(strInput.c_str(), CP_UTF8).m_sz;注意代码里只要出现过 CW2A、CA2W、W2A、A2W 这一类宏就统一显式写代码页不要依赖默认值。CP_ACP 和 CP_UTF8 的结果差异很大默认参数在换一台机器后可能就不一样。这个写法对服务端返回的 JSON 字节、从文件里读出来的 UTF-8 文本都适用。2.4 从 MFC combo box 控件读取文本并继续向下转换把控件取值和类型转换连起来是 MFC 对话框里最常见的组合。比如一个下拉框里放的是编号字符串用户选中后要转成 int 做查询完整路径是这样的CComboBox* pCombo (CComboBox*)GetDlgItem(IDC_COMBO_TYPE); int nIndex pCombo-GetCurSel(); if (nIndex CB_ERR) { return; // 没有任何选中项 } CString strItem; pCombo-GetLBText(nIndex, strItem); int nTypeId _ttoi(strItem);GetCurSel 返回当前选中项的索引没有选中时是 CB_ERR所以先判一下避免用 -1 去取文本。GetLBText 的第二个参数传 CString 引用内部会先分配足够的缓冲区再把文本拷贝进去。_ttoi 在这里做窄宽自适应Unicode 工程下它展开为 _wtoiANSI 工程下展开为 _atoi代码不需要分叉。如果下拉框里的文本不是纯数字而是像 ID:12 这种带前缀的格式直接用 _ttoi 会得到 0因为解析从头开始遇到非数字就停了。这种场景我一般先做一次 CString::Tokenize 或直接 Find 后 Mid 截取把 12 取出来再转换。转换前多确认一次数据形态比转换后对着 0 排查半天要快。3. MFC 数值与字符串互转CString::Format、_ttoi 与安全解析3.1 CString::Format 的格式说明符与变参匹配CString::Format 在功能上等价于 sprintf 的宽字符版但它是成员函数不需要自己管目标缓冲区长度。用法上最大的坑在变参匹配Format 是可变参数函数编译器不会检查参数类型和格式说明符是否一致类型不匹配时轻则输出乱码严重时直接读越界。int nCount 128; double fFactor 0.03; CString strPath _T(C:\\data); CString strMsg; strMsg.Format(_T(count%d, factor%.4f, path%s), nCount, fFactor, (LPCTSTR)strPath);这里三个格式符分别对应 int、double、TCHAR 字符串指针。关键是第三个参数不能直接传 strPath 这个对象必须写成 (LPCTSTR)strPath。原因在于可变参数不经过 C 类型转换传进去的是 CString 对象本体而 %s 期望拿到的是指针两者在内存布局上完全不同。常用格式符在 Unicode 工程下的对应关系如下。格式说明符参数实际类型常见错误%sconst wchar_t* / const char*直接传 CString 对象%dint传了 long long 或 DWORD%uunsigned int传了负数或无符号混用%I64d__int64传了 int%.2fdouble传了 float默认提升后其实没问题%lswchar_t*在 ANSI 工程里用 %s 传宽字符3.2 _ttoi、_tcstoul、_ttof 的解析边界_ttoi 是字符串转整数的最短路径但它对异常输入的表现很粗糙空字符串返回 0全部是非数字字符返回 0溢出时返回 0 或 LONG_MAX且不提供任何可区分的手段。若解析的是人工输入建议升级到 _tcstol 这类带结束指针的版本CString strInput _T(0x1A); TCHAR* pEnd nullptr; long lValue _tcstol(strInput, pEnd, 16); if (pEnd strInput.GetString()) { // 一个有效字符都没解析出来输入格式不对 } if (pEnd ! nullptr *pEnd ! _T(\0)) { // 说明后面还有残留字符比如 12abc }_tcstol 的第三个参数 base 传 16 表示按十六进制解析传 0 时允许 0x 前缀、八进制前缀等 C 风格写法。pEnd 指向解析结束时停住的位置如果 pEnd 指向的位置不是字符串结尾说明输入里混入了无法解析的字符。浮点数用 _ttof但它内部调用的也是 strtod同样存在“无有效转换时返回 0.0”的边界关键场景下应该用 _tcstod 并检查结束指针。3.3 数值拼 SQL 或日志时的转换要点MFC 的老项目里经常能看到用 Format 拼 SQL 和日志的做法。拼数字时的问题不大拼字符串时才需要小心CString strSql; strSql.Format(_T(SELECT * FROM orders WHERE id%d AND status%s), nOrderId, (LPCTSTR)strStatus);这种写法能跑的前提是 strStatus 里没有单引号。一旦状态字符串来自外部输入闭合单引号会改变 SQL 语义。MFC 的 CDatabase/CRecordset 支持参数绑定正确的做法是先用 ? 占位再调用 AddParameter 绑定变量让数据库驱动处理转义。日志拼接相对宽松但也要先确认日志文件用什么编码打开Windows 记事本对 UTF-8 无 BOM 文件的识别不稳定CString 直接写文件时会输出 UTF-16统一转成 UTF-8 再写入会更省事。4. MFC 跨边界类型转换std::vector、BSTR 与 getaddrinfo 地址4.1 CArray 与 std::vector 互转CArray 是 MFC 的容器std::vector 是 STL 的容器一个工程里混用两者的场景基本出现在老代码向外传递数据的边界。互转前要先确认元素的拷贝语义CString 是引用计数的拷贝开销很小所以元素可以直接转。CArrayCString, CString arrMfc; arrMfc.Add(_T(alpha)); arrMfc.Add(_T(beta)); std::vectorCString vecStd; vecStd.assign(arrMfc.GetData(), arrMfc.GetData() arrMfc.GetSize());GetData 在 CArray 里返回底层连续缓冲区指针在 Debug 版下仍可用。反向转换更简单遍历 vector 逐个 Addstd::vectorCString vecStd { _T(x), _T(y) }; CArrayCString, CString arrMfc; for (const CString s : vecStd) { arrMfc.Add(s); }这里有个容易忽略的点CArray 的第二个模板参数是 ARG_TYPEAdd 时如果传的是 CString 对象模板参数用 CString 可以避免一次临时对象拷贝。如果数组元素是 POD 类型比如 int 或 DWORD用 assign 几乎是零成本如果是自定义结构体则要注意 CArray 的连续内存布局在结构体含有指针成员时跨模块复制不会自动深拷贝。4.2 CString 与 BSTR/CComBSTR 互转BSTR 是 COM 里的字符串类型特点是字符串前面有一个 4 字节的长度前缀因此允许内部包含 \0。把 CString 转成 BSTR 的标准做法是 AllocSysStringCString strValue _T(COM 数据); BSTR bstr strValue.AllocSysString(); if (bstr ! nullptr) { // 把 bstr 交给 COM 接口 ::SysFreeString(bstr); }AllocSysString 使用 SysAllocString 分配内存这块内存只能由 SysFreeString 释放绝不能 delete 或交给 CoTaskMemFree。使用 CComBSTR 可以让释放自动化CComBSTR bstrTmp(strValue); BSTR bstrParam bstrTmp; // CComBSTR 提供隐式转换但只读CComBSTR 的析构会自行调用 SysFreeString所以 bstrParam 只能在 bstrTmp 生存期内使用。反向转换即 BSTR 转 CString直接构造即可BSTR bstrSource ::SysAllocString(Labc); CString strResult(bstrSource); ::SysFreeString(bstrSource);CString 的构造函数会把 BSTR 按宽字符拷进来BSTR 为 nullptr 时 CString 构造出来的也是空串不会崩。需要注意SysAllocString 的 L 前缀在 Unicode 工程下是 wchar_t 字面量在 ANSI 工程下不是跨工程移植时尽量用 CComBSTR 配合 _T 宏来避免字面量类型不一致。4.3 getaddrinfo 返回的地址结构与 CString 互转网络代码里最常见的类型转换是把 getaddrinfo 返回的 sockaddr 结构里的 IP 地址转换成可打印字符串。getaddrinfo 是 ANSI/Wide 都支持的 Winsock API地址解析结果与当前工程的字符集没有直接关系所以这里要操作的是 inet_ntop 这类独立于 TCHAR 的函数addrinfo hints { 0 }; hints.ai_family AF_UNSPEC; hints.ai_socktype SOCK_STREAM; addrinfo* pList nullptr; int rc ::getaddrinfo(strHost, strPort, hints, pList); if (rc ! 0) { return; // 解析失败可以用 gai_strerrorA 取错误描述 } char szAddr[INET6_ADDRSTRLEN] { 0 }; for (addrinfo* pCur pList; pCur ! nullptr; pCur pCur-ai_next) { void* pAddr nullptr; if (pCur-ai_family AF_INET) { pAddr ((sockaddr_in*)pCur-ai_addr)-sin_addr; } else if (pCur-ai_family AF_INET6) { pAddr ((sockaddr_in6*)pCur-ai_addr)-sin6_addr; } else { continue; } inet_ntop(pCur-ai_family, pAddr, szAddr, sizeof(szAddr)); CString strIp(szAddr); // 这里 strIp 就是可打印的 IP 字符串 } ::freeaddrinfo(pList);inet_ntop 返回的是 char*构造 CString 时走的还是 ANSI 转宽字符的路径。按 Windows 当前代码页转在中文系统上得到的 CString 能正常显示如果想把它作为 UTF-8 数据传输就需要用 2.3 节里的 CW2A 再转一次。另外getaddrinfo 的第一个参数 strHost 可以传域名也可以传 IP 字面量传 IP 时它一样会成功返回不需要手动区分。5. 用调试器和最小用例验证 MFC 类型转换结果5.1 先确认当前工程字符集拿到任何一个工程的代码第一件该做的验证是确认字符集。最直接的是一行输出#ifdef UNICODE ::OutputDebugStringW(LUnicode build); #else ::OutputDebugStringA(ANSI build); #endif用调试器输出到 Output 窗口而不是弹 MessageBox避免改到界面代码。如果要写成日志加一个函数把当前 _UNICODE 状态打出来后面所有转换问题的归因都会快很多。5.2 用 GetBuffer 观察缓冲区归属遇到 CString 内容莫名被改或崩溃时优先怀疑 GetBuffer 后忘调 ReleaseBufferCString str _T(temp); LPTSTR pBuf str.GetBuffer(64); _tcscpy_s(pBuf, 64, _T(new data)); // 此时不能读 str内部状态未更新 str.ReleaseBuffer();验证方法很简单在 ReleaseBuffer 前后分别看 str.GetLength()。前者返回旧的逻辑长度后者返回新内容长度。DLL 边界上传 CString 时直接传引用比传指针更稳但接收方必须使用同一个 MFC 运行时这一点用断点在构造处看 _CrtIsValidHeapPointer 就能暴露。5.3 最小转换用例把转换写成不依赖对话框的小函数在程序启动时跑一遍BOOL SelfTestConversion() { CString strNum _T(127); if (_ttoi(strNum) ! 127) return FALSE; CString strDup strNum; if (strDup ! _T(127)) return FALSE; std::string utf8 CW2A(strDup.GetString(), CP_UTF8).m_sz; CString strBack CA2W(utf8.c_str(), CP_UTF8).m_sz; return strBack strDup; }这个用例覆盖解析、赋值、UTF-8 往返三条路径。往返转换后 CString 内容一致说明编码转换没有丢数据不一致时基本可以确定代码页参数写错了。把所有转换函数都按这个模式补上自测比在对话框里点按钮排查要快得多。本文还有配套的精品资源点击获取
返回列表