简介:面向仍在使用 Visual C++ 6.0 的开发者,这份资源聚焦于在 VC6.0 工程中集成 JSONCPP 并解决中文字符解析乱码的完整案例。资源包共 72 个文件,约 3.77MB,以 h 头文件、cpp 源文件、inl 内联实现为主,辅以 obj、sbr 等编译中间文件、工程配置(dsp、dsw、opt)以及说明文档,源码与工程文件齐备,可直接打开对照调试。内容围绕 jsoncpp-src-0.5.0 源码的引入方式展开,涵盖 json_value、json_reader、json_writer 等核心模块的调用,并针对非 ASCII 字符给出 UTF-8 编码处理与读写配置思路,配套使用说明文档记录了测试通过的步骤与常见陷阱。已有 590 人学习,适合需要维护老旧 VC6.0 项目、又要在其中完成 JSON 数据读写的开发者参考,可据此快速搭建可运行的解析与序列化示例,减少集成与编码排错上的反复试错。
1. 老 VC6 工程要接 JSON:为什么我最后选了 JsonCpp 而不是手写解析
前阵子接手一个维护了十几年的老工控上位机,编译器还是 VC6.0,界面用 MFC 画的,通信层全是自定义的字节协议。甲方突然要求对接一套新的数据接口,报文格式是 JSON,字段里还夹着中文设备名。我第一反应是找个轻量库塞进去,结果试了两个现代 C++ 的 JSON 库,全卡在编译器上——VC6 对 C++ 标准的支持停留在上个世纪,模板、命名空间、STL 都有残缺,很多库连头文件都过不去。最后翻出来的是 JsonCpp,一个用纯 C++ 写的 JSON 解析与序列化库,源码结构简单、依赖少,稍作裁剪就能在 VC6 下编译通过,中文乱码问题也能在字符串编码这一层解决掉。这份全案例资源就是围绕「VC6.0 + JsonCpp + 中文防乱码」这条链路整理的,包含可直接编译的工程、封装好的读写接口和几个典型报文的解析示例。适合两类人:一是手里还压着 VC6 老项目、不得不做 JSON 对接的维护工程师;二是想搞清楚 JsonCpp 内部 Value、Reader、Writer 三层结构怎么用的人。下面我按「先跑通、再拆结构、最后填坑」的顺序讲,能抄的地方直接抄。
2. 把 JsonCpp 塞进 VC6 工程:源码裁剪与编译配置
2.1 为什么不能直接拿最新版源码丢进去
JsonCpp 的官方仓库一直在更新,新版大量使用了std::string的现代用法、auto、移动语义这些 VC6 根本不认的语法,直接编译会报出一屏错误。我的做法是找一份较早期的稳定版本源码(资源包里已经带了一份裁剪过的),只保留json_reader.cpp、json_value.cpp、json_writer.cpp三个核心实现文件和对应的头文件,把json_config.h里的编译开关按 VC6 的能力改掉。常见做法是把JSON_USE_EXCEPTION关掉,改用返回码判断错误,因为 VC6 的异常处理在 MFC 混合编译时容易出玄学问题;同时把JSON_HAS_INT64打开,保证大整数不丢精度。
裁剪时最容易翻车的是头文件包含顺序。VC6 的<string>实现和 JsonCpp 里对std::string的 typedef 有冲突,需要在json_config.h里显式指定JSONCPP_STRING的类型。我一般会在工程设置里把 JsonCpp 的源码目录加到 Include 路径,然后在stdafx.h之后、业务头文件之前引入json.h,顺序错了会报一堆重定义。
2.2 工程配置的四个关键开关
VC6 的工程设置(Project → Settings)里有几处必须改,否则编译能过、运行会崩:
| 配置项 | 位置 | 建议值 | 原因 |
|---|---|---|---|
| 运行时库 | C/C++ → Code Generation | 与主工程一致(多为 Multithreaded DLL) | 不一致会导致 new/delete 跨模块崩溃 |
| 预处理器定义 | C/C++ → Preprocessor | 加JSON_USE_EXCEPTION=0 | 关闭异常,走返回码 |
| 结构成员对齐 | C/C++ → Code Generation | 8 字节(默认) | 与 JsonCpp 内部布局匹配 |
| 警告等级 | C/C++ → General | Level 3 | Level 4 会刷出大量无害警告淹没真错误 |
配置完成后先单独编译三个 cpp 文件,确认生成 obj 无错误,再挂到主工程里。这一步别偷懒,我见过有人直接把源码拖进工程一起编,结果链接阶段报unresolved external symbol,查了半天是某个 cpp 没加进工程。
2.3 最小可运行示例:解析一段带中文的报文
先跑通一个最小例子,确认库能用。下面这段代码放在一个 MFC 按钮响应里,解析一段包含中文设备名的 JSON:
#include "json/json.h" #include <string> void CTestDlg::OnParseJson() { // 待解析报文,注意源文件需保存为 UTF-8 无 BOM,或在此处做编码转换 const char* szJson = "{\"device\":\"温度传感器A\",\"value\":23.5,\"online\":true}"; Json::Reader reader; Json::Value root; // 关闭异常模式下,parse 返回 bool,错误信息在 root 中不保留,需用 reader.getFormattedErrorMessages() if (!reader.parse(szJson, root)) { CString strErr; strErr.Format("解析失败: %s", reader.getFormattedErrorMessages().c_str()); AfxMessageBox(strErr); return; } // 取值时先判断类型,避免类型不符导致断言 if (root["device"].isString()) { std::string dev = root["device"].asString(); // 此处 dev 为 UTF-8 字节流,显示到 MFC 控件前需转成 GBK CString strDev = Utf8ToGbk(dev); m_editDevice.SetWindowText(strDev); } double val = root["value"].asDouble(); bool online = root["online"].asBool(); }逻辑说明:Json::Reader::parse是入口,第二个参数是输出根节点。关闭异常后它返回布尔值,失败时用getFormattedErrorMessages()拿错误位置,比异常信息更直观。取值前用isString()、isDouble()做类型判断是血泪经验——JsonCpp 在类型不符时默认返回零值或空串,不判断的话线上会出现「字段明明有值却读成 0」的诡异现象。asString()返回的是std::string,里面是原始字节,中文能否正确显示取决于源报文编码和后续转换,这一点在第 4 章专门讲。
参数说明:szJson必须是合法的 UTF-8 或纯 ASCII 字节流;如果报文来自网络,先确认对端编码。Utf8ToGbk是自定义转换函数,VC6 下用MultiByteToWideChar加WideCharToMultiByte两步走,不要用CString的隐式转换,那个在中文环境下会丢字节。
3. JsonCpp 的三层结构:Value、Reader、Writer 怎么配合
3.1 Value 是数据容器,不是字符串
很多人第一次用 JsonCpp 会把Json::Value当成一个万能字符串,直接asString()取所有字段,结果数字字段取出来是空。Json::Value内部是一个带类型标记的联合体,支持 null、int、uint、double、string、bool、array、object 七种类型。解析后的树形结构里,每个节点都保留自己的类型,取值时必须用对应的asXxx()。数组用size()加下标遍历,对象用getMemberNames()拿键名再逐个取。
一个实用技巧:遍历未知结构的对象时,先isObject()判断,再getMemberNames(),对每个键用value[name].type()分支处理。这样即使对端加了新字段,老代码也不会崩。资源包里的dumpValue示例就是干这个的,递归打印整棵树,调试接口时非常好用。
3.2 Reader 的解析选项与错误定位
Json::Reader构造时可以传一个Features结构体,控制是否允许注释、是否允许尾逗号、是否严格模式。对接第三方接口时我一般把allowComments打开,因为有些对端会在报文里塞//注释,严格模式直接报错。错误定位靠getFormattedErrorMessages(),它返回的字符串里带行号和列号,比单纯一个 false 有用得多。
注意一个坑:Reader对象不要复用去解析多段报文后还依赖它的错误信息,内部状态会累积。我的习惯是每次解析新建一个Reader,代价很小,省得排查时被残留错误信息误导。
3.3 Writer 输出中文与格式化控制
序列化用Json::FastWriter或Json::StyledWriter。前者输出紧凑单行,适合网络传输;后者带缩进,适合写日志。两者默认都会对非 ASCII 字符做转义,输出成\uXXXX形式,这在纯 ASCII 通道里没问题,但如果对端要求原样中文,就得关掉转义。老版本 JsonCpp 没有直接开关,常见做法是继承Writer重写writeValue,或者输出后自己做一次\u还原。资源包里给了一个ChineseFastWriter的封装,思路就是把std::string里的\u序列按 UTF-8 还原回去,代码不长,但省得每次手写。
写数组时注意append和resize的区别:append动态增长,resize预分配后要用下标赋值,后者在已知元素个数时效率更高。构造嵌套对象时,root["a"]["b"] = 1这种链式写法在 JsonCpp 里是合法的,会自动创建中间节点,但前提是root本身是 object 类型,如果root是 null,链式赋值会静默失败,这点要留意。
4. 中文乱码的根因与三种解法
4.1 乱码不是 JsonCpp 的锅,是编码链路断了
中文乱码的本质是「字节流编码」和「显示端编码」不一致。JsonCpp 内部对字符串不做任何编码转换,asString()原样返回报文里的字节。如果报文是 UTF-8,而 MFC 控件按 GBK 解释,就会出乱码;反过来,如果源文件是 GBK 保存的,字符串字面量进到 JsonCpp 里再输出,对端按 UTF-8 解析也会乱。所以第一步永远是确认三处编码:源文件保存编码、网络报文编码、界面显示编码。资源包里的示例统一约定「内部一律 UTF-8,只在界面边界转 GBK」,这个约定能覆盖绝大多数老工控场景。
4.2 方案一:边界转换,内部保持 UTF-8
这是我最推荐的方案。所有 JsonCpp 的输入输出都用 UTF-8 字节流,只在写入 MFC 控件、写配置文件、弹消息框这几个边界做转换。转换函数用 Win32 API 两步走:
// UTF-8 转 GBK,VC6 下可用 CString Utf8ToGbk(const std::string& utf8) { // 第一步:UTF-8 -> UTF-16 int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0); if (wlen <= 0) return CString(""); wchar_t* wbuf = new wchar_t[wlen]; MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wbuf, wlen); // 第二步:UTF-16 -> GBK(代码页 936) int glen = WideCharToMultiByte(CP_ACP, 0, wbuf, -1, NULL, 0, NULL, NULL); char* gbuf = new char[glen]; WideCharToMultiByte(CP_ACP, 0, wbuf, -1, gbuf, glen, NULL, NULL); CString str(gbuf); delete[] wbuf; delete[] gbuf; return str; }逻辑说明:CP_UTF8到CP_ACP(当前系统 ANSI 代码页,中文系统即 936)必须经过宽字符中转,不能一步到位。两次调用都是「先问长度、再分配、再转换」的模式,这是 Win32 编码转换的标准写法。参数-1表示输入以 null 结尾,输出也会带 null 终止符。注意new出来的缓冲区必须配对delete[],VC6 没有智能指针,漏了就是内存泄漏。
4.3 方案二:源文件统一存 UTF-8 无 BOM
如果工程里硬编码的 JSON 字符串比较多,最省事的办法是把源文件另存为 UTF-8 无 BOM,然后在工程设置里加/utf-8编译选项——但 VC6 不支持这个选项,所以这条路在 VC6 下走不通。退而求其次的做法是把所有中文字面量抽到一个资源文件或单独的.h里,用\x转义写成 UTF-8 字节序列,虽然可读性差,但能保证编译进去的字节就是 UTF-8。资源包里的zh_strings.h就是这么干的,维护起来麻烦,但一劳永逸。
4.4 方案三:解析前统一转码
如果对端报文编码不可控(有的设备发 GBK,有的发 UTF-8),就在parse之前做一次探测和转换。探测可以用简单的启发式:统计高位字节的分布,GBK 的双字节首字节范围是 0x81-0xFE,UTF-8 的多字节首字节是 0xC0-0xFD 且后续字节是 0x80-0xBF,两者有重叠,纯启发式不可靠。更稳的做法是让对端在协议头里带编码标识,或者约定固定编码。我一般要求接口文档写死 UTF-8,实在改不了就在配置里加一个「报文编码」选项,人工指定,别指望自动探测,那个翻车率太高。
5. 避坑与排查:五个真实踩过的坑
5.1 解析成功但取值为空
现象:parse返回 true,root["name"].asString()却是空串。原因:字段名大小写或空格不一致,或者该字段实际是 null 类型。解决:先用root.isMember("name")判断存在性,再判断type(),调试时用dumpValue打印整棵树对照。我现在的习惯是每个接口解析完先打一行关键字段日志,出问题直接看日志,比断点快。
5.2 中文在日志里正常、在界面里乱码
现象:写文件的中文没问题,SetWindowText出来是问号或方块。原因:日志文件按字节写,界面控件按系统代码页解释,两者编码不一致。解决:界面边界统一走Utf8ToGbk,不要依赖CString的隐式构造。另外检查对话框资源是否用了 Unicode 字体,VC6 默认 ANSI 控件对某些 GBK 扩展字符支持不好。
5.3 大整数精度丢失
现象:报文里{"id": 1234567890123},取出来变成1234567890122或负数。原因:没开JSON_HAS_INT64,JsonCpp 把大整数当 double 存,超过 2^53 就丢精度。解决:在json_config.h里定义JSON_HAS_INT64,取值用asInt64()或asUInt64(),不要用asInt()。VC6 对__int64的支持是有的,放心用。
5.4 多线程下偶发崩溃
现象:主线程解析正常,工作线程解析偶发访问违例。原因:JsonCpp 的Value引用计数不是线程安全的,跨线程共享同一个Value树会出问题。解决:每个线程独立解析、独立持有Value,需要传递时序列化成字符串再传。资源包里的线程示例就是这么做的,虽然多一次序列化开销,但稳定。
5.5 链接报错 unresolved external symbol
现象:编译通过,链接报Json::Value::~Value未解析。原因:某个 cpp 没加进工程,或者运行时库配置不一致导致符号修饰不同。解决:确认三个核心 cpp 都在工程文件列表里,检查 C/C++ 的运行时库设置与主工程完全一致,清理后重新编译全部。
6. 进阶:把解析封装成可复用的取值层
老项目里最怕的是每个接口都手写一遍isMember加type判断,代码又长又容易漏。我的做法是封一层薄薄的取值助手,把「缺字段返回默认值、类型不符返回默认值、中文自动转码」三件事收口。核心是一个模板函数加几个特化:
// 通用取值:不存在或类型不符时返回默认值 template<typename T> T GetJsonValue(const Json::Value& node, const char* key, T defVal); template<> std::string GetJsonValue<std::string>(const Json::Value& node, const char* key, std::string defVal) { if (!node.isObject() || !node.isMember(key)) return defVal; const Json::Value& v = node[key]; if (!v.isString()) return defVal; return v.asString(); // 返回 UTF-8,调用方按需转码 } template<> double GetJsonValue<double>(const Json::Value& node, const char* key, double defVal) { if (!node.isObject() || !node.isMember(key)) return defVal; const Json::Value& v = node[key]; if (!v.isNumeric()) return defVal; return v.asDouble(); }逻辑说明:模板主声明只声明不定义,靠特化提供具体实现,这样调用方写GetJsonValue<std::string>(root, "device", "")时编译器会选到对应特化。isNumeric()同时覆盖 int 和 double,比单独判断isInt()更宽容。返回的std::string保持 UTF-8,转码责任留给界面层,这样这层封装不依赖 MFC,可以单独测试。
参数说明:node传父对象,key传字段名,defVal是缺省值。注意node必须是 object 类型,传数组进来会直接返回默认值,这是有意为之——数组取值应该用下标,不该走这个接口。
封装完之后,一个接口的解析代码从几十行缩到五六行,而且缺字段、类型错、中文乱码三类问题在源头就被挡住了。验证方法很简单:构造几段畸形报文(缺字段、类型错、中文、大整数、嵌套数组),跑一遍看是否都返回预期默认值且不崩。我一般会把这组畸形报文固化成一个小测试函数,每次改完解析层跑一遍。
从那以后我每次接老工程的 JSON 对接,都强制先把「编码约定、取值封装、畸形报文测试」这三件事走一遍再写业务代码,返工率明显下来了。希望帮到你。
本文还有配套的精品资源,点击获取