简介:这份资源面向仍在使用 Visual C++ 6.0 的开发者,尤其是需要在老项目中处理 JSON 数据、又常被中文乱码困扰的 C++ 程序员。它基于 jsoncpp-src-0.5.0 源码,完整演示了在 VC6.0 中集成 JSONCPP、解析与生成 JSON 的全过程,并重点解决中文字符的编码与防乱码问题,属于可直接复用的实战案例。压缩包共 72 个文件,约 3.77MB,包含 24 个 h 头文件、9 个 cpp 源文件、6 个 inl 内联实现文件,以及工程文件、资源文件、说明文档和已编译的 exe 等,覆盖从源码到可运行工程的完整结构。资源内附详细使用说明文档,梳理了集成步骤、编码配置与常见陷阱,并给出对话框与控制台两种测试案例,方便读者对照验证中文解析效果。目前已有 590 人学习,适合希望快速跑通 VC6.0 下 JSONCPP 中文处理的开发者参考。
1. VC6.0 调 JSONCPP 这件事,为什么现在还有人踩坑
如果你手上还有一套跑在 VC6.0 上的老工程,突然要对接一个 HTTP 接口,返回的是 JSON,你大概率会经历这样一个过程:先找一个能编译的 JSON 库,发现大多数现代库早就把 VC6 支持砍了;再退一步想自己手写解析,遇到嵌套对象和中文转义直接翻车。VC6.0 调用 JSONCPP 全案例这件事,核心难点从来不是“怎么调”,而是“怎么让一个 1998 年的编译器和一套现代 C++ 库和平共处,并且中文不乱码”。
这篇文章面向的是还在维护 VC6.0 遗留系统的工程师,或者被要求“在老平台上加一个 JSON 解析功能”的开发者。我会把 JSONCPP 在 VC6.0 下的编译、集成、解析、中文处理和排错完整走一遍,参数怎么设、坑在哪、为什么这样选,都讲清楚。读完你应该能在一个干净的老工程里,把 JSON 解析跑通,并且中文不乱码。
2. 为什么 VC6.0 下选 JSONCPP:选型理由与版本取舍
2.1 VC6.0 的编译器边界决定了你能选什么
VC6.0 的 C++ 编译器对标准 C++ 的支持停留在非常早期的阶段。它没有完整的 STL 支持,模板偏特化、部分命名空间特性、std::string的某些行为都和现代编译器有差异。很多现代 JSON 库(比如 nlohmann/json、RapidJSON 的较新版本)直接依赖 C++11 甚至 C++17 特性,在 VC6.0 下根本编译不过。
JSONCPP 之所以还能被考虑,是因为它的早期版本(0.5.x、0.6.0 附近)对 C++ 标准要求相对低,代码里大量使用了自定义的std::string封装和Json::Value容器,没有依赖太多现代特性。但即便如此,直接拿最新版 JSONCPP 丢进 VC6.0,依然会报一堆错。常见做法是选一个较老的 JSONCPP 版本,或者对源码做少量适配。
提示:不要试图用最新版 JSONCPP 硬编 VC6.0,版本越新,对 C++ 标准要求越高,适配成本呈指数上升。
2.2 JSONCPP 的核心结构:三个类就够用了
JSONCPP 的使用面其实很窄,核心就三个东西:
Json::Value:表示一个 JSON 值,可以是对象、数组、字符串、数字、布尔或 null。Json::Reader:把 JSON 文本解析成Json::Value。Json::Writer(常用Json::FastWriter或Json::StyledWriter):把Json::Value序列化成文本。
在 VC6.0 下,你不需要用到 JSONCPP 的全部功能,把这三个类用熟,就能覆盖 90% 的接口对接场景。选型时优先确认你拿到的 JSONCPP 源码里有没有json_reader.cpp、json_value.cpp、json_writer.cpp这三个文件,它们是核心实现。
2.3 把 JSONCPP 源码加入 VC6.0 工程的正确姿势
VC6.0 没有现代 IDE 的“添加现有项”那么顺手,但步骤并不复杂。我一般会这样做:
第一步,在工程目录下建一个jsoncpp文件夹,把 JSONCPP 的include/json/下的头文件和src/lib_json/下的.cpp文件放进去。注意,VC6.0 对文件路径长度和中文路径比较敏感,路径里不要有中文和空格。
第二步,在 VC6.0 的Project -> Add To Project -> Files里,把json_reader.cpp、json_value.cpp、json_writer.cpp这三个文件加进工程。不要加json_internalarray.inl之类的内联文件,它们会被自动包含。
第三步,在Project -> Settings -> C/C++ -> Preprocessor的Additional include directories里,加上 JSONCPP 头文件所在目录,比如.\jsoncpp\include。
第四步,在Project -> Settings -> C/C++ -> Code Generation里,把Use run-time library设成Multithreaded DLL或Debug Multithreaded DLL,要和你的主工程保持一致。如果主工程用的是静态 CRT,JSONCPP 也要用静态 CRT,否则会出现链接冲突。
// 一个最小的 VC6.0 下 JSONCPP 使用示例 #include <json/json.h> #include <iostream> #include <string> int main() { // 待解析的 JSON 文本,注意中文用 UTF-8 编码 std::string strJson = "{\"name\":\"测试\",\"age\":30}"; Json::Value root; Json::Reader reader; // 解析 bool bRet = reader.parse(strJson, root); if (!bRet) { // 解析失败,输出错误信息 std::cout << "parse error: " << reader.getFormatedErrorMessages() << std::endl; return -1; } // 读取字段 std::string name = root["name"].asString(); int age = root["age"].asInt(); std::cout << "name: " << name << ", age: " << age << std::endl; return 0; }这段代码的逻辑很直接:reader.parse把字符串解析到root,然后通过root["key"]取值。参数说明上,parse的第二个参数是输出用的Json::Value,第三个参数默认是true,表示允许注释;在 VC6.0 下建议保持默认,不要乱改。asString()和asInt()是类型转换,如果字段不存在或类型不对,会返回默认值(空字符串或 0),不会抛异常,这一点在排查问题时要注意。
3. 中文乱码的根因与防乱码处理方案
3.1 VC6.0 下中文乱码到底出在哪一步
中文乱码在 VC6.0 + JSONCPP 场景里,通常不是 JSONCPP 本身的问题,而是编码链路没对齐。VC6.0 默认的源文件编码是 GBK(简体中文 Windows 的 ANSI 代码页),而 JSON 标准要求字符串是 Unicode,实际传输中绝大多数接口用的是 UTF-8。于是链路变成:你的源码里写的是 GBK 中文字面量,JSONCPP 按字节处理,输出到接口或从接口读回来时,编码不一致,就乱码了。
具体表现有三种:一是解析后asString()拿到的中文是乱码;二是序列化后写出去的中文变成\uXXXX或者问号;三是控制台输出时显示乱码,但实际数据是对的。要区分清楚是哪一种,再对症处理。
3.2 统一用 UTF-8 作为中间编码
最稳妥的做法是:不管源码是什么编码,进入 JSONCPP 之前,把中文转成 UTF-8;从 JSONCPP 出来之后,再转回你需要的编码。VC6.0 下没有现成的std::codecvt可用,常见做法是用 Windows API 的MultiByteToWideChar和WideCharToMultiByte做转换。
// GBK 转 UTF-8 std::string GbkToUtf8(const std::string& strGbk) { // 先把 GBK 转成宽字符 int nWideLen = MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, NULL, 0); if (nWideLen <= 0) return ""; wchar_t* pWide = new wchar_t[nWideLen + 1]; MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, pWide, nWideLen); pWide[nWideLen] = L'\0'; // 再把宽字符转成 UTF-8 int nUtf8Len = WideCharToMultiByte(CP_UTF8, 0, pWide, -1, NULL, 0, NULL, NULL); if (nUtf8Len <= 0) { delete[] pWide; return ""; } char* pUtf8 = new char[nUtf8Len + 1]; WideCharToMultiByte(CP_UTF8, 0, pWide, -1, pUtf8, nUtf8Len, NULL, NULL); pUtf8[nUtf8Len] = '\0'; std::string strUtf8(pUtf8); delete[] pWide; delete[] pUtf8; return strUtf8; }这段代码的关键参数是CP_ACP和CP_UTF8。CP_ACP表示当前系统 ANSI 代码页,在简体中文 Windows 上就是 GBK。MultiByteToWideChar先把 GBK 转成宽字符,WideCharToMultiByte再把宽字符转成 UTF-8。两次转换都先调用一次获取长度,再分配内存,这是 Windows API 的标准用法。注意new和delete[]要配对,VC6.0 下没有智能指针,内存泄漏要自己盯住。
反过来,UTF-8 转 GBK 就是把两个 API 的参数对调,CP_UTF8在前,CP_ACP在后。
3.3 解析和序列化时怎么保证中文不乱
解析时,如果接口返回的是 UTF-8 的 JSON 文本,直接丢给reader.parse就行,JSONCPP 会按字节存进Json::Value。你取出来的时候,asString()返回的也是 UTF-8 字节流。如果你要在 VC6.0 的界面上显示,需要再转成 GBK。
序列化时,如果你构造的Json::Value里存的是 GBK 中文,直接FastWriter::write出来的 JSON 文本就是 GBK 编码的,发给接口大概率乱码。正确做法是先把 GBK 转成 UTF-8,再塞进Json::Value。
// 构造带中文的 JSON 并序列化 Json::Value root; root["name"] = GbkToUtf8("张三"); // 先转 UTF-8 root["city"] = GbkToUtf8("北京"); Json::FastWriter writer; std::string strJson = writer.write(root); // 此时 strJson 里的中文是 UTF-8 编码,可以直接发给接口这里用FastWriter而不是StyledWriter,是因为FastWriter输出紧凑,没有多余换行和缩进,适合网络传输。StyledWriter适合调试时看结构。参数上,FastWriter默认会在末尾加一个换行符,如果接口对末尾换行敏感,可以用writer.write(root)之后手动去掉最后一个\n。
注意:JSONCPP 在序列化时,默认会把非 ASCII 字符转成
\uXXXX转义形式。如果你不希望转义,需要确认你用的 JSONCPP 版本是否支持writer.omitEndingLineFeed()之类的选项,或者接受转义形式,因为\uXXXX本身也是合法 JSON,解析方通常能正确处理。
4. 避坑与排查:VC6.0 下 JSONCPP 的 5 个血泪经验
4.1 编译报错 “cannot open include file: ‘json/json.h’”
现象:在 VC6.0 里编译时,提示找不到json/json.h。
原因:头文件搜索路径没设对,或者 JSONCPP 的目录结构和你写的不一致。JSONCPP 的头文件通常在include/json/下,所以#include <json/json.h>要求include目录在搜索路径里。
解决:在Project -> Settings -> C/C++ -> Preprocessor -> Additional include directories里加上.\jsoncpp\include,注意是include这一层,不是include\json。如果你把json.h直接放在了工程目录下,那就改成#include "json.h",但这样会破坏 JSONCPP 内部的相对包含关系,不推荐。
4.2 链接报错 “unresolved external symbol”
现象:编译通过,链接时提示一堆Json::Reader::parse之类的未解析外部符号。
原因:JSONCPP 的.cpp文件没有加入工程,或者加入了但没参与编译。VC6.0 里有时候文件加进去了,但工程设置里被排除了。
解决:检查FileView里json_reader.cpp、json_value.cpp、json_writer.cpp是否都在,右键每个文件看Settings,确认Exclude file from build没有被勾上。另外,如果你用的是静态库形式的 JSONCPP,要确认.lib文件在Project -> Settings -> Link -> Object/library modules里被引用。
4.3 解析中文时asString()返回空或乱码
现象:JSON 文本里明明有中文,root["name"].asString()返回空字符串或者乱码。
原因:JSON 文本的编码和 JSONCPP 期望的编码不一致。JSONCPP 内部按 UTF-8 处理,如果你传进去的是 GBK 编码的文本,解析可能失败或者得到乱码。
解决:在reader.parse之前,确认strJson是 UTF-8 编码。如果是 GBK,先用GbkToUtf8转一遍。另外,检查 JSON 文本里的中文是否被正确转义,有些接口返回的是\u4e2d\u6587形式,JSONCPP 能自动解析成中文,不需要额外处理。
4.4 序列化后中文变成\uXXXX
现象:用FastWriter写出来的 JSON 字符串里,中文全变成了\u4e2d这样的转义序列。
原因:这是 JSONCPP 的默认行为,它会把所有非 ASCII 字符转义,以保证输出的 JSON 是纯 ASCII 的。这本身不是 bug,是符合 JSON 标准的。
解决:如果你不希望转义,可以尝试修改 JSONCPP 的json_writer.cpp里的valueToQuotedString函数,把转义逻辑去掉。但更推荐的做法是接受转义形式,因为接收方解析\uXXXX是标准行为。如果接收方明确要求中文原样输出,那就只能改源码,改完后要重新编译 JSONCPP。
4.5 Debug 版和 Release 版混用导致崩溃
现象:Debug 下跑得好好的,Release 下解析 JSON 时程序崩溃。
原因:VC6.0 下 Debug 和 Release 使用的 CRT 版本不同,内存分配和释放跨模块时容易出问题。JSONCPP 编译时用的 CRT 和主工程不一致,new出来的内存在另一个模块里delete,就会崩溃。
解决:确保 JSONCPP 和主工程的Use run-time library设置一致。Debug 版都用Debug Multithreaded DLL,Release 版都用Multithreaded DLL。如果主工程是静态 CRT,JSONCPP 也要用静态 CRT。改完设置后,全部重新编译一遍,不要只编一个文件。
5. 进阶技巧:把 JSON 解析封装成可复用的 VC6.0 工具类
5.1 封装一个简单的 JSON 辅助类
在 VC6.0 下反复写reader.parse和root["key"].asString()很啰嗦,而且每次都要处理编码转换。我一般会封装一个CJsonHelper类,把解析、取值、编码转换都包进去。
// JsonHelper.h #ifndef __JSON_HELPER_H__ #define __JSON_HELPER_H__ #include <json/json.h> #include <string> class CJsonHelper { public: CJsonHelper(); ~CJsonHelper(); // 解析 UTF-8 的 JSON 文本 bool Parse(const std::string& strUtf8Json); // 获取字符串字段,自动转成 GBK 返回 std::string GetString(const std::string& strKey); // 获取整型字段 int GetInt(const std::string& strKey); // 设置字符串字段,自动从 GBK 转成 UTF-8 存入 void SetString(const std::string& strKey, const std::string& strGbkValue); // 序列化成 UTF-8 的 JSON 文本 std::string ToString(); private: Json::Value m_root; Json::Reader m_reader; }; #endif这个类的设计思路是:对外接口用 GBK,对内存储和序列化用 UTF-8。Parse接收 UTF-8 文本,GetString返回 GBK,SetString接收 GBK,ToString输出 UTF-8。这样调用方不需要关心编码转换,减少出错概率。
5.2 实现中的两个关键点
第一个关键点是GetString里的编码转换。如果字段不存在,root[key]会返回一个 null 类型的Json::Value,asString()返回空字符串。这时候直接返回空字符串就行,不要去做编码转换,否则可能出问题。
第二个关键点是SetString里的转换。先GbkToUtf8,再赋值给m_root[key]。注意Json::Value的赋值会拷贝字符串,所以临时变量的生命周期不用担心。
// JsonHelper.cpp 片段 std::string CJsonHelper::GetString(const std::string& strKey) { if (!m_root.isMember(strKey)) { return ""; } std::string strUtf8 = m_root[strKey].asString(); if (strUtf8.empty()) { return ""; } return Utf8ToGbk(strUtf8); // 转回 GBK } void CJsonHelper::SetString(const std::string& strKey, const std::string& strGbkValue) { std::string strUtf8 = GbkToUtf8(strGbkValue); m_root[strKey] = strUtf8; }isMember用来判断字段是否存在,避免对不存在的字段做无意义操作。Utf8ToGbk和GbkToUtf8就是前面那两个转换函数的封装,实现逻辑一样,只是参数对调。
5.3 验证方法:用一个带中文的 JSON 跑通全流程
封装完之后,用一个包含中文的 JSON 做端到端验证。构造一个 GBK 的中文字符串,用SetString存进去,ToString输出,再用Parse解析回来,GetString取出,看是否和原始字符串一致。
CJsonHelper helper; helper.SetString("name", "测试中文"); helper.SetString("city", "上海"); std::string strJson = helper.ToString(); // strJson 应该是 UTF-8 编码的 JSON CJsonHelper helper2; helper2.Parse(strJson); std::string strName = helper2.GetString("name"); // strName 应该等于 "测试中文"如果strName和原始字符串一致,说明编码链路是通的。如果不一致,重点检查GbkToUtf8和Utf8ToGbk的实现,以及Parse传入的文本是不是 UTF-8。
提示:VC6.0 的调试器对 UTF-8 字符串显示不友好,调试时看到的乱码不一定是真的乱码,建议把中间结果写到文件里,用支持 UTF-8 的编辑器查看。
5.4 一个我踩过的坑:Json::Value的默认值陷阱
Json::Value在取值时,如果类型不匹配,不会报错,而是返回默认值。比如root["age"].asInt(),如果age字段是字符串"30",asInt()会返回 0,而不是 30。这个行为在 VC6.0 下尤其要注意,因为老系统的接口字段类型经常不稳定。
我的习惯是,在取值之前先用isInt()、isString()判断类型,或者用isConvertibleTo检查。如果类型不对,要么做转换,要么记录日志。不要假设接口返回的类型永远正确,血泪经验告诉我,接口字段类型变更是最常见的翻车原因之一。
if (m_root["age"].isInt()) { int nAge = m_root["age"].asInt(); } else if (m_root["age"].isString()) { // 字符串形式的数字,手动转换 int nAge = atoi(m_root["age"].asString().c_str()); }这段代码展示了类型判断的写法。isInt()和isString()是Json::Value的成员函数,返回 bool。atoi是 C 标准库函数,VC6.0 下可用。注意asString()返回的是std::string,要.c_str()才能传给atoi。
5.5 最后一点个人习惯
在 VC6.0 下做 JSON 解析,我最大的习惯是:所有涉及编码转换的地方,都写单元测试。VC6.0 没有现代测试框架,我就写一个简单的main函数,把各种边界情况跑一遍:空字符串、纯中文、中英混合、特殊字符、超长字符串。每次改完 JSONCPP 的配置或者编码转换函数,都重新跑一遍。这个习惯帮我省了很多后悔药,因为编码问题一旦上线才发现,排查成本极高。
希望帮到你。
本文还有配套的精品资源,点击获取