
简介面向仍在使用 Visual C 6.0 维护旧项目的开发者与学习者这套案例聚焦 jsoncpp 0.5.0 源码在 VC6 中的集成、JSON 解析与生成以及中文字符防乱码处理适合课程设计、老系统改造和接口联调等场景。压缩包共 72 个文件约 3.77MB以 24 个头文件、9 个 cpp 源文件和 6 个 inl 内联实现为主体并配有工程配置、资源文件、编译中间文件、说明文档与可运行示例便于直接对照搭建控制台或对话框程序。已有 591 人学习说明其在老旧环境 JSON 开发场景中具有一定参考价值。资源将源码引入、编码设置、解析与序列化验证、编译运行排错串成完整闭环并附带测试通过的源码类使用说明可帮助读者理解 VC6 下 jsoncpp 的目录组织与依赖关系对字段类型、中文写入文件与界面显示等常见问题提供可操作参考少走配置和中文乱码弯路形成可复用的旧项目 JSON 处理方案。1. 需求先摆出来为什么还要在 VC6.0 里调 jsoncpp接手一个老工控上位机项目时真正卡住进度的往往不是算法而是那台只能装 VC6.0 的编译机。换 IDE 意味着重装系统、重配第三方控件、把现场几十台设备重新联调一遍没人愿意批这个工期。可对接方早就换成 HTTP 接口了返回的是 JSON键名是中文值里是「一号冷却泵」这种设备名。于是问题变得很具体在只认 C98 的编译器上把 jsoncpp 编进去还要保证中文从界面写进文件、再从文件读回界面一个字节都不差。这套方案讲清三件事jsoncpp 在 VC6.0 下怎么选版本、怎么配工程GBK 与 UTF-8 的转换怎么分层才能不把乱码问题扩散到每个模块以及最后怎么验收「中文不乱码」这条需求。适合正在做课程设计、毕业设计里带配置读写或设备对接模块的人也适合接手老项目、需要在不动工具链的前提下补上 JSON 解析能力的开发者。2. 工程骨架jsoncpp 版本选择与 VC6.0 编译配置老工程加第三方库翻车往往不在代码逻辑而在工程配置。VC6.0 的编译器、链接器、运行库三者之间有一堆隐含约定jsoncpp 又是个用了 STL 容器和模板的库两边的默认值对不上就会在链接阶段炸出一堆看不懂的符号。这一章先把地基打平后面的代码才有意义。2.1 版本卡位为什么锁在 0.6 这条线1.x 系列的 jsoncpp 源码里能看到std::unique_ptr、override、nullptr、范围 for 循环这些特性在 VC6.0 上连语法分析都过不去改源码的工作量比重写一个大。0.6.0-rc2 这条线是纯 C98 写法Json::Reader/Json::StyledWriter/Json::FastWriter这套接口已经够覆盖配置读写、接口对接的绝大多数场景社区里在 VC6.0 上跑通的案例也集中在这个版本。挑版本时看三个点能省掉一轮试错源码里有没有nullptr和auto有就直接放弃有没有std::unique_ptr/std::make_shared有就同上成员函数模板有没有偏特化写法VC6.0 对成员模板的支持是残缺的templatetypename T T get(const char*, T)这种签名在 VC6.0 上经常报「无法推导模板参数」。还有一个容易忽略的点头文件和静态库必须来自同一份源码。Json::Value的内存布局在不同版本之间变过头文件用 A 版本、lib 用 B 版本编译期一切正常运行到Value的析构函数就崩而且崩的位置和真正的错因隔着好几层调用栈排查起来是纯血泪。2.2 目录结构与静态库工程我一般把 jsoncpp 编成静态库而不是把三个 cpp 直接塞进主工程。原因是主工程的编译选项五花八门jsoncpp 需要一个干净的编译环境单独一个 lib 工程改配置的时候心里有数。文件组织如下路径内容说明3rd/jsoncpp/include/json/json.h、reader.h、value.h、writer.h、config.h、forwards.h、autolink.h对外头文件主工程只包含这一层3rd/jsoncpp/src/json_reader.cpp、json_value.cpp、json_writer.cpp三个编译单元缺一不可3rd/jsoncpp/lib/jsoncpp.lib静态库产物src/业务代码、CnvString.h/.cpp、JsonHelper.h/.cpp主工程建库步骤不复杂但每一步都有坑新建 Win32 Static Library 工程不要勾「预编译头」jsoncpp 源码里没有stdafx.h勾了反而要额外配置把三个 cpp 加进工程头文件搜索路径指向include/编译选项里运行库选/MT静态链接 CRT或/MD动态链接 CRT这个选择必须和主工程完全一致后面会单独讲为什么打开/GX启用异常处理还是关掉取决于config.h里的JSON_USE_EXCEPTION两者必须配套输出目录指到lib/Debug 和 Release 各出一份文件名带d后缀区分。主工程这边只需要三步附加包含目录指向include/附加库目录指向lib/链接器输入里加jsoncpp.libDebug 配置加jsoncppd.lib。听起来很基础但我见过太多次 Debug 配置链了 Release 的库表现是单步调试时变量值莫名其妙查一整天。2.3 编译开关与三处补丁config.h是 jsoncpp 留给使用方的配置入口VC6.0 下需要改的地方集中在这里// json/config.h 针对 VC6.0 的裁剪改完记得重新编 lib #ifndef JSON_CONFIG_H_INCLUDED #define JSON_CONFIG_H_INCLUDED // 1. 关掉异常。VC6 的 /GX 与老版 STL 混用时异常穿过 STL 内部帧容易出问题 // 而且工控类项目普遍不允许异常逃逸到消息循环改成返回错误码更好控。 #define JSON_USE_EXCEPTION 0 // 2. VC6 编译 STL 必关的两个警告不关的话输出窗口几千行噪音真正的错误反而被埋掉 #if defined(_MSC_VER) _MSC_VER 1300 #pragma warning(disable:4786) // 调试信息里标识符被截断到 255 字符 #pragma warning(disable:4251) // 非 dll 接口的 STL 成员导出警告 #endif #endif // JSON_CONFIG_H_INCLUDED第一处是异常开关。设为 0 之后jsoncpp 内部遇到断言失败会走JSON_FAIL_MESSAGE宏默认行为是打印并 abort。做客户端工具的话我一般把这个宏重定义成写日志加返回避免用户点一下就把程序关掉。注意改完config.h必须重编 lib只改头文件不重编行为不会变这种「改了没效果」的错觉最消耗时间。第二处是windows.h的min/max宏。这两个宏是无参数宏定义会把std::min(a, b)直接展开成((a) (b) ? (a) : (b))std::min里的::和一拼接编译器就报error C2039: min : is not a member of std。两种改法选一种就行// stdafx.h 最顶部必须在任何 windows 头之前 #define NOMINMAX // 让 windef.h 不定义 min/max 宏 #include windows.h #undef min // 兜底某些第三方头仍会带进来 #undef max另一种是在 jsoncpp 源码里把调用点写成(std::min)(a, b)加一对括号阻断宏展开。前一种改动小推荐但如果工程里已经有大量代码依赖min/max宏就选后一种免得牵一发动全身。第三处是预编译头。jsoncpp 源码不认stdafx.h把它加进主工程编译时第一行就报「无法打开预编译头文件」。要么给这三个 cpp 单独关掉「使用预编译头」要么就按 2.2 的做法单独建库后者更干净。3. 编码链路设计GBK 与 UTF-8 在三层之间的转换方案中文乱码之所以烦是因为它不出错、只出错字。程序照常跑日志照常打只是本该显示「冷却泵」的地方显示成「鍐峰嵈娉点」。等发现的时候脏数据已经写进现场设备的配置文件了。所以这一层不能靠「试出来」得先把链路画清楚。3.1 乱码到底发生在哪一步在 VC6.0 里一段中文要经过三个编码环境环节典型编码说明源文件与界面控件GBK代码页 936即CP_ACPVC6 默认按系统 ANSI 保存源文件MFC 的CString、GetWindowText拿到的都是 GBK 字节Json::Value内存中的字符串建议统一 UTF-8jsoncpp 内部只把std::string当字节流不做任何转换落盘的 JSON 文件UTF-8无 BOMJSON 规范要求文本交换用 UTF-8带 BOM 反而会让解析器报错关键结论只有一句jsoncpp 不做编码转换你写进去什么字节它就原样吐出来。所以「jsoncpp 解析中文乱码」这个说法本身不准确乱码发生在你把 GBK 字节当 UTF-8 塞进去的那一刻或者把 UTF-8 字节直接丢给只认 GBK 的控件的那一刻。顺带说一个常被问到的现象中文值落盘后有时是原样的 UTF-8 字节有时是一串\u8bbe\u5907这样的转义。两种写法解析回来是同一个字符串差异来自 writer 的转义策略不同版本行为不完全一样。工程上不必纠结验收时确认「写出去能被自己读回来、能被对方系统解析」就够了。由此定下一条工程约定全项目遵守内存中的Json::Value统一存 UTF-8进出口各转一次。界面进来的 GBK 转成 UTF-8 再赋值取出来显示之前再转回 GBK。约定一旦破了比如某个模块图省事直接塞 GBK那这个字段就会成为整份数据里唯一的乱码点而且只在特定设备上复现。3.2 转换层封装CnvString 的两个方向转换本身是两个 Win32 API 的事但没有封装的话每个业务文件里都会出现四五行的临时缓冲区和长度计算改起来是灾难。封装成一个只做转换、不带状态的工具类放在最底层谁都依赖它、它不依赖谁。// CnvString.h VC6.0 下的 GBK / UTF-8 互转只做字节层转换不做业务判断 #ifndef __CNV_STRING_H__ #define __CNV_STRING_H__ #include windows.h #include string #include vector class CnvString { public: // GBK系统 ANSI 代码页转 UTF-8 static std::string GbkToUtf8(const std::string strGbk); // UTF-8 转 GBK static std::string Utf8ToGbk(const std::string strUtf8); private: // 多字节转宽字符uCodePage 传 CP_ACP 或 CP_UTF8 static std::vectorwchar_t MultiToWide(const std::string str, UINT uCodePage); // 宽字符转多字节 static std::string WideToMulti(const std::vectorwchar_t vecWide, UINT uCodePage); }; #endif // __CNV_STRING_H__实现里用std::vectorwchar_t而不是std::wstring这是 VC6.0 上的经验老版 STL 的basic_string在resize和c_str()上有几处行为不一致用来做 API 出参容易踩到未终止字符串的坑。vector的vec[0]语义清晰配一个显式的L\0结尾最稳。// CnvString.cpp #include CnvString.h std::vectorwchar_t CnvString::MultiToWide(const std::string str, UINT uCodePage) { std::vectorwchar_t vecWide; if (str.empty()) return vecWide; // 第四个参数传 str.size() 而不是 -1字符串中间可能含 \0-1 会在这里截断 int nLen ::MultiByteToWideChar(uCodePage, 0, str.c_str(), (int)str.size(), NULL, 0); if (nLen 0) return vecWide; // 输入含非法字节返回空让上层判断 vecWide.resize(nLen 1, L\0); // 多留一位放结尾的 L\0 ::MultiByteToWideChar(uCodePage, 0, str.c_str(), (int)str.size(), vecWide[0], nLen); vecWide[nLen] L\0; return vecWide; } std::string CnvString::WideToMulti(const std::vectorwchar_t vecWide, UINT uCodePage) { std::string strOut; if (vecWide.empty()) return strOut; // 这里同样按实际长度传避免把结尾的 L\0 也写进结果 int nLen ::WideCharToMultiByte(uCodePage, 0, vecWide[0], (int)(vecWide.size() - 1), NULL, 0, NULL, NULL); if (nLen 0) return strOut; strOut.resize(nLen); ::WideCharToMultiByte(uCodePage, 0, vecWide[0], (int)(vecWide.size() - 1), strOut[0], nLen, NULL, NULL); return strOut; } std::string CnvString::GbkToUtf8(const std::string strGbk) { return WideToMulti(MultiToWide(strGbk, CP_ACP), CP_UTF8); } std::string CnvString::Utf8ToGbk(const std::string strUtf8) { return WideToMulti(MultiToWide(strUtf8, CP_UTF8), CP_ACP); }这段代码有几个设计取舍值得说明。第一CP_ACP在简体中文系统上等于 936也就是 GBK但它是「当前系统代码页」的意思不是硬编码的 936。写CP_ACP比写936更正确但也意味着程序在日文系统上行为会变做现场部署工具时要在文档里写清楚运行环境。第二vecWide.size() - 1这个写法看着别扭但它表达的是「不含结尾 L\0 的有效长度」比再存一个长度变量省事。第三两个转换都用中间宽字符中转看似绕了一圈实际是 Windows 上唯一可靠的路径——GBK 和 UTF-8 之间没有官方的直接转换 API只能借 UTF-16 过一道。3.3 参数与边界不可映射字符、BOM、长度计算API 调通只是及格线真正体现经验的是边界处理。标志位一律传 0。网上不少示例会传MB_ERR_INVALID_CHARS或WC_ERR_INVALID_CHARS这两个标志在 XP 时代的系统上不被支持传了直接返回失败而且失败得毫无提示。为了兼容老设备上的系统版本传 0然后用「返回长度是否为 0」来判断失败简单可靠。不可映射字符会被静默替换。从 UTF-8 转 GBK 时如果源串里有 GBK 表达不了的字符——比如某些生僻字、带变体选择符的符号——WideCharToMultiByte不会报错而是把它替换成?。这是数据层的静默损坏比抛异常更危险。工程上的做法是转换后比较长度或做一次回环校验发现降级就记一条日志让使用者知道「这个字符存不进去」而不是等到三个月后发现设备名对不上。BOM 必须在解析前剥掉。对方系统导出的 JSON 文件十有八九带 UTF-8 BOM就是开头那三个字节EF BB BF。jsoncpp 看到第一个字节不是{或[直接报语法错误错误信息还只给你一个偏移量。处理方式很简单读文件后判断前三字节是就erase(0, 3)两行代码的事但不知道的话能查半天。长度一律用size()不用strlen。JSON 里的\u0000解析出来会变成真正的 0 字节嵌在字符串中间c_str()到那里就断了。std::string本身是二进制安全的只要所有长度计算都用size()这条路就通。4. 核心实现解析、构造、序列化与文件落盘的完整代码前面两章把地基和编码约定铺好了这一章是可以直接抄进工程的部分。代码按「进来—处理—出去」的顺序组织每一段都对应一个真实会遇到的场景而不是 API 罗列。4.1 解析入口与错误定位解析这一层要解决的不是「能不能解析」而是「解析失败时怎么让人看懂」。jsoncpp 的getFormattedErrorMessages()返回的是英文加行列偏移直接弹到界面上使用者完全不知道该怎么办。所以入口函数做成固定签名入参 GBK出参 GBK 错误信息内部全部转 UTF-8 处理。// JsonHelper.cpp 统一解析入口 #include JsonHelper.h #include CnvString.h bool ParseGbkJson(const std::string strGbkText, Json::Value root, std::string strErrGbk) { strErrGbk.erase(); // 1. 出口统一界面给的是 GBK进 jsoncpp 之前转成 UTF-8 std::string strUtf8 CnvString::GbkToUtf8(strGbkText); if (strUtf8.empty() !strGbkText.empty()) { strErrGbk 文本转码失败可能混入了非法字节请检查文件编码; return false; } // 2. collectComments 传 false注释不进 Value减少一次深拷贝 Json::Reader reader; if (!reader.parse(strUtf8, root, false)) { // 3. 错误信息也转回 GBK否则界面上是一串问号 strErrGbk CnvString::Utf8ToGbk(reader.getFormattedErrorMessages()); return false; } return true; }collectComments这个参数值得单独说一句。传true会把注释也挂到Value上代价是每个节点多一次内存分配解析大文件时开销明显。配置文件里带注释的场景不多传false更合适如果确实需要保留注释那就要接受它在序列化时会丢——jsoncpp 的 writer 不输出注释。还有一处容易忽略解析失败后root的状态是未定义的可能是上一次解析的残留。调用方要养成习惯失败分支直接返回不要再去读root。4.2 构造与序列化中文值的写入顺序序列化这边最容易犯的错是「先拼字符串再整体转码」。看着省事实际会把 JSON 的结构字符引号、冒号、转义符一起参与转码遇到特殊字符就破坏了格式。正确顺序是先把每个中文字段单独转成 UTF-8交给Json::Value组装最后整体输出时再决定是否转成 GBK。// 构造一份带中文的设备配置返回值是 GBK方便直接写界面或日志 std::string BuildDeviceJsonGbk() { Json::Value root(Json::objectValue); // 显式声明对象类型避免后续 operator[] 断言 // 键名用英文跨系统交换时最稳不会因为对方不认 UTF-8 键而整份失败 root[deviceId] Json::Value(CnvString::GbkToUtf8(DEV-001)); root[deviceName] Json::Value(CnvString::GbkToUtf8(一号冷却泵)); root[temperature] Json::Value(36.5); Json::Value arrSample(Json::arrayValue); for (int i 0; i 3; i) { Json::Value item(Json::objectValue); item[index] Json::Value(i); item[remark] Json::Value(CnvString::GbkToUtf8(采样点)); arrSample.append(item); // append 是深拷贝item 出作用域不影响结果 } root[samples] arrSample; Json::StyledWriter writer; // 调试用带缩进正式落盘换 Json::FastWriter 省体积 std::string strUtf8 writer.write(root); // 只有最终交付给界面/GBK 日志时才转回去 return CnvString::Utf8ToGbk(strUtf8); }这里有个取舍要讲清楚键名用英文还是中文。中文键名写在 JSON 里是完全合法的解析也没问题问题出在「跨系统」这一环——对方系统如果把 JSON 当 GBK 读中文键名直接变成乱码而且还不好排查因为它看起来和值乱码一模一样。我一般建议键名走英文或拼音值里放开用中文这样即使某一段链路编码没对齐结构至少是完整可读的。另一个是StyledWriter和FastWriter的选择。前者带缩进和换行人眼友好体积大约是后者的两倍后者是紧凑格式适合落盘和传输。配置文件的场景我一般用StyledWriter出问题的时候直接打开文件就能看比多写一个日志函数划算。4.3 取值与遍历类型判断和默认值兜底取值这一层的核心原则是「不信任输入」。对方系统升级一次字段类型从数字变成字符串是常有的事asInt()遇到字符串会返回 0看起来没崩实际业务数据已经错了。所以每个取值都要先判类型再给默认值。// 从节点取字符串缺键、类型不对、空值三种情况都返回默认值 std::string GetStrOrDef(const Json::Value node, const char* pszKey, const std::string strDefGbk) { if (!node.isObject() || !node.isMember(pszKey)) return strDefGbk; // 用 get 而不是 operator[]const 对象上调用 operator[] 在老版本里会断言 Json::Value v node.get(pszKey, Json::Value()); if (!v.isString()) return strDefGbk; return CnvString::Utf8ToGbk(v.asString()); } // 遍历对象的所有键键名本身也要转码 void DumpKeys(const Json::Value node, std::vectorstd::string vecKeysGbk) { vecKeysGbk.clear(); if (!node.isObject()) return; Json::Value::Members names node.getMemberNames(); for (size_t i 0; i names.size(); i) vecKeysGbk.push_back(CnvString::Utf8ToGbk(names[i])); }Json::Value::Members在 0.6 里就是std::vectorstd::string遍历用下标比用迭代器安全因为 VC6 的 STL 在容器被修改后迭代器失效的检查很弱出问题是内存越界而不是断言。size_t做循环变量在 VC6 上会有signed/unsigned比较警告用强制转换或直接改int都行不影响正确性。数值这块要留意isInt()对1.0这种带小数点的写法返回false但asInt()能取到 1。如果业务上把温度、压力都当浮点就统一用isDouble()asDouble()别一半用整型一半用浮点边界值上迟早对不齐。4.4 文件读写二进制模式与 BOM 剥离文件这一层只有两个要点但两个都能单独造成「解析失败」一是必须用二进制模式二是必须剥 BOM。// 读整个文件到内存调用方负责后续转码 bool ReadAllBytes(const char* pszPath, std::string strOut) { strOut.erase(); // 必须 rb文本模式会把 0x1A 当文件结束符还会把 CRLF 悄悄转成 LF FILE* fp fopen(pszPath, rb); if (NULL fp) return false; char buf[4096]; size_t nRead 0; while ((nRead fread(buf, 1, sizeof(buf), fp)) 0) strOut.append(buf, nRead); // append 带长度避免中间有 \0 被截断 fclose(fp); return true; } // 剥掉 UTF-8 BOM必须在 ParseGbkJson 之前调用 void StripUtf8Bom(std::string strText) { if (strText.size() 3 (unsigned char)strText[0] 0xEF (unsigned char)strText[1] 0xBB (unsigned char)strText[2] 0xBF) { strText.erase(0, 3); } }0x1A这个细节是血泪教训。有些设备导出的配置文件末尾带一个0x1A文本模式读到那里就当 EOF 了后面的内容全部丢失表现出来是「JSON 后半段解析失败」而行号指向的位置完全正常。改成rb之后一切正常但如果不改你会以为是 jsoncpp 的 bug。写文件同理用wb并且落盘前确认内容是 UTF-8。如果业务要求文件必须能被记事本直接打开看中文那就写 UTF-8 加 BOM但要在文档里注明「本工具输出的文件带 BOM」免得对方系统解析失败反过来找你。5. 避坑清单VC6.0 调 jsoncpp 最容易翻车的六个点这一章把前面零散提到的坑集中起来按「现象 → 原因 → 解决」的形式列清楚。这几条基本覆盖了我在老工程里见过的绝大多数问题按这个顺序排查八成能定位到根因。5.1 编译与链接阶段现象一编译报error C2039: min : is not a member of std错误指向 jsoncpp 源码。原因是windows.h里的min/max是无参数宏展开时把std::min(a, b)里的std::和(拆开了编译器看到的是一个不存在的标识符。这类错误的迷惑性在于它指向的根本不是你写的代码第一次遇到会以为是库版本不对。解决办法两种二选一在stdafx.h最顶部定义NOMINMAX再包含windows.h然后#undef min/#undef max兜底或者把 jsoncpp 里所有std::min/std::max改成(std::min)(a, b)这种加括号的写法。前者改动小后者不影响其他依赖宏的老代码看工程现状选。现象二链接报LNK2005: _malloc 已经在 LIBCMTD.lib 中定义或者程序启动时在 CRT 初始化阶段直接崩。原因是 jsoncpp 的 lib 和主工程用了不同的运行库。一个用/MT静态 CRT一个用/MD动态 CRT两套 CRT 各有各的堆跨库分配的內存在另一套堆里释放表现就是随机崩溃有时候跑一整天都不复现。解决办法是把两个工程的「代码生成 → 运行时库」改成完全一致Debug 配 Debug、Release 配 Release并且在项目文档里写死这条约定。顺带说一句异常开关也要配套config.h里JSON_USE_EXCEPTION设成 0主工程就该关/GX设成 1 就开混着来会报error C4530或者运行时直接terminate。5.2 运行与内存阶段现象三调试版跑得好好的Release 版在取字段时弹断言提示Json::Value::operator[]的 key 不存在。原因是在const Json::Value上用了operator[]。老版本 jsoncpp 的 const 版本operator[]在键不存在时会断言失败而 Release 下断言被裁掉行为变成返回一个未定义的引用后面读它就越界。解决办法是统一走isMember()判断加get()取值封装成 4.3 里的GetStrOrDef业务代码不直接碰 本文还有配套的精品资源点击获取