简介:面向 Visual C++ 6.0 下的 C++ 开发者,这份资源聚焦 JSONCPP 源码在 VC6.0 工程中的集成与调用,重点解决中文解析、序列化时的乱码问题。资源内含可运行的测试案例工程,完整展示从工程配置、源码引入到中文 JSON 读写验证的全流程,适合需要在老版本 VC 环境中处理 JSON 数据的开发者直接参考。
包体信息:文件总数 72 个,压缩包 3.77MB。主要包含 24 个头文件、9 个 cpp 源文件、6 个 inl 模板实现文件,覆盖 json_reader、json_writer、json_value 等核心模块;另有 doc 使用说明、dsp/dsw 工程文件、exe 可执行示例及 txt 说明文档,便于对照调试。
目前已有 587 人学习下载。除了完整 VC6.0 源码工程外,还配有《VC6.0 测试通过的 JSONCPP 源码类使用说明.doc》、必看说明和 MyJson 测试文本,能帮助读者理清头文件包含顺序、编码设置和常见坑点,减少自行摸索成本。 VC6.0 配 JSONCPP,这个话题放到 2025 年聊,第一反应肯定是“这编译器还没退役?”但实际做过老项目维护的朋友都懂,很多工业软件、教育类系统、银行柜台程序,后台核心代码就是用 VC6.0 写的,你说重构?领导一句“能跑就别动”就能让你闭嘴。所以遇到“VC6.0 里头要解析 HTTP 接口返回的 JSON”这种需求,根本绕不开老编译器兼容性这道坎。
我这段时间正好把一个遗留系统的数据对接模块翻新了一遍,接口返回的全是 JSON,其中有大量中文内容,比如用户姓名、地址、备注信息。最头疼的不是 JSON 解析,而是中文编码:JSONCPP 处理的是 UTF-8,VC6.0 的时代默认却是 ANSI/GBK,两者不对齐就是满屏乱码。折腾了几天,总算整理出一套无措版方案,从 JSONCPP 的版本选择、VC6.0 编译配置,到中文编码转换、常见报错排查,全流程跑通。这篇就当是给同样啃老代码的朋友一份参考笔记。
1. 为什么用 JSONCPP?老编译器的选型思路
1.1 VC6.0 能用的 JSON 库其实没几个
先别急着写代码,选库这一步就足够劝退好多人。VC6.0 发布于 1998 年,对 C++ 标准的支持停留在早期模板阶段,基本等于“C++ 半生不熟”。现代 C++ 写的 JSON 库,比如 nlohmann/json、rapidjson 的较新版本,通通要求 C++11 起跳,放到 VC6.0 上直接就是一个接一个心碎的错误列表。
VC6.0 能用的库,得满足三个条件:一是纯模板或者纯手工 C++ 98 写法,二是没有用 C++11 之后的新特性,三是在网上有大量老前辈踩过坑、留过经验。满足这些条件的,最典型的就是JSONCPP 的 0.5.0 版本。这个版本发布于 2010 年前后,语法风格非常老派,全面兼容 VC6.0。虽然官方仓库现在主页写的是 1.x 版本,但 GitHub 那个 release 列表往下翻,0.5.0 的源码包还是能下到的。
选 0.5.0 还有一个好处:它是单一源码包,不依赖 CMake 构建系统。新版本的 JSONCPP 强制用 CMake 生成工程,VC6.0 的集成开发环境根本不好挂 CMake。而 0.5.0 的目录结构很简单,核心源码文件就那么几个,直接拖进工程就能编译,省一大半事。
1.2 JSONCPP 0.5.0 的核心文件构成
JSONCPP 0.5.0 解压后,重点不要被那些示例和文档带跑,你真正需要加进工程的只有下面这几类文件:
- 头文件:
json/json.h(它内部会包含json/config.h等相关头) - 实现文件:
src/lib_json/json_reader.cpp - 实现文件:
src/lib_json/json_writer.cpp - 实现文件:
src/lib_json/json_value.cpp
这四个文件齐活,就能够完成 JSON 的解析、生成、修改、遍历。还有几个辅助头文件,比如json_tool.h、json_batchallocator.h,它们在源码包里有,编译器会自动引用,不用手动处理。
有一点要注意:0.5.0 的源码里json/config.h有多处对JSONCPP_DISABLE_DLL、JSONCPP_STATIC这类宏的判断。如果你只是把 cpp 文件加入工程直接编译,不提前定义宏,链接时大概率会报__declspec(dllimport)相关的错误。我的做法是在工程预处理器宏里手动加上JSONCPP_DISABLE_DLL,明确告诉编译器和链接器:咱们静态编译,不需要动态库导入导出。这个坑我当年第一次踩的时候,愣是浪费了大半天,后来发现只是少了一个宏定义。
1.3 涉及 h 文件存储编码的硬件级问题
如果是头文件或者源码文件里有中文注释,一定还要留意一个细节:VC6.0 默认把源码文件当作 ANSI 编码解析,如果你的 cpp 文件保存的是 UTF-8(尤其是带 UTF-8 BOM 的),VC6.0 可能会把中文注释识别成一堆乱码,严重时直接编译报错。更稳妥的做法,是把所有参与编译的源代码文件统一保存成 ANSI 编码(即 GBK)。我习惯用 Notepad++ 的“转为 ANSI 编码”功能批量处理,配合 JSONCPP 源码一起做,避免后期玄学问题。
2. 环境准备与库编译实操
2.1 完整文件清单和目录规整
VC6.0 建工程,先不想代码的事,把目录结构整清楚是避免后面所有路径错误的前提。我一般是这样组织的:
MyProject/ ├── jsoncpplib/ │ └── json/ │ ├── json.h │ ├── config.h │ ├── ... ├── src/ │ ├── json_reader.cpp │ ├── json_writer.cpp │ └── json_value.cpp └── main.cpp工程文件本身放在 MyProject 目录下。这样配置头文件包含路径时,直接把MyProject根目录加进“Tools -> Options -> Directories -> Include files”,代码里写#include "json/json.h",编译器就能顺利找到。注意,不要直接引用 JSONCPP 源码包内部的include/json文件夹,因为源码里会有相对路径引用,一挪位置就容易找不到头文件。
2.2 工程配置的几个关键点
新建一个 Win32 Console Application 空工程,把上面四个 cpp 文件拖进去。然后右键工程属性(Project -> Settings),重点检查以下设置:
- C/C++ 选项卡,Category 选 Preprocessor,在 Additional include directories 填头文件路径;
- Preprocessor definitions 里加上
JSONCPP_DISABLE_DLL,有需要也可以加WIN32;_DEBUG;CONSOLE; - C/C++ 选项卡,Category 选 Code Generation,Use run-time library 选 Debug Multithreaded 或 Release Multithreaded,对应静态 CRT;
- Link 选项卡,Category 选 General,确保 Output file 名称没有冲突;工程类型如果是 Console,链接子系统就是 Console。
这样配置完,直接 F5 编译,顺利的话一次性通过。如果编译json_reader.cpp时报'for' loop initial declaration used outside C++'99 mode之类的错误,说明你打开的源码不是严格 C++98 规范的版本,或者 IDE 打开了“强制 C 模式”。VC6.0 默认按 C++ 编译,偶尔也会误判 .cpp 文件为 C 文件,需要检查一下工程里的文件类型确实是 C++ Source File。
2.3 静态链接的额外收获
把 JSONCPP 编译进自己的 exe,好处是不需要带着额外的 dll 文件到处拷贝。这在老系统部署环境尤其省心:目标机器五花八门,少一个 dll 就是一场事故。我打包出来的 release 版本只有一个 exe,丢到任何 Windows 上都能跑,无环境依赖。这一点,和很多用 C# 的同事交付时要配 .NET Framework 版本完全不同,算是一点“老技术的小固执”带来的踏实感。
3. 完整调用案例:解析与生成 JSON 的正确姿势
3.1 最基础的解析代码框架
先放一个最常见也最完整的调用样例,这段代码我一般放在一个公共工具类里面,供上层业务逻辑反复调用。它实现的功能是:从接口响应字符串中解析出 JSON 对象,读取顶层字段和嵌套字段,并遍历数组。
#include "json/json.h" #include <iostream> #include <fstream> #include <string> using namespace std; int main() { // 模拟一段HTTP接口返回的JSON响应,注意这段字符串本身是UTF-8编码 string strJSON = "{\"errCode\":0,\"errMsg\":\"成功\",\"data\":{\"userId\":10086,\"name\":\"张三\",\"tags\":[\"VIP\",\"老客户\"]}}"; Json::Reader reader; Json::Value root; // 核心解析:把JSON字符串解析进root对象 if (!reader.parse(strJSON, root)) { cerr << "JSON解析失败: " << reader.getFormattedErrorMessages() << endl; return -1; } // 读取整数和字符串类型字段 int errCode = root["errCode"].asInt(); string errMsg = root["errMsg"].asString(); int userId = root["data"]["userId"].asInt(); string name = root["data"]["name"].asString(); cout << "errCode: " << errCode << endl; cout << "errMsg: " << errMsg << endl; cout << "userId: " << userId << endl; cout << "name: " << name << endl; // 遍历数组节点 Json::Value tags = root["data"]["tags"]; for (unsigned int i = 0; i < tags.size(); i++) { cout << "tag[" << i << "]: " << tags[i].asString() << endl; } return 0; }这代码看起来简单,但已经覆盖了 JSONCPP 日常使用的绝大部分场景:Reader.parse负责把字符串变成Value,Value重载了operator[]用来取字段,asInt、asString做类型转换。解析失败时的getFormattedErrorMessages非常关键,它能打出具体出错位置,一个字符都不差,比闷头猜强太多。
3.2 生成 JSON 的写法
生成 JSON 相对更简单,直接用Json::Value往里面填充字段,然后交给FastWriter或者StyledWriter输出。区别是StyledWriter输出的字符串带换行缩进,适合调试看;FastWriter输出紧凑字符串,适合传输。
Json::Value request; request["cmd"] = "getUserInfo"; request["page"] = 1; request["pageSize"] = 20; Json::Value filter; filter["ageMin"] = 18; filter["ageMax"] = 60; request["filter"] = filter; // Value可以嵌套 Json::FastWriter writer; string out = writer.write(request); cout << out << endl;输出结果大概是:
{"cmd":"getUserInfo","page":1,"pageSize":20,"filter":{"ageMin":18,"ageMax":60}}这里有个容易忽略的细节:JSONCPP 的FastWriter::write默认在末尾追加一个换行符\n。如果要拿去和别人对接,要先out.erase(out.length()-1)把末尾换行去掉。这个坑我问过不止三个同行,大家居然都遇到过,只能说 JSONCPP 的作风太复古。
3.3 用文件保存和读取 JSON
实际项目中,JSON 字符串往往来自文件,而不是只写在代码里。这里有个 VC6.0 特有的隐患:ifstream默认按文本模式读取文件,遇到 Windows 的\r\n会把它转换掉,如果文件里还有 UTF-8 编码的中文,转换过程可能把字符破坏。更稳的做法是改用二进制方式读整个文件,然后交给 JSONCPP 做解析,编码转换逻辑完全由自己控制:
static string ReadAllContent(const string& filePath) { ifstream in(filePath.c_str(), ios::binary); if (!in) return ""; string content; char buf[1024]; while (in.read(buf, sizeof(buf))) content.append(buf, static_cast<size_t>(in.gcount())); content.append(buf, static_cast<size_t>(in.gcount())); return content; }读取进来之后,再看文件原本的编码。如果文件是 UTF-8 编码,直接交给reader.parse,后续输出到界面或写入数据库时再做转换。如果你自作聪明把读进来的字节先转成 GBK 再交给 JSONCPP,那asString拿回来的中文字段就会变成乱码,因为 JSONCPP 内部默认是把字符串节点当 UTF-8 处理的。这一点务必记牢。
4. 中文解析防乱码的完整解决方案
4.1 乱码的本质原因
聊到防乱码,必须先把原理捋明白。JSON 协议标准规定 JSON 文本必须使用 UTF-8、UTF-16 或 UTF-32 编码,其中在网络传输中最常见的就是 UTF-8。JSONCPP 的std::string内部保存的就是原始 UTF-8 字节序列,它不做任何编码转换。而 VC6.0 时代的 Windows 中文系统,本地代码页是 936(GBK/ANSI),cout输出字符串时直接按本地代码页解释,于是 UTF-8 字节被当成了 GBK 显示,中文自然变成“锟斤拷”“烫烫烫”这类经典乱码。
一句话总结:不是 JSONCPP 不支持中文,而是 UTF-8 和 GBK 之间需要一道转换桥梁。
4.2 核心转换函数:UTF-8 和 GBK 互转
Windows 上做编码转换,标准做法是走 Windows API 的两步法:先用MultiByteToWideChar把多字节编码转成 UTF-16 宽字符(wchar_t),再用WideCharToMultiByte把宽字符转成目标多字节编码。这个思路不限语言,C 和 C++ 均适用,而且跨版本兼容。
我封装的两个函数如下:
#include <windows.h> #include <string> // UTF-8字符串 转 GBK字符串 string Utf8ToGbk(const string& strUtf8) { int len = MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), -1, NULL, 0); if (len <= 0) return ""; wchar_t* wszGb2312 = new wchar_t[len + 1]; MultiByteToWideChar(CP_UTF8, 0, strUtf8.c_str(), -1, wszGb2312, len); wszGb2312[len] = L'\0'; int mblen = WideCharToMultiByte(CP_ACP, 0, wszGb2312, -1, NULL, 0, NULL, NULL); if (mblen <= 0) { delete[] wszGb2312; return ""; } char* szGb2312 = new char[mblen + 1]; WideCharToMultiByte(CP_ACP, 0, wszGb2312, -1, szGb2312, mblen, NULL, NULL); szGb2312[mblen] = '\0'; string strGbk(szGb2312); delete[] szGb2312; delete[] wszGb2312; return strGbk; } // GBK字符串 转 UTF-8字符串 string GbkToUtf8(const string& strGbk) { int len = MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, NULL, 0); if (len <= 0) return ""; wchar_t* wszUtf8 = new wchar_t[len + 1]; MultiByteToWideChar(CP_ACP, 0, strGbk.c_str(), -1, wszUtf8, len); wszUtf8[len] = L'\0'; int mblen = WideCharToMultiByte(CP_UTF8, 0, wszUtf8, -1, NULL, 0, NULL, NULL); if (mblen <= 0) { delete[] wszUtf8; return ""; } char* szUtf8 = new char[mblen + 1]; WideCharToMultiByte(CP_UTF8, 0, wszUtf8, -1, szUtf8, mblen, NULL, NULL); szUtf8[mblen] = '\0'; string strUtf8(szUtf8); delete[] szUtf8; delete[] wszUtf8; return strUtf8; }在这里,CP_UTF8指 UTF-8 代码页,CP_ACP指系统默认 ANSI 代码页,中文 Windows 下就是 GBK。函数末尾删除暂存 buffer,避免内存泄漏。这个版本我在 VC6.0 下编译过,没有 C 标准库新特性依赖,也能正常使用。
4.3 在 JSON 解析场景中的正确调用链
有了转换函数,完整的中文调用链应该是这样的:读取接口返回的 JSON(UTF-8)→reader.parse解析 → 从Value取出 UTF-8 字符串 → 需要显示时Utf8ToGbk转成 GBK → 输出到控制台或写进界面控件。
我的一个完整示例片段:
// 假设strJSON是接口返回的完整响应,内部包含中文 string strJSON = ReadAllContent("response.json"); Json::Reader reader; Json::Value root; if (!reader.parse(strJSON, root, false)) { cerr << "解析失败" << endl; return -1; } string nameUtf8 = root["data"]["name"].asString(); string nameGbk = Utf8ToGbk(nameUtf8); // 显示用GBK cout << "姓名: " << nameGbk << endl; // 如果后续要把中文拼入一条新的JSON请求,必须转回UTF-8 Json::Value req; req["queryName"] = nameUtf8; // 这里应该是UTF-8,不能传GBK Json::FastWriter writer; string reqStr = writer.write(req);注意上面的代码,req["queryName"]一定要填 UTF-8 版本的字符串,否则生成出的 JSON 字符串在别的系统解析时又乱码。转换方向必须和场景匹配,这是整个防乱码方案的核心逻辑。
4.4 处理控制台输出乱码的最后一公里
VC6.0 的控制台窗口有个恼人的默认问题:即使你把字符串转成了 GBK,cout依然可能显示乱码。原因在于控制台代码页和系统 ANSI 代码页有时候不一致,尤其是程序在 Windows 7 及以上版本运行时,默认控制台代码页可能是 437 或 936,取决于系统区域设置。
最简单粗暴的方案,是在程序初始化时调用SetConsoleOutputCP(CP_ACP),把控制台输出代码页强制切到系统 ANSI。
#include <windows.h> int main() { SetConsoleOutputCP(CP_ACP); // 让控制台和系统代码页一致 SetConsoleCP(CP_ACP); // 之后的cout输出GBK字符串显示正常 }操作顺序上要注意:先设置代码页,再执行任何cout输出。你在 main 函数开头写上这两行,后面所有调试输出都能避免再被“锟斤拷”击穿。
还有一种更古老的办法,是调用system("chcp 936"),它会直接修改控制台代码页。不过system会多弹出一个子进程开关,在正式发布程序里不推荐,我一般只在本地调试时用。
4.5 特殊字符和转义问题
JSONCPP 处理中文时,还有一类坑是转义。比如接口返回的 JSON 字符串中,中文可能被转义成了\u5f20\u4e09这种 Unicode 转义序列。JSONCPP 的Reader在解析时,会自动把\uXXXX转成 UTF-8 字节串,所以你在root["name"].asString()拿到的已经是正常的中文 UTF-8 了,不需要自己手动解码。这一点很多新手容易被误导,以为要自己解析\u前缀。
反过来,生成 JSON 时如果你手动拼字符串而不是用Json::Value,那中文字符会出现不转义的情况,JSON 规范里允许直接输出 UTF-8 中文,大多数接口都能接受。但如果对接方严格要求\uXXXX形式,你只能额外写一个转义函数。JSONCPP 0.5.0 自带的FastWriter不支持强制转义中文,这个需求只能自己实现。
5. 编译问题排查与避坑实录
5.1 链接报错:无法解析的外部符号
这是最多人遇到的问题。把json_reader.cpp加进工程后,链接时报unresolved external symbol "public: void __thiscall Json::Reader::parse(...)",基本可以确定是两个原因:要么实现文件没有真正参与编译(工程里没加入或文件被排除),要么JSONCPP_DISABLE_DLL宏没有定义导致Json::Reader被声明成了 dllimport。
检查方法不要太依赖 IDE,直接看工程目录中的.dsp文件,确认里面有没有SOURCE=..\src\json_reader.cpp这样的行。没有就手动加。宏定义在“Project Settings -> C/C++ -> Preprocessor -> Preprocessor definitions”里加,注意区分 Debug 和 Release 两种配置,不要只改一种。
5.2 编译期报错:缺少JSONCPP_DISABLE_DLL
编译json_value.cpp或json_reader.cpp时,如果报warning C4273: inconsistent dll linkage或者直接出C2039等错误,多半是因为json/config.h里判断导出的宏没定义好。直接把JSONCPP_DISABLE_DLL加进工程预处理器宏,清空_DEBUG和NDEBUG冲突项之后再试。这个宏是 0.5.0 版本特有的控制点,新版本已经改掉了,所以网上搜到的新版解决方案在 VC6 上不一定适用。
5.3 控制台中文乱码的多种呈现形态
这里整理一个对照表,方便快速定位乱码问题:
| 显示内容 | 可能原因 | 处理方式 |
|---|---|---|
| 一堆菱形和“锟斤拷” | 控制台代码页不是936 | 程序初始化调用SetConsoleOutputCP(CP_ACP) |
| 中文变成英文字母加反斜杠数字 | JSON里是转义序列,如\u5f20,且解析失败 | 确认解析是否真正成功,查看reader.getFormattedErrorMessages() |
| 源码里的中文字符串编译后显示乱码 | 源文件编码不是ANSI,VC6.0 误读 | 用 Notepad++ 把文件转成 ANSI 编码 |
| 从 JSONCPP 取出的字段传递给其他系统后乱码 | 传给对方的不是 UTF-8 而是 GBK | 检查生成请求 JSON 前是否误用了Utf8ToGbk |
这个表我建议贴在项目快速笔记里,遇到乱码问题先按表排查。绝大多数情况都逃不出这三种:文件编码、控制台代码页、转换方向反了。
5.4 数组和嵌套对象为空的问题
还有一类问题,解析没报错,但root["data"]拿到的是空对象。比如没有判断节点是否存在就直接调用asString,JSONCPP 0.5.0 在字段缺失时会返回一个“默认值”而不是抛异常,你debug 时很难看出问题。判断字段是否存在的正确姿势是:
if (root["data"].isObject()) { if (root["data"].isMember("name")) { string name = root["data"]["name"].asString(); } }先用isObject判断目标节点是否为对象,再用isMember判断字段是否存在,最后才取值。这套组合拳能避免绝大多数字段缺失导致的逻辑错误,同时也能顺带避免isnull误判。
6. 一点实操心得
回头复盘这个小项目,最值得记录的教训其实是:解决方案不复杂,复杂的是把各种环境因素叠加在一起时产生的迷之表现。VC6.0 本身是 98 年出生的老古董,JSONCPP 0.5.0 也是老代码,加上 Windows 编码体系这门玄学,任何一个环节松散都会导致整体崩盘。但一旦你理解了编码转换链,这套老组合反而很稳定,我现在已经把它封装成内部公共库,新项目遇到类似需求,基本一天之内就能交付。
最后再分享一个小技巧:如果你手头有好几个老工程都要用 JSONCPP,与其每个工程重复拷源码,不如把编译好的.lib文件归档起来。VC6.0 工程里配置好库搜索路径后,只需要在工程设置里填一个静态库名,后续维护量会小很多。这个做法我用了两年,稳定省心,推荐给你。
本文还有配套的精品资源,点击获取