资讯详情

资讯详情

VC6.0下用jsoncpp解析JSON并解决中文乱码

简介面向VC6.0开发者的完整JSONCPP调用案例包帮助在Visual C 6.0环境中快速解析与生成JSON数据并重点解决中文字符在读写时出现的乱码问题。资源基于jsoncpp-src-0.5.0附带可运行的对话框/控制台示例工程覆盖源码集成、编码处理、测试验证等环节。包体共72个文件压缩后约3.77MB其中头文件与cpp源文件构成JSONCPP核心库inl内联文件辅助模板实现dsp/dsw保存工程配置exe是编译好的演示程序doc和txt则为使用说明与注意事项各类型分工明确方便对照查阅。目前已有590人学习下载。资源内含“VC6.0 测试通过的JSONCPP源码类使用说明.doc”和“必看.txt”详细梳理了源码加入项目的步骤、中文UTF-8编码的设置方法以及常见乱码陷阱示例工程演示了从输入JSON到解析、序列化再到界面显示中文的完整流程并附有多个可运行/可测试的文件。对于需要维护老项目、或首次在VC6.0上接入JSONCPP的开发者是一份可直接参照的成套资料。1. VC6.0 环境里的 jsoncpp一个让你不用换编译器就能解析 JSON 的完整方案如果你还在维护一套 VC6.0 时代的老代码最难受的往往不是业务逻辑复杂而是想接一个现代接口时发现手里没有顺手的 JSON 解析库总不能为了让程序读接口就逼着公司把整套编译器全部升级。这套“VC6.0 调用 JSONCPP 全案例_支持解析中文防乱码【无措版】”解决的就是这个尴尬它在 VC6.0 环境下把 jsoncpp 完整编译好、调用链全部打通并且针对最头疼的中文乱码问题给出了现成的编码转换方案。不管你是要写一个读取 HTTP 接口返回的小工具还是要在遗留系统里增加配置文件的 JSON 解析能力拿到这份资源都能直接照着改。下面是整个拆解过程从目录结构讲起到核心代码再到避坑记录和二次开发思路。2. 环境准备在 VC6.0 里把 jsoncpp 编译成静态库并配进业务工程jsoncpp 是个开源 C JSON 库它本身依赖很少理论上任何 C 编译器都能编译但 VC6.0 毕竟太老标准库支持不完整直接拿新版 jsoncpp 源码丢进 VC6.0 十有八九会报一堆语法错误。所以第一步不是写业务代码而是先把 jsoncpp 的源码在 VC6.0 里编译成静态库再把头文件和库文件配置到业务工程里。2.1 先看明白资源里的目录结构拿到资源后不要急着往工程里拖文件先把目录结构看清楚。jsoncpp 的老版本源码一般长这样jsoncpp-src/ ├── include/ │ └── json/ │ ├── json.h # 统一入口头文件 │ ├── reader.h # Json::Reader 声明 │ ├── value.h # Json::Value 声明 │ └── writer.h # Json::Writer 相关声明 ├── src/ │ ├── json_reader.cpp │ ├── json_value.cpp │ └── json_writer.cpp ├── CMakeLists.txt └── README.md核心源码就三个 cpp 文件对应三个核心类ValueJSON 值对象、Reader解析器、Writer序列化器。老版本 jsoncpp 不像新版本有大量模板和 C11 特性所以 VC6.0 编译时只需要处理少量兼容性问题比如把某些 using 语句调整位置、处理一下 size_t 和 int 的类型转换。这套资源说是“无措版”关键就在于这些兼容点已经提前改好你不需要自己去啃编译报错。提示如果资源里已经自带编译好的 .lib 文件也别跳过这一节因为二次开发时你可能要调整编译选项重新生成库文件。2.2 把三个 cpp 文件编成静态库打开 VC6.0新建工程选Win32 Static Library工程名字可以叫json_vc6_lib。然后把src目录下的json_reader.cpp、json_value.cpp、json_writer.cpp加进来include 目录指向资源里的include文件夹。这里有几个关键配置要注意。项目属性里C/C - Preprocessor建议加上_CRT_SECURE_NO_WARNINGSVC6 里虽然没有这个宏但后续如果迁移到新版本编译器这个宏能消掉一堆 CRT 安全警告。字符集设置成Use Multi-Byte Character Set因为老工程里大多数界面代码用的是 MBCS强制切到 Unicode 反而更麻烦。静态库的编译结果是一个.lib文件切到 Release 配置编译一次再切到 Debug 配置编译一次生成两个库分别对应后续业务工程的 release 和 debug 链接。实际工程里我会为这两个库加上版本后缀比如json_vc6_lib.lib和json_vc6_libd.lib链接时不容易混淆。【无措版】资源里可能直接给了已经编译好的库文件但如果你的工程有特殊需求比如要调试 jsoncpp 内部代码那就得自己重新编译步骤和上面一样。编译完成后库文件会输出到工程的 Debug 或 Release 目录里。2.3 把库配置进业务工程业务工程里使用 jsoncpp 的方式分两步让编译器找得到头文件让链接器找得到库文件。假设你的业务工程叫MyApp.dsp在 VC6.0 的Tools - Options - Directories里把资源里的include路径加入Include files列表把包含.lib文件的目录加入Library files列表。更推荐的做法是在每个源文件顶部直接写#include json/json.h #pragma comment(lib, json_vc6_lib.lib)用#pragma comment(lib, ...)的好处是库文件的依赖关系直接写在代码里别人拿到你的源码时不需要再去手动配置工程属性也不容易出现“我明明设置了路径为什么还链接失败”的问题。如果你同时生成了 debug 和 release 两个版本的库可以用#ifdef _DEBUG区分链接#ifdef _DEBUG #pragma comment(lib, json_vc6_libd.lib) #else #pragma comment(lib, json_vc6_lib.lib) #endif这个配置方式在 VC6.0 里很常见也是这套资源里所有案例通用的基础后面的调用代码全部依赖这一步成功。3. 核心代码拆解从构造 JSON 到解析返回的完整调用链jsoncpp 的调用模型其实很简单一个Json::Value对象代表任意 JSON 值Reader负责把字符串转成ValueWriter负责把Value转回字符串。只要把这三者的关系理解透所有案例代码都是在这条链路上做文章。3.1 Json::Value构造数据的两种姿势构造 JSON 数据时最直观的方式是用operator[]链式赋值#include json/json.h Json::Value root; root[name] zhang_san; root[age] 30; root[skills][0] c; root[skills][1] jsoncpp; root[address][city] shenzhen; Json::Value extra; extra[email] zhangexample.com; root[extra] extra;operator[]返回的是Value的引用所以可以连续赋值像是操作一个std::map。这里有个关键点当root[skills]是第一次出现时jsoncpp 会自动把它初始化为null类型你再对[skills][0]赋值时它又会自动变成数组类型。这种自动类型晋升在多数时候很方便但要注意它可能会导致意外的内存增长比如循环里不断用不存在的索引赋值Value 对象会不断扩容。构造完成后可以用root.toStyledString()直接输出成格式化的 JSON 文本快速验证结构std::string jsonText root.toStyledString(); printf(%s\n, jsonText.c_str());这段代码输出的是带缩进的 JSON 字符串每个 key 和 value 对应关系一目了然。在实际调用链里root会作为请求体的一部分被传给序列化函数。3.2 Json::Reader解析字符串与文件解析是反方向的操作Reader::parse接受一个字符串和两个指针把 JSON 文本填充到目标Value对象里Json::Value parsed; Json::Reader reader; std::string inputJson {\code\:200,\message\:\ok\,\data\:{\list\:[1,2,3]}}; bool success reader.parse(inputJson, parsed); if (!success) { // 解析失败时reader 里保存了错误信息 printf(parse error: %s\n, reader.getFormattedErrorMessages().c_str()); return; } int code parsed[code].asInt(); std::string message parsed[message].asString(); const Json::Value data parsed[data];parse的第一个参数还可以是const char*加长度或者直接传std::string。老版本 jsoncpp 的parse返回bool新版改成抛出异常这套资源用的是老版本接口所以判断返回值是一种习惯。第二个参数是输出对象因为 C 没有多返回值所以用引用传参的方式把结果带回。解析完成后parsed[code]的类型是Value通过.asInt()、.asString()可以转成具体类型。这里有一个重要的检查习惯在调用.asInt()之前应该先用.isInt()判断避免Value内部类型和期望类型不匹配时 undefined behavior。文件解析也只是换了一个数据来源把文件内容读进std::string再调用parse即可。老项目里读取文件时要注意文件打开模式二进制模式读出来的字符串不会丢失任何字节文本模式在 Windows 下可能被换行符转换影响。3.3 Json::Writer序列化为 UTF-8 文本构造和解析之外输出也是高频操作。老版本 jsoncpp 里最常用的是FastWriter和StyledWriterJson::FastWriter fastWriter; std::string compactText fastWriter.write(root); Json::StyledWriter styledWriter; std::string prettyText styledWriter.write(root);FastWriter输出的 JSON 是压缩的没有多余空格适合网络传输StyledWriter输出带换行和缩进适合写日志和调试。两者的输出都是 UTF-8 编码的字节串这一点对后面的中文乱码处理至关重要。实际开发中我一般会封装一个函数把 writer 的选择作为参数暴露出去这样调试时能切换输出格式std::string serializeJson(const Json::Value value, bool pretty false) { if (pretty) { return Json::StyledWriter().write(value); } return Json::FastWriter().write(value); }序列化结果最终会被写入文件、发送到套接字或者传给界面显示。无论走哪条路都要记住它现在是一段 UTF-8 字节流如果目标环境不是 UTF-8就绕不开下一章说的乱码问题。3.4 一个完整函数解析带嵌套数组的响应单个 API 的调用在真实项目里不会像前面那样零散通常会把“拼装请求 JSON - 发送网络请求 - 解析响应 JSON”封装成一个函数。下面这段代码是案例里比较有代表性的处理的是嵌套两层的数据结构也是很多接口返回的典型形态bool handleApiResponse(const std::string responseText) { Json::Value dict; Json::Reader reader; if (!reader.parse(responseText, dict)) { printf(response parse failed\n); return false; } if (!dict.isObject()) { printf(response root is not object\n); return false; } // 检查业务码 if (!dict.isMember(code)) { printf(no code field\n); return false; } int code dict[code].asInt(); if (code ! 0) { printf(business error, code %d, msg %s\n, code, dict.get(message, ).asString().c_str()); return false; } // data 本身是数组遍历每个元素 const Json::Value data dict[data]; if (!data.isArray()) { printf(data field is not array\n); return false; } for (int i 0; i (int)data.size(); i) { const Json::Value item data[i]; std::string name item.get(name, ).asString(); int id item.get(id, 0).asInt(); printf(item id%d name%s\n, id, name.c_str()); } return true; }这个函数里有三处值得模仿的防御性写法用isMember判断字段是否存在再访问用get(key, default)提供默认值替代可能触发类型转换问题的operator[]遍历前先判断目标节点是不是数组。老版本 jsoncpp 对类型不匹配的处理比较“宽容”往往不会抛出明确的错误而是返回一堆垃圾值所以访问路径上每一层都要有类型检查这是我在项目里吃过亏之后总结的习惯。4. 中文防乱码的实现从 GBK 到 UTF-8 的两层编码处理标题里“支持解析中文防乱码”是整个资源最核心的卖点。原因很简单VC6.0 时代的中文 Windows 系统本地代码页是 GBK程序内部字符串默认按 GBK 处理界面控件、控制台输出、文件读写全都是这个编码体系。而 jsoncpp 解析和序列化 JSON 时用的是 UTF-8。两者的编码空间有重叠但中文部分完全不兼容如果不做转换解析结果在界面上显示出来就是一堆乱码这在“玄学”和“科学”之间往往只差一层编码转换代码。4.1 乱码的根因三处编码不一致先说清楚乱码到底是怎么产生的。一个 JSON 字符串从网络接口返回时字节流是 UTF-8 编码比如“深圳”在 UTF-8 里占 6 个字节每三个字节表示一个字。jsoncpp 解析后Value内部存的还是这 6 个字节。你用printf输出时控制台默认按 GBK 解释这 6 个字节结果当然不是“深圳”两个字。同样的道理如果你在程序里硬编码一个中文字符串作为 keyVC6.0 编译器默认把源码里的字符串常量按 GBK 存入.exe而 jsoncpp 在查找 key 时比较的是字节序列UTF-8 编码的“深圳”和 GBK 编码的“深圳”是完全不同的字节串所以用operator[]查永远查不到。这就是为什么有些开发者明明解析成功了却取不到中文字段值。4.2 封装 GBK 与 UTF-8 的转换函数转换函数是这套资源里防乱码的关键部件。VC6.0 的运行时库有mbstowcs/wcstombs这一对函数但处理 UTF-8 和 GBK 互转时更可靠的是 Windows API 的MultiByteToWideChar和WideCharToMultiByte组合先转成宽字符UTF-16作为中间层再转成目标编码#include windows.h #include string std::string Utf8ToGbk(const std::string utf8Str) { if (utf8Str.empty()) { return ; } // 第一步UTF-8 - UTF-16 int wLen MultiByteToWideChar( CP_UTF8, 0, // 输入是 UTF-8 utf8Str.c_str(), -1, // -1 表示处理整个字符串含结尾 NULL, 0 // 第一次调用获取所需缓冲区大小 ); if (wLen 0) { return ; } wchar_t* wBuf new wchar_t[wLen]; MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, wBuf, wLen); // 第二步UTF-16 - GBK int gbkLen WideCharToMultiByte( CP_ACP, 0, // 本地代码页中文系统下是 GBK wBuf, -1, NULL, 0, NULL, NULL ); if (gbkLen 0) { delete[] wBuf; return ; } char* gbkBuf new char[gbkLen]; WideCharToMultiByte(CP_ACP, 0, wBuf, -1, gbkBuf, gbkLen, NULL, NULL); std::string result(gbkBuf); delete[] gbkBuf; delete[] wBuf; return result; }这段代码的模式很典型第一次调用 API 时传NULL作为输出缓冲区让系统返回所需长度第二次调用才真正填充内容。这样能避免缓冲区太小导致数据截断也是老代码里最朴素的内存安全写法。对应的GbkToUtf8函数只是把CP_ACP和CP_UTF8的位置对调代码结构几乎一样业务代码里一般两个都要封装。这两个函数在案例里被反复调用也是防乱码方案的基础工具。注意MultiByteToWideChar的第四个参数传-1时会处理到字符串末尾的\0并把\0算进长度因此wBuf和gbkBuf要预留足够空间否则返回的std::string会带一个多余的结尾字符。更稳的写法是把-1改成(int)utf8Str.length()并在WideCharToMultiByte里传入实际字节数不包含结尾这块细节在排错时经常被忽略。4.3 解析链路里怎么插入转换转换函数封装好之后关键的问题是插在哪里。我一般会在两个边界位置做转换网络接口数据进Reader::parse之前把 GBK 字符串转成 UTF-8 再交给 jsoncpp数据从Value取出来显示到屏幕之前把 UTF-8 转成 GBK。std::string displayName Utf8ToGbk(item[name].asString()); printf(姓名: %s\n, displayName.c_str());这样做的原因在于jsoncpp 本身是一个纯文本解析算法它不关心业务层的编码体系只负责按 UTF-8 处理输入输出所以只要保证“喂给它的字节流是 UTF-8、从它那里拿出来的字节流按需转成界面编码”即可。如果把转换放在一半的解析路径上反而会因为部分字段已经被解析成 UTF-8、部分还是 GBK 而更乱。4.4 容易被忽略的两个细节BOM 和文件读写模式除了字符串转换还有两个细节专门处理“解析中文防乱码”时的边角问题。第一个是 UTF-8 文件的 BOM。有些接口或配置文件会在开头带三个字节的EF BB BFjsoncpp 不会自动跳过 BOM导致第一个字段名解析出错。可以在解析前判断字符串开头是不是\xEF\xBB\xBF是的话直接截掉if (inputStr.compare(0, 3, \xEF\xBB\xBF) 0) { inputStr inputStr.substr(3); }第二个是文件打开模式。用 C 的std::ifstream读取文件时如果不指定std::ios::binary在 Windows 上会触发换行符转换读进来的字节长度跟实际文件不一致。遇到“文件里明明有数据但解析后是空的”这种情况先把读取模式改成二进制通常问题就解决了。这套资源里的案例代码全部按二进制模式读取文件也是处理中文不乱码的一个必要前提。5. 常见问题与避坑记录VC6 jsoncpp 的五次翻车现场这套资源里附带了不少常见问题的说明这里挑五个我在实际调试中最容易遇到的记录一下每一条都是“现象 - 原因 - 解决”的完整链条按顺序排查能省下大半天时间。5.1 编译时抛出 C2664 或 C2440std::string 无法隐式转换现象调用reader.parse(stdStringValue, root)时编译器报“cannot convert parameter from std::string to char*”。 原因VC6.0 的 STL 里std::string没有提供c_str()给所有 API 使用部分重载版本匹配不上。这不是 jsoncpp 的问题而是老标准库的隐式转换不够完善。 解决显式传const char*即reader.parse(str.c_str(), root)同时在接口定义上多用const std::string而不是裸指针两头都留好转换余地。这套资源的案例代码里所有 parse 调用都统一走的这个模式。5.2 链接时找不到 json_reader.obj 或 LNK2001现象编译通过链接时报unresolved external symbol public: bool __thiscall Json::Reader::parse(...)。 原因jsoncpp 的源文件没有编进工程或者编进去了但工程配置里没生成到最终链接产物。VC6.0 的工程有两种常见模式一是把 cpp 文件直接拖进业务工程一起编译二是先编成 lib 再链接报 LNK2001 绝大多数情况是第二种模式里 lib 路径没有配对。 解决先用布满整个工程搜索一遍“json_reader.cpp”是否存在于编译列表中再确认#pragma comment(lib, ...)的路径和实际 lib 文件是否一致最后检查 debug 和 release 的库是否混用。5.3 运行时不崩溃但获取的字段值为 0 或空现象解析成功返回值是true但parsed[count].asInt()得到 0parsed[name].asString()得到空串而输入 JSON 里明明有数据。 原因字段路径写错了或者 JSON 数据的结构跟你预期的不一样。比如接口返回的数组套在data里两层嵌套你却用一层路径去取jsoncpp 会返回一个默认构造的ValueasInt就是 0。 解决把解析后的内容用StyledWriter::write(parsed)打印出来看parsed的实际层次结构再逐层核对路径。这一步能在大多数场景里精准定位问题比猜路径要快得多。5.4 中文字典内容能解析但查 key 失败现象用root[名称]取中文字段得到的Value是 null而打印StyledWriter输出时明明有这个 key。 原因源文件里的中文字符串常量被 VC6.0 按 GBK 编码写入 exe而 JSON 文本里的 key 是 UTF-8 编码两者字节序列完全不一样字符串比较永远失败。 解决把中文字段名的访问统一改成先转码再查找比如把源码里的 GBK 字面量转成 UTF-8 后用isMember判断。具体做法是写一个工具或直接用GbkToUtf8(名称)作为 key保证两边编码一致。这也是标题里“防乱码”覆盖到的一个业务场景。5.5 解析包含大文件的 JSON 时内存占用过高现象一个几 MB 的 JSON 配置文件程序启动时Reader::parse之后内存飙升几十 MB性能表现不佳。 原因Json::Value的结构设计初衷是方便访问每个节点都有额外类型标记和子节点指针对大型数组和对象来说内存开销会比原始文本大出数倍。老版本 jsoncpp 的解析器又没有流式接口一次 parse 必然全部展开。 解决对于超大 JSON考虑拆分后分段解析或者改用 SAX 风格的解析库如果只能用 jsoncpp尽量把 Value 的生存周期缩短解析完提取需要的数据后就立刻释放不要在整个程序生命周期里持有大对象。案例里提供了“提取后立即释放”的写法值得参考。6. 二次开发扩展从固定接口到参数可配的 JSON 工具函数实例代码大多是硬编码的接口调用真实项目里一个功能往往要对接多个接口每个接口的请求参数都不一样。如果每次都从头写一遍ReaderValueWriter的样板代码能累死人。把通用操作抽成一个工具函数是更合适的方向。这里分享一个我自己的改造习惯把“填充请求参数”变成一张表驱动而不是写死几十个root[param] xxx。比如要拼一个登录请求可以维护一个配置映射Json::Value buildRequest(const std::mapstd::string, std::string params) { Json::Value req; for (std::mapstd::string, std::string::const_iterator it params.begin(); it ! params.end(); it) { req[it-first] it-second; } return req; }业务层只需要准备参数表具体的序列化交给工具函数。改造后的调用方式变成std::mapstd::string, std::string p; p[username] GbkToUtf8(userInput); p[password] GbkToUtf8(passInput); std::string reqBody serializeJson(buildRequest(p));这类简单的封装在工程里有很高的实用价值后续新增接口时只需加一张表不用再动 JSON 操作代码。二次开发时另一个值得做的是自检函数。每次改动完 jsoncpp 相关代码我会调用一个内部接口打印最近一次完整的解析输入和输出用StyledWriter格式化之后肉眼检查结构。这个习惯在排查“是数据问题还是代码问题”时非常管用也比单步调试更高效因为直接看文本就能发现是编码不对还是字段路径不对。老实说VC6.0 这套环境早就不算主流了但存量系统不会因为编译器版本老就自动消失该接的接口、该做的配置解析一样都躲不掉。这套资源把最麻烦的编译兼容和中文编码问题提前趟平了省掉的是反复翻资料、试各种宏定义的暗时间。从那以后我每次在继承代码里遇到 JSON 解析都会把“喂进去的字节流是什么编码、取出来显示到什么地方”这个问题先问清楚再动手写代码而不是等到界面上吐一堆乱码才回头找原因。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →