简介:面向Visual C++ 6.0开发者的JSONCPP完整集成案例包,重点解决在老旧VC6.0环境下解析和生成JSON时中文乱码的难题。资源基于jsoncpp-src-0.5.0源码,无需预编译库文件,可直接将json_value.cpp、json_reader.cpp、json_writer.cpp等加入工程。包内共72个文件,包含24个头文件、9个cpp源文件、6个inl内联实现、6个obj/sbr编译中间文件,以及2份doc说明和工程配置所需的dsp/dsw/rc等,压缩包整体约3.77MB。除源码与可执行exe外,特别收录《【重要】VC6.0 测试通过的JSONCPP源码类使用说明.doc》和必看.txt,逐一讲解如何集成源码、规避StdAfx包含顺序问题、通过UTF-8编码转换防止中文乱码,并给出可直接运行的对话框测试程序,方便对照修改。目前已有587人学习下载,适合需要在VC6.0中快速接入JSONCPP并处理中文数据的开发者参考。
1. 项目背景:为什么还在VC6.0的老项目里接JSONCPP
说句实在话,第一次听到“VC6.0调用JSONCPP”这个需求时,我第一反应是:都什么年代了,还在用二十多年前的集成开发环境?但真接手过后才发现,这绝不是个案。工业设备上位机、老式检测仪器、银行柜面系统,甚至一些军工配套软件,底层核心逻辑都是VC6.0时代写出来的,跑了十几年稳定得不得了,你说推倒重写?那代价不是一般公司愿意承担的。尤其是当老系统需要对接新平台下发的数据时,JSON几乎成了绕不开的格式:配置中心下发策略、MES系统回传工单、设备状态上报、远程指令下发……新系统清一色吐JSON,老系统根本没法直接吃。
那有人问了,VC6.0能不能不接JSON,自己拼字符串不就行了?还真不行。JSON有嵌套结构、数组、转义字符、Unicode编码,手写解析器短平快场景还能凑合,一旦字段变了、层级多了、数据量上来了,你会被边界条件活活折磨死。这也是为什么需要引入一个成熟的JSON解析库。在VC6.0环境下,能用的开源解析库其实就那么几个:cJSON(纯C语言,轻量但功能弱)、json-c(Linux系出身,VC6下编译费劲)、JSONCPP(功能全、STL风格、VC6可用)。综合下来,JSONCPP是兼容老编译器的最佳选择。
但这里有一个致命坑点:JSONCPP版本差异极大。新版本的JSONCPP(1.8.x和1.9.x)大量使用了C++11甚至C++14特性——auto关键字、std::unique_ptr、override、移动语义等等——VC6.0根本编译不过去。如果你直接跑到GitHub上下载最新源码然后往VC6项目里拖,睁开眼就是一屏报错。所以“无措版”的前提,首先就是选对版本。
我这次用的是jsoncpp 0.5.0,在VC6.0上实测能编能跑,配合正确的编码转换函数,解析含中文的JSON数据不会出现乱码,整个过程走的弯路和踩过的坑,下面一个一个说清楚。
2. 编译前准备:版本选型与工程配置
2.1 拿到正确版本:jsoncpp 0.5.0的获取与目录结构
如果你搜索“jsoncpp 0.5.0下载”,会找到很多源码包,但务必去官方源或可信镜像站拿,避免下载到被篡改的文件。0.5.0的源码包解压后,核心目录是include/json和src,前者放头文件,后者放实现。实际工程中,你只需要把include/json目录下的头文件拷到项目里,把src目录下的json_reader.cpp、json_value.cpp、json_writer.cpp这三个文件添加到VC6工程中即可。
这里有个容易踩的坑:VC6.0的C++标准支持不完整,jsoncpp 0.5.0源码里部分文件使用了一些比较老练但VC6能接受的写法,不要擅自用新版代码替换某个文件,否则会引入不兼容。我在第一次尝试时想着“既然0.5.0的reader太旧,不如把新版reader文件混进来”——结果编译直接报unrecognized template declaration/definition,折腾了半个下午才意识到新旧文件不能混用。
2.2 VC6工程配置:字符集、头文件路径和编译选项
新建或打开VC6工程后,依次点击Project -> Settings -> C/C++页签,在Preprocessor definitions里保证已有WIN32;_DEBUG;_CONSOLE这几项,不需要额外定义_UNICODE或UNICODE。为什么要强调这个?因为VC6默认支持多字节字符集(MBCS),如果你的工程不小心开了Unicode,后面调用MultiByteToWideChar和WideCharToMultiByte时,接口参数会有一堆TCHAR转换的麻烦,代码反而更繁琐。做中文JSON解析,工程使用多字节字符集即可,配合Windows API做编码转换,这是最终能跑通的关键组合。
接着在Preprocessor下面找到Additional include directories,填入你放置json头文件的路径。比如我把json文件夹直接放到了工程根目录,那这里填.\就行。如果填错路径,编译时会报fatal error C1083: Cannot open include file: 'json/json.h': No such file or directory,这属于最基础的头文件路径问题。
最后要注意VC6的编译标准是C++98(甚至还没完全实现),所以代码里不要出现for(auto it : xxx)这种写法,老老实实用迭代器。也别用std::unique_ptr,用裸指针加手动delete反而更稳妥。记住,在老环境里,朴素就是最大的兼容性。
3. VC6.0调用JSONCPP全案例:解析、遍历、构造
3.1 基础解析:把JSON字符串变成Json::Value对象
拿到一段JSON字符串,第一步是解析成可操作的对象。jsoncpp 0.5.0的解析入口是Json::Reader加Json::Value,典型代码如下:
#include "json/json.h" #include <string> #include <iostream> bool ParseJsonString(const std::string& jsonStr, Json::Value& root) { Json::Reader reader; bool success = reader.parse(jsonStr, root); if (!success) { // 获取并打印详细的错误信息,便于定位问题 std::string errMsg = reader.getFormatedErrorMessages(); std::cerr << "JSON parse failed: " << errMsg << std::endl; } return success; }注意这里有个细节:在较新版本的jsoncpp中,getFormatedErrorMessages()函数改名为getFormattedErrorMessages(),多了一个字母“t”,但在0.5.0版本里,方法名是getFormatedErrorMessages(少一个t)。如果你从网上抄代码,容易抄到新版写法,在0.5.0下编译会报方法不存在,这也是典型的“版本混搭”问题。
解析成功后,读取字段值:
Json::Value nameField = root["name"]; std::string name = nameField.asString();asString()返回std::string,但在VC6.0中,如果你把std::string直接通过cout输出,遇到中文大概率乱码,这是因为jsoncpp解析时会假定JSON是UTF-8编码,而std::string只是字节容器,编码本身不受保护。后面第四节详细讲怎么处理。
3.2 嵌套对象和数组的遍历
实际的JSON结构很少是纯平铺的,更多是对象套对象、对象套数组。完整案例代码:
// 假设JSON数据如下: // {"status": 200, "data": {"count": 2, "items": [{"name":"张三","age":18}, {"name":"李四","age":20}]}} Json::Value root; if (!ParseJsonString(jsonStr, root)) return; int status = root["status"].asInt(); int count = root["data"]["count"].asInt(); Json::Value items = root["data"]["items"]; std::cout << "items size = " << items.size() << std::endl; for (int i = 0; i < (int)items.size(); i++) { Json::Value item = items[i]; std::string name = item["name"].asString(); int age = item["age"].asInt(); // 这里name是UTF-8字节串,显示前必须做编码转换 std::cout << "item " << i << ": name=" << name << ", age=" << age << std::endl; }有几个细节需要注意。第一,items.size()返回的是unsigned int,在VC6下和int比较会有符号警告,建议强制转换。第二,遍历数组时,items[i]返回的是一个Json::Value引用,但如果你写的循环变量不是引用类型,它会拷贝一份,性能稍差但功能没问题,老编译器下不追求这个。第三,如果某个字段在JSON中不存在,调用asString()等函数会返回默认值(空字符串或0),并不会抛异常,这在开发调试阶段经常导致“查了半天才发现是字段名拼错了”的低级问题。
3.3 构造JSON并生成字符串
除了解析,更多场景是程序需要主动构造一个JSON,然后发送到远端。用jsoncpp构造JSON非常直观:
Json::Value root; root["cmd"] = "device_report"; root["device_id"] = 10001; root["timestamp"] = 1699999999; Json::Value reportData; reportData["cpu_usage"] = 32.5; reportData["memory_usage"] = 64.8; root["data"] = reportData; // 生成紧凑格式JSON字符串 Json::FastWriter fastWriter; std::string outJson = fastWriter.write(root); // 生成带缩进的美化格式 Json::StyledWriter styledWriter; std::string prettyJson = styledWriter.write(root);FastWriter和StyledWriter是jsoncpp 0.5.0的两个写器类。write()返回的字符串末尾自带一个换行符,这个细节在实际联调时容易忽略——发送包体时长度计算要留意,不然对方解析可能因为多了一个换行而产生异常。如果想去掉末尾换行,可以用outJson.substr(0, outJson.length() - 1)。
构造含中文的JSON时,最稳妥的做法是先准备好UTF-8编码的中文字符串,再赋给Json::Value。如果你手里是GBK/ANSI编码的字符串,直接赋值会导致生成的JSON里出现非法UTF-8字节序列,别人解析时就会看到乱码或直接报错。所以构造一方也需要做编码转换,方向跟解析时正好相反。
4. 中文防乱码:从底层原理到完整方案
4.1 乱码根源:UTF-8与GBK的“鸡同鸭讲”
要彻底解决乱码,必须先弄清楚乱码是怎么来的。JSON标准明确规定,JSON文本必须使用UTF-8编码。也就是说,对方发给你的JSON字符串,里面的中文字符是以UTF-8字节序列存放的。举例来说,汉字“张”,在UTF-8下是三个字节E5 BC A0,而在GBK(Windows中文版默认的ANSI代码页)下,它是两个字节D5 C5。
VC6.0时代的程序默认使用GBK编码,也就是说,如果你直接用cout输出std::string变量,你的终端或日志窗口会按GBK去解释字节序列。现在jsoncpp把UTF-8的三个字节E5 BC A0原封不动地塞进std::string,输出时被GBK解读,就变成了乱码“寮犱笁”之类的东西。根子就在于两边用的字节解释规则不一致。
解决思路一句话:既然JSON和jsoncpp都要求UTF-8,那我们在解析后,把UTF-8字符串转换为系统当前代码页(GBK)再交给界面显示;在构造JSON前,把GBK字符串转换为UTF-8再赋给Json::Value。
4.2 核心转换函数:让中文在两个世界之间自由流转
Windows提供了两对API:MultiByteToWideChar和WideCharToMultiByte。转换路径是:UTF-8 ->MultiByteToWideChar(CP_UTF8, ...)-> 宽字符 ->WideCharToMultiByte(CP_ACP, ...)-> GBK,反方向同理。完整的可复用代码:
// UTF-8转ANSI(Windows多字节代码页,通常为GBK) std::string Utf8ToAnsi(const std::string& utf8Str) { if (utf8Str.empty()) return ""; int wLen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, NULL, 0); if (wLen <= 0) return utf8Str; // 转换失败时原样返回,便于排查 wchar_t* wBuf = new wchar_t[wLen]; MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, wBuf, wLen); int aLen = WideCharToMultiByte(CP_ACP, 0, wBuf, -1, NULL, 0, NULL, NULL); if (aLen <= 0) { delete[] wBuf; return utf8Str; } char* aBuf = new char[aLen]; WideCharToMultiByte(CP_ACP, 0, wBuf, -1, aBuf, aLen, NULL, NULL); std::string result(aBuf); delete[] wBuf; delete[] aBuf; return result; } // ANSI转UTF-8 std::string AnsiToUtf8(const std::string& ansiStr) { if (ansiStr.empty()) return ""; int wLen = MultiByteToWideChar(CP_ACP, 0, ansiStr.c_str(), -1, NULL, 0); if (wLen <= 0) return ansiStr; wchar_t* wBuf = new wchar_t[wLen]; MultiByteToWideChar(CP_ACP, 0, ansiStr.c_str(), -1, wBuf, wLen); int uLen = WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, NULL, 0, NULL, NULL); if (uLen <= 0) { delete[] wBuf; return ansiStr; } char* uBuf = new char[uLen]; WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, uBuf, uLen, NULL, NULL); std::string result(uBuf); delete[] wBuf; delete[] uBuf; return result; }注意几个细节:第一,MultiByteToWideChar的第四个参数传-1时,函数会根据源字符串的\0自动判断长度,并在目标缓冲区末尾自动补上\0,因此我们new出来的缓冲区长度是包含结尾空字符的。第二个注意点是std::string result(aBuf)这样的构造方式,如果字符串中间有\0会用截断,但正常的中文文本不会出现这种问题,实际测试没问题。第三,转换失败时原样返回字符串,不是为了假装成功,而是为了让你在调试时一眼看出“这个字符转不了”,方便定位脏数据。
使用方式很简单。解析时:
std::string rawName = root["name"].asString(); std::string displayName = Utf8ToAnsi(rawName); std::cout << "name = " << displayName << std::endl; // 不乱码构造时:
std::string gbkName = "张三"; std::string utf8Name = AnsiToUtf8(gbkName); root["name"] = utf8Name;这样一转换,两边的中文就都能正常显示了。我实测在VC6.0的Win32控制台程序里,用cout输出转换后的中文,控制台正确显示“张三”“李四”,不再出现乱码;把Utf8ToAnsi换成printf输出也是一样的效果。
5. 常见报错与排查技巧实录
5.1 编译阶段报错:老编译器与新代码的拉锯战
编译jsoncpp源码时最常遇到的报错,我归了一下类,方便你在自查时快速定位:
| 报错特征 | 可能原因 | 解决方案 |
|---|---|---|
fatal error C1083: Cannot open include file: 'json/json.h' | 头文件路径没配置 | 在Project / Settings / C/C++ / Preprocessor / Additional include directories中添加json头文件所在目录 |
error C2039: 'getFormatedErrorMessages' : is not a member of 'Json::Reader' | 用了新版本的API调用方式,或混杂了新版源码 | 确认使用的是jsoncpp 0.5.0,并使用旧版类名getFormatedErrorMessages |
error C2065: 'auto' : undeclared identifier | 代码中使用了C++11的auto关键字 | 把auto替换为显式类型,或者用迭代器 |
error LNK2001: unresolved external symbol _main | 工程类型选错(建成了Windows应用而不是控制台程序) | 在Project / Settings / Link / Output / Subsystem中选Console,并确认有main函数 |
error C2248: 'std::basic_string...' : cannot access private member | VC6的STL版本太老,和某些代码不兼容 | 尽量使用std::string源码中的char*字符串直接操作,不要过度使用STL高级特性 |
5.2 乱码问题:转换函数加了为何还是乱
最常见的情况是,有人把转换函数加上了,但发现中文字符虽然不像之前那么“天书”,却变成了类似“???”的字符,或者干脆空了。这多半是下面几种原因:
第一,源数据本身不是UTF-8。比如对方虽然声称发的是JSON,但实际内部用GBK生成了字符串(很多国内老系统就这么干),你的jsoncpp解析没问题,但asString()拿到的字节是GBK的,你再按UTF-8转ANSI,等于把GBK当成UTF-8来解释,自然会失败。排查方法:把拿到的原始字节以十六进制打印出来,看看汉字对应的字节序列长度——UTF-8汉字固定三字节,GBK汉字固定两字节,一眼就能分辨。
第二,转换函数里缺少失败保护或返回了原始字符串,掩盖了问题。我在函数里故意设计了转换失败时原样返回,这个设计是有意为之——你要看到字符串没有变化,就知道是源数据编码有问题,而不是转换逻辑有bug。如果你把失败的返回改成空字符串,排查起来就会难得多。
第三,控制台窗口本身的代码页问题。就算你在程序里转换对了,如果控制台代码页不是GBK(例如某些系统默认是936即GBK,但有的精简版系统或Windows英文版控制台是437),显示还是会错乱。解决方法是程序开头调用SetConsoleOutputCP(936)强制设置控制台代码页为GBK,或者使用SetConsoleOutputCP(CP_UTF8)配合/utf-8编译选项去统一。但VC6.0对/utf-8编译选项支持不好,所以最终的推荐组合:程序内转换到GBK +SetConsoleOutputCP(936),双保险。
5.3 链接报错:源文件没加全或lib库缺失
jsoncpp 0.5.0使用源码方式集成不需要额外lib文件,你只需要确保json_reader.cpp、json_value.cpp、json_writer.cpp三个文件都在工程里。如果漏了任何一个,链接时会报unresolved external symbol错误,而且报错信息中函数名会让你看得一头雾水。我的排查方法是:在FileView中检查Source Files下有没有这三个文件,没有就手动添加。
另一个链接期怪问题是重复定义:有时候你把jsoncpp的lib文件也链接进来,同时又添加了源码,就会出现LNK2005重复定义错误。要么源码,要么预编译lib,二选一,别混着来。VC6下我推荐纯源码方式,省去一堆路径配置。
6. 实际使用中的几个心得
整个项目做下来,我最深刻的体会是:老环境不等于不能用,关键是知道哪些坑不能踩。VC6.0虽老,但它的编译产物在新版Windows(从Win7到Win11)上稳定运行了这么多年,可靠性早已被验证过了。相比之下,新版IDE编译出来的程序反而时不时因为兼容性问题被安全软件误报,当然这是后话。
还有两个小技巧,直接分享给你们。第一个,VS6的IDE在编辑中文注释时会偶尔出现光标错乱的现象,很多人以为是系统问题,其实只要把源文件用带BOM的UTF-8保存,问题就解决了大半。第二个,如果你在使用jsoncpp构造JSON时发现StyledWriter输出的字符串包含了中文字符的原始UTF-8字节,不要慌,这是正常现象——对方解析时能正确读出,你本地如果想预览可读性好的JSON,建议用一个支持UTF-8的编辑器(比如Notepad++)打开检查,而不是在VC6的简陋控制台里较劲。
回到这套方案的“无措版”定位上,我实际在VC6.0 + Windows 7 + Windows 10两种系统环境、以及一个从Win XP升级上来的老工业触摸屏程序里,完整验证过。解析含中文的配置JSON、构造含中文的上报JSON、数组遍历、异常处理,所有路径都跑通,一次编码问题都没再出现。如果你们项目上遇到类似的老系统对接新协议需求,按照这个流程做,大概率一天之内就能跑通全部功能。
本文还有配套的精品资源,点击获取