☰
VC6.0调用JSONCPP中文乱码解决方案:UTF-8与GBK转换实战
2026/10/8 14:38:16 网站建设 项目流程

简介:面向VC6.0开发者的JSONCPP集成与中文防乱码完整案例包,基于jsoncpp-src-0.5.0源码,演示在Visual C++ 6.0的Win32控制台与对话框工程中如何配置、编译和调用JSONCPP,重点解决中文解析与序列化时常见的乱码问题,适合需要维护旧版C++项目或正在学习JSON数据格式处理的初中级开发者。资源内含可直接导入的VC6.0工程文件(.dsp/.dsw)、全部核心源码(.h/.cpp/.inl)、编译中间文件以及已生成的exe示例,并附有《【重要】VC6.0 测试通过的JSONCPP源码类使用说明.doc》、“必看.txt”和ReadMe等文档,从编码设置、源码集成到运行验证给出完整说明。压缩包共72个文件,涵盖头文件、C++实现、内联模板、对话框资源、调试信息及多种说明文档,整体仅3.77MB,轻量易用。目前已有590人学习浏览,适合在老旧VC6.0环境中快速接入JSONCPP并实现中文兼容的C++开发者和遗留项目维护者。通过对照案例工程与文档,可以直接获得可运行的解析中文JSON测试程序、常见错误排查思路和编码处理技巧,减少反复试错成本。

1. VC6.0 调用 JSONCPP:老编译器里为什么也在折腾 JSON 中文编码

VC6.0 调用 JSONCPP 做 JSON 解析,最折磨人的不是库本身,而是中文防乱码。系统还是老的,机器跑着 XP,VC6.0 的界面黄得像上个世纪——但项目还得继续维护,对面新系统的接口也还是 JSON。问题就出在字符集上:VC6.0 默认的源代码和字符串都按 GBK 处理,而 JSONCPP 解析出来的字符串一律是 UTF-8 字节,两边不对齐,中文就变成“鈥濄涓”这类鬼字样。这不是 JSONCPP 的锅,是 VC6.0 的字符集和 JSON 标准之间的冲突。这篇文章把 VC6.0 环境下调用 JSONCPP 的完整方案讲透:怎么选库、怎么加进工程、怎么解析中文、怎么生成中文 JSON,以及 5 个最常见的报错和乱码教训。适合维护老项目、要把历史 C++ 程序接入新 JSON 接口、又不想升级编译环境的开发者。照着复现,能少走一个月的弯路。

2. 编译准备:把 JSONCPP 源文件塞进 VC6.0 工作区的两种接法

2.1 选哪个版本:老版本源码包优先

VC6.0 是 1998 年的产品,对 C++ 标准的支持停在 C++98 甚至更早。最新版 JSONCPP 用了大量 C++11 特性,VC6.0 编译直接就是一堆“fatal error C1001”或者“语法错误”。所以下载 jsoncpp 库的时候,目标不是 master 分支,而是 0.x 系列的老版本,比如 0.6.0 这类。常见做法是去 SourceForge 的历史版本页,或者 GitHub 仓库的 Tags 列表里找旧标签,不要用所谓的“最新稳定版”。老版本源码包目录很清楚:include/json/下放头文件,src/lib_json/下是三个.cpp文件,没有 CMake 依赖,也省去了生成库的麻烦。

另一个重要理由:老版本 JSONCPP 不依赖 Boost,只用了标准库和 Windows API,对 VC6.0 的模板支持比较友好。新版源码里出现了std::unique_ptr、std::move、std::is_constructible这些东西,VC6.0 根本不认识。也有同行用“jsoncpp-master 改一版硬编”的做法,但结果一般是改了几十个编译错误,最后连自己加的改动在哪都找不到。我一般不建议这么干,VC6.0 不是用来编译现代 C++ 的,硬上不划算。

2.2 把源码加入 VC6.0 工程的最小步骤

拿到源码包后,我这里给出最踏实的一套操作,不生成库,直接把源码编进你的主工程。

第一步,把 jsoncpp 目录拷贝到你的工程目录下,比如丢到third_party/jsoncpp。第二步,在 VC6.0 的 FileView 里右键“Source Files”,选“Add Files to Folder”,把src/lib_json下的json_reader.cpp、json_writer.cpp、json_value.cpp加进去。第三步,打开 Project -> Settings -> C/C++ -> Preprocessor,在“Additional include directories”里填上..\third_party\jsoncpp\include,注意路径要配到包含json/json.h的那个目录。第四步,确认 C/C++ -> General 里 Debug 和 Release 的“Warning level”不要设成“Level 4”,VC6 对模板实例化的警告很多,设成 Level 3 就够,省得刷屏。

文件作用
json_reader.cpp实现 Json::Reader,负责把字符串或流解析成 Value
json_writer.cpp实现 Json::FastWriter 和 Json::StyledWriter,负责序列化
json_value.cpp实现 Json::Value 的存储、索引、类型转换

加完之后写一个最小测试,验证编译和链接能通。

#include <stdio.h> #include "json/json.h" int main() { const char* testJson = "{\"code\":0,\"msg\":\"ok\"}"; Json::Reader reader; Json::Value root; if (reader.parse(testJson, root)) { // asInt 读取整数,asString 读取字符串 int code = root["code"].asInt(); std::string msg = root["msg"].asString(); printf("code=%d, msg=%s\n", code, msg.c_str()); } else { printf("parse failed\n"); } return 0; }

注意这里没有中文,就为了先跑通环境。reader.parse接受const char*,内部会把字符串内容复制进Value,所以不用担心testJson是常量导致生命周期问题。root["code"]返回的是Json::Value的引用,asInt()做类型转换;如果键不存在,返回默认值 0 而不是报错,这是 JSONCPP 老版本的行为,新手容易误判,后面避坑章再细说。asString()返回std::string,在 VC6.0 里务必.c_str()再传给printf,千万别把std::string直接给%s,那是未定义行为。

2.3 用静态库方式接法:适合多个工程共用

如果公司里有好几个 VC6.0 工程都要调用 JSONCPP,每个工程都塞三个.cpp会重复编译,比较蠢。另一个接法是单独建一个 Win32 Static Library 工程,把三个.cpp编译成一个jsoncpp_vc6.lib,其他工程只链接这个 lib。

具体操作:在 VC6.0 里新建工程,选“Win32 Static Library”,把三个.cpp加进去,编译生成 lib。然后调用方在 Project -> Settings -> Link -> Object/library modules 里输入jsoncpp_vc6.lib,再在 Link -> Input 的“Additional library path”里填 lib 所在目录。头文件路径还是要配,方法跟 2.2 一样。

这里有一个必须盯死的参数:C/C++ -> Code Generation -> Use run-time library。VC6.0 里 Debug 工程默认是“Debug Multithreaded”(对应/MTd),Release 默认是“Multithreaded”(对应/MT)。你用静态库方式时,lib 工程和调用工程的这个选项必须完全一致。如果 lib 用/MTd,调用工程用/MDd,链接阶段几乎必然报 LNK2005,后面避坑里细讲。源码直接加入主工程的方式就无所谓,因为源文件跟着主工程的参数编译,天然一致。

2.4 VC6.0 编译 JSONCPP 的报错速查

编译老版本 JSONCPP 时,最常见的是下面几类:

错误现象原因处理
fatal error C1083: Cannot open include file: 'json/json.h'头文件路径没配检查 Additional include directories
error C2065: 'string' : undeclared identifier没包含<string>或编译器不自动带在代码里#include <string>
error C2664: cannot convert parameter 2 from ...VC6 对隐式类型转换极严格显式调用.c_str()或强转类型
warning C4251: ... needs to have dll-interface模板类跨 DLL 导出的警告不必理会,这是 VC6 时代的老提示

如果遇到编译错误发生在 jsoncpp 的源码内部,比如说“C2910”或“Cannot open include file: 'config.h'”,那说明你下错版本了。老版本源码包有一个json/config.h在include/json目录下,如果缺,多半是从新版仓库里拷的头文件。直接回炉重找完整源码包,别在残缺文件上浪费时间。这一步是“无错版”的地基,地基歪了,后面全白搭。

3. 解析中文防乱码:UTF-8 与 GBK 互转的两种落地写法

3.1 乱码根源:JSON 的 UTF-8 和 VC6.0 的 GBK 默认字符串

JSON 标准 RFC 8259 明确规定,JSON 文本必须用 UTF-8 编码。JSONCPP 严格遵守,所以从 JSON 里解析出来的std::string就是 UTF-8 字节。而 VC6.0 在中文 Windows 下,char默认按多字节字符集处理,运行时的printf、Win32 的MessageBoxA都按系统代码页 936(GBK)解释字符串。两边一对接,就出问题。

举一个具体的例子:汉字“张”的 UTF-8 编码是E5 BC A0三个字节,这三个字节被 GBK 解码时,E5 BC正好是一个生僻汉字“宷”的编码,A0又是一个控制符,最后显示成“宷”或空白,后面几个字也连锁错位。反过来,如果直接把 GBK 字符串塞给 JSONCPP,生成的文件里写的是 GBK 字节,不是合法 UTF-8,服务器或其他下游程序读了会直接报非法字节序列。

所以不要试图绕开编码问题。JSONCPP 内部只认 UTF-8,业务显示层只认 GBK,二者必须通过显式转码搭桥。谁在中间偷懒,谁就等着在测试现场被领导围观乱码。

3.2 UTF-8 转 GBK 的 Win32 转码函数

最稳妥的做法是借助 Windows APIMultiByteToWideChar和WideCharToMultiByte。思路是先把 UTF-8 解码成 UTF-16 宽字符,再把宽字符编码成 GBK。VC6.0 里wchar_t在 Windows 平台下就是 16 位,正好对应 UTF-16 的代码单元。

// utf8_gbk.h #ifndef UTF8_GBK_H #define UTF8_GBK_H #include <string> #include <windows.h> // UTF-8 转 GBK,失败时返回原字符串,避免返回空串造成二次问题 inline std::string Utf8ToGbk(const std::string& utf8) { if (utf8.empty()) return ""; // 第一步:UTF-8 -> UTF-16 int wLen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0); if (wLen <= 0) return utf8; wchar_t* wBuf = new wchar_t[wLen]; MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wBuf, wLen); // 第二步:UTF-16 -> GBK int gbkLen = WideCharToMultiByte(CP_ACP, 0, wBuf, -1, NULL, 0, NULL, NULL); if (gbkLen <= 0) { delete[] wBuf; return utf8; } 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; } #endif

参数说明:CP_UTF8是代码页 65001,也就是 UTF-8;CP_ACP是系统默认 ANSI 代码页,中文系统就是 936。函数里用-1表示输入字符串以\0结尾,让 API 自动处理结尾的终止符;new wchar_t[wLen]得到的缓冲区容量足够放下 UTF-16 字符串及其\0结束符。这里手动new/delete是为了兼容 VC6.0 对std::wstring薄弱的模板实现,虽然丑,但稳定。

调用方式就简单了:

Json::Reader reader; Json::Value root; const char* jsonText = "{\"name\":\"\\u5f20\\u4e09\"}"; // 等价于 UTF-8 的“张三” if (reader.parse(jsonText, root)) { std::string utf8Name = root["name"].asString(); // UTF-8 字节 std::string gbkName = Utf8ToGbk(utf8Name); // 转成 GBK 用于显示 MessageBoxA(NULL, gbkName.c_str(), "结果", MB_OK); }

注意root["name"].asString()返回的std::string里的字节。转换后的gbkName传给 ANSI 版本的 Win32 API 才能显示中文,别用MessageBoxW,除非你的工程配置了 Unicode 字符集且把所有交互相都改成宽字符。大多数老工程还是多字节集,用 A 系 API 最省事。

3.3 GBK 转 UTF-8 的函数:构造 JSON 前必须过一遍

写 JSON 文件时,方向相反。你的源代码保存成 GBK 编码,字符串字面量“张三”在编译后就是 GBK 字节。直接赋给Json::Value,JSONCPP 会原封不动地把这些字节写进 JSON 文件,文件就不是合法 UTF-8。解决办法是在赋值前把 GBK 转成 UTF-8。

// 继续写在 utf8_gbk.h 里 inline std::string GbkToUtf8(const char* gbk) { if (gbk == NULL || gbk[0] == '\0') return ""; // 第一步:GBK -> UTF-16 int wLen = MultiByteToWideChar(CP_ACP, 0, gbk, -1, NULL, 0); if (wLen <= 0) return gbk; wchar_t* wBuf = new wchar_t[wLen]; MultiByteToWideChar(CP_ACP, 0, gbk, -1, wBuf, wLen); // 第二步:UTF-16 -> UTF-8 int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, NULL, 0, NULL, NULL); if (utf8Len <= 0) { delete[] wBuf; return gbk; } char* utf8Buf = new char[utf8Len]; WideCharToMultiByte(CP_UTF8, 0, wBuf, -1, utf8Buf, utf8Len, NULL, NULL); std::string result(utf8Buf); delete[] utf8Buf; delete[] wBuf; return result; }

这里两个 API 的第一个参数和前面正好对调:从 GBK 到 UTF-8,第一步用CP_ACP,第二步用CP_UTF8。很多人写反,结果越转越乱。返回值里我写成return gbk;而不是return "";,是因为如果某个字无法转换,至少把原始内容传递出去,调试时问题容易暴露,不至于静默变成空字符串。

赋值时这样写:

Json::Value root; root["name"] = GbkToUtf8("张三"); // 源文件里是 GBK 字面量,转成 UTF-8 再给 JSONCPP root["city"] = GbkToUtf8("北京");

这样写出的 JSON 文件会是标准 UTF-8,不依赖 JSONCPP 的内部编码,也不依赖 Windows 的代码页开关。

3.4 什么时候转:两条边界法则

核心原则可以压缩成两句话:进入 JSONCPP 之前必须是 UTF-8,从 JSONCPP 出来转成 GBK 再交给 Win32 控件或控制台。这叫“内部保持 UTF-8,外部按 GBK 落地”。

具体到代码里,我习惯把转码调用放在两个地方:一是从Json::Value取字符串后、准备printf或MessageBoxA之前,调用Utf8ToGbk;二是准备要给Json::Value赋值前,把 GBK 字符串先过GbkToUtf8。千万不要尝试改 JSONCPP 源码,让它内部用 GBK 存储字符串。JSONCPP 的序列化、\uXXXX解析、键名排序全部建立在 UTF-8 字节流上,你改了存储,writer 和 reader 全会错位,那是无底洞。

验证这个回路是否正确,用一个完整的读写测试:

// 构造并写出 Json::Value v; v["user"] = GbkToUtf8("王工"); std::string jsonText = Json::FastWriter().write(v); FILE* f = fopen("out.json", "wb"); // 二进制写,避免换行被替换 fwrite(jsonText.c_str(), 1, jsonText.length(), f); fclose(f); // 读回来再显示 FILE* f2 = fopen("out.json", "rb"); char buf[1024] = {0}; fread(buf, 1, sizeof(buf) - 1, f2); fclose(f2); Json::Value r; Json::Reader().parse(buf, r); // 从 Value 拿的是 UTF-8,显示前转 GBK printf("读出来:%s\n", Utf8ToGbk(r["user"].asString()).c_str());

如果输出“王工”正常,说明编码链路是通的。如果你看到“鐜嬪”,说明Utf8ToGbk没有生效,或读文件时已经解码出错。

4. 全案例演练:解析、生成、改键值、嵌套 JSON 的完整代码

4.1 案例一:读取 UTF-8 JSON 文件并打印中文

第一个案例解决“从一个 JSON 文件里读数据”这个最普遍的需求。读取文件时,我不用std::ifstream的迭代器构造字符串,那个写法在 VC6.0 的旧标准库里有坑——窄字符流的istreambuf_iterator在实例化时容易崩溃或读到空内容。无错版的稳定做法是 C 风格fopen+fread,一字节一字节用量来读,绝不多读。

#include <stdio.h> #include <string.h> #include "json/json.h" #include "utf8_gbk.h" // 以二进制方式读入 UTF-8 文件,完整传给 JSONCPP bool ReadJsonFile(const char* path, Json::Value& root) { FILE* f = fopen(path, "rb"); if (NULL == f) return false; // 定位到文件末尾,拿长度 fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); // 多给 1 字节,保证字符串终止符合法 char* buf = new char[len + 1]; if (len > 0) { size_t rd = fread(buf, 1, len, f); buf[rd] = '\0'; } else { buf[0] = '\0'; } fclose(f); bool ok = Json::Reader().parse(buf, root); delete[] buf; return ok; } int main() { Json::Value root; if (!ReadJsonFile("user.json", root)) { printf("读取或解析失败\n"); return 1; } // asString 返回 UTF-8 字节 std::string utf8Name = root["name"].asString(); // 转成 GBK 再交给 MessageBoxA std::string gbkName = Utf8ToGbk(utf8Name); MessageBoxA(NULL, gbkName.c_str(), "姓名", MB_OK); return 0; }

这里fread的返回值rd可能小于len,尤其是网络盘或特殊设备,所以终止符一定要用buf[rd],而不是固定buf[len]。Json::Reader().parse(buf, root)是临时对象调用成员函数,VC6.0 支持这种写法,返回bool表示解析是否成功。解析成功后,buf可以立刻释放,因为Json::Value已经复制了内容。

如果user.json是 Windows 记事本保存的 UTF-8 带 BOM 文件,这个案例会失败,原因和解决办法在避坑章 5.3 给出。

4.2 案例二:构造含中文的 JSON 并写回文件

第二个案例是把程序里的配置或结果输出成 JSON 文件。先看关键代码,重点在写文件前的中文转码。

#include <stdio.h> #include "json/json.h" #include "utf8_gbk.h" bool SaveJsonFile(const Json::Value& root, const char* path) { // FastWriter 输出紧凑格式,StyledWriter 输出带缩进的格式 Json::StyledWriter writer; std::string output = writer.write(root); // 已经是 UTF-8 字节流 FILE* f = fopen(path, "wb"); // 二进制写,避免 \n 被替换成 \r\n if (NULL == f) return false; fwrite(output.c_str(), 1, output.length(), f); fclose(f); return true; } int main() { Json::Value root; root["name"] = GbkToUtf8("张三"); // GBK 字面量转 UTF-8 root["age"] = 18; root["address"]["city"] = GbkToUtf8("北京"); // 嵌套对象,直接用 [""] 创建 // 修改键值:直接再赋一次 root["age"] = 19; if (SaveJsonFile(root, "out.json")) { printf("保存成功,文件编码为 UTF-8 无 BOM\n"); } return 0; }

Json::StyledWriter生成带缩进的文本,方便人检查;Json::FastWriter生成无空格的紧凑文本,适合接口传输。老版本里FastWriter会把非 ASCII 字符转义成\uXXXX,所以如果你用紧凑格式打开out.json,看到的可能是"name":"\u5f20\u4e09",这个不是乱码,后面第 5 章会单独说怎么让它输出原始中文。root["address"]["city"]这种方式会自动创建address这个嵌套对象,不需要提前声明,这是 JSONCPP 很方便的设计。修改键值就是重新赋值,简单粗暴。

写文件用"wb"而不是"w",很重要。文本模式下 VC6.0 的fwrite会把每一个\n替换成\r\n,JSON 字符串里如果包含\n换行,会导致字节数变化,极端情况下破坏 UTF-8 序列。二进制模式完全不做转换,原样写入。

4.3 案例三:解析嵌套数组并遍历键值

嵌套数组也是高频需求。比如接口返回一个用户列表,里面每个用户又有嵌套对象。代码如下:

int main() { const char* jsonText = "{\"list\":[" "{\"name\":\"\\u5f20\\u4e09\",\"score\":88}," "{\"name\":\"\\u674e\\u56db\",\"score\":76}" "],\"total\":2}"; Json::Reader reader; Json::Value root; if (!reader.parse(jsonText, root)) { printf("parse failed\n"); return 1; } Json::Value list = root["list"]; // 拿到数组 int count = (int)list.size(); // VC6 里 size() 返回无符号数,先转 int for (int i = 0; i < count; ++i) { Json::Value item = list[i]; // 取第 i 个元素 std::string name = item["name"].asString(); int score = item["score"].asInt(); printf("第 %d 个:%s,分数 %d\n", i + 1, Utf8ToGbk(name).c_str(), score); } printf("总数:%d\n", root["total"].asInt()); return 0; }

Json::Value list = root["list"];这里是一次拷贝。JSONCPP 老版本的Value拷贝是深拷贝,数据量不大时没问题;如果列表有几十万条,建议用const Json::Value& list = root["list"];避免拷贝开销。VC6.0 对size()的默认返回unsigned int,循环变量用int会有 warning C4018,我直接强转,省得编译日志里全是黄条。

printf里我用了Utf8ToGbk(name).c_str()。注意这里Utf8ToGbk返回一个临时对象,.c_str()指针的有效期持续到 printf 调用结束,所以这个写法是安全的。如果先保存再打印,更稳妥,但临时对象方式在单条语句里也成立。

4.4 案例四:跨文件调用的封装模块

实际项目里 JSONCPP 的调用不会只在一个文件里发生。多个窗口、多个业务模块都要解析 JSON,如果每个人都记得转码,迟早有人忘记。所以做一个封装层,把“从 Value 取字符串并转 GBK”变成唯一入口,这是跨文件调用的正确姿势。

// json_utils.h #ifndef JSON_UTILS_H #define JSON_UTILS_H #include "json/json.h" #include <string> bool LoadJsonUtf8(const char* filePath, Json::Value& root); bool SaveJsonUtf8(const char* filePath, const Json::Value& root); // 统一从 Value 里取字符串,并转成 GBK 返回 std::string GetJsonStringAsGbk(const Json::Value& root, const char* key); #endif
// json_utils.cpp #include "json_utils.h" #include "utf8_gbk.h" #include <stdio.h> bool LoadJsonUtf8(const char* filePath, Json::Value& root) { FILE* f = fopen(filePath, "rb"); if (NULL == f) return false; fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); char* buf = new char[len + 1]; size_t rd = (len > 0) ? fread(buf, 1, len, f) : 0; buf[rd] = '\0'; fclose(f); // 去掉 UTF-8 BOM 头,避免 jsoncpp 解析失败 if (rd >= 3 && (unsigned char)buf[0] == 0xEF && (unsigned char)buf[1] == 0xBB && (unsigned char)buf[2] == 0xBF) { memmove(buf, buf + 3, rd - 3); buf[rd - 3] = '\0'; } bool ok = Json::Reader().parse(buf, root); delete[] buf; return ok; } bool SaveJsonUtf8(const char* filePath, const Json::Value& root) { Json::StyledWriter writer; std::string output = writer.write(root); FILE* f = fopen(filePath, "wb"); if (NULL == f) return false; fwrite(output.c_str(), 1, output.length(), f); fclose(f); return true; } std::string GetJsonStringAsGbk(const Json::Value& root, const char* key) { if (!root.isMember(key)) return ""; return Utf8ToGbk(root[key].asString()); }

业务代码只关心键名,不再关心底层编码:

#include "json_utils.h" int main() { Json::Value root; if (!LoadJsonUtf8("config.json", root)) { printf("加载失败\n"); return 1; } std::string name = GetJsonStringAsGbk(root, "name"); printf("姓名:%s\n", name.c_str()); return 0; }

这个封装有几个隐藏收益:首先,全项目只有GetJsonStringAsGbk这一个地方调用了Utf8ToGbk,以后想换成别的转码实现,只改一个文件。其次,LoadJsonUtf8把 BOM 处理收进公共代码,谁读文件都不会再踩首键解析失败的坑。最后,isMember先判断键是否存在,避免取到无效键后返回空字符串而不是崩溃。跨文件调用时,务必让所有模块都 includejson_utils.h,不要直接 includejson/json.h再各自转码,不然封装就白做了。

5. VC6.0 常见的 5 个 JSONCPP 调用避坑记录

5.1 链接报错 LNK2005:运行时库不一致

现象:编译全部通过,链接时报一堆重复符号,最常见的是_free、_malloc、__imp__...,错误格式是LNK2005: _free already defined in LIBCMT.lib。Debug 和 Release 表现还不一样。

原因:JSONCPP 源文件或 lib 使用的运行时库和主工程不一样。VC6.0 的运行时库选项有单线程、多线程、多线程调试、多线程 DLL 四种,不同选项对应不同的 CRT 链接方式。两个模块各用自己的堆,链接器在解析 CRT 符号时就打架了。

解决:先搞清楚主工程是/MD还是/MT。在 Project -> Settings -> C/C++ -> Code Generation -> Use run-time library 里,Debug 选“Debug Multithreaded DLL”,Release 选“Multithreaded DLL”,这是最常见组合。如果你用的是静态库方式,把 jsoncpp 的 lib 工程也改成完全相同的选项。如果源码直接加入主工程,这个问题自然消失。

5.2 解析出来的中文变成“鈥濄涓”乱码

现象:用printf("%s", root["name"].asString().c_str())直接输出,或者MessageBoxA显示,出现一排生僻字和空格,比如“鈥濄涓”或“鏂囩”。

原因:asString()返回的是 UTF-8 字节,而输出函数按 GBK 解释。UTF-8 三字节一字符,GBK 两字节一字符,错位后整个字符串面目全非。

解决:输出前必须经过Utf8ToGbk。这是最核心的一条,记住“JSONCPP 不输出 GBK,除非你转了”。另外,不要用cout << root["name"]直接输出,VC6.0 标准库的operator<<只认char*,而且不会帮你转码,结果一样乱。

5.3 带 BOM 的文件首键解析失败

现象:Lodading JSON string with BOM.reader.parse返回false。但用命令行查看文件内容,看起来没问题,用其他语言解析也正常。

原因:Windows 记事本保存 UTF-8 文件时,会在最前面写入EF BB BF三个字节,这就是 BOM。JSON 标准不允许文件开头有 BOM,JSONCPP 的解析器把EF BB BF当作非法字符处理,第一行就报错,整个解析直接失败。

解决:读文件后,判断前三个字节如果是EF BB BF,用memmove把后面的数据前移三个字节,并更新终止符。代码已经写在 4.4 的LoadJsonUtf8里。自己写文件时不要加 BOM,用二进制模式fwrite写的 JSON 天然无 BOM。如果客户拿来的是带 BOM 文件,你的程序要有兼容能力,这是老项目里常见的“隐性需求”。

5.4 Debug 正常 Release 崩溃,或者反过来

现象:Debug 编译运行一切正常,Release 编译通过但运行到delete或函数返回时崩溃;也有反过来的,Release 好端端,Debug 一跑就挂。

原因:两个模块使用的 CRT 不同,比如 Debug 用了/MDd,Release 用了/MT。std::string在 VC6.0 里不是 POD,内部有指针指向堆内存。如果 A 模块申请了内存,B 模块释放,而 A、B 分别链接不同版本的 CRT,堆管理器和运行时错误处理逻辑不一致,轻则崩溃,重则内存损坏。

解决:最彻底的办法是把 JSONCPP 源码直接加入主工程,让它和业务代码用同一套运行时库。如果非要用独立 lib,lib 工程和调用工程的 Use run-time library 必须一模一样。还要避免在 DLL 边界传递std::string,如果 DLL 接口必须返回字符串,用const char*加自定义释放函数。

5.5 生成后的 JSON 里中文全是\u5f20\u4e09

现象:FastWriter写出out.json,打开一看"name":"\u5f20\u4e09",虽然不是乱码,但甲方说“这不是我要的中文”,要求文件里直接显示“张三”。

原因:JSONCPP 老版本出于安全考虑,对非 ASCII 字符统一转义成\uXXXX。这是 JSON 标准允许的表示法,任何标准 JSON 解析器都能正确还原。只是文件可读性差,调试时看一眼都是转义序列,烦。

解决:如果你坚持要原始中文,可以修改json_writer.cpp里的appendQuotedString函数。找到类似“else if (i < 0x80)输出原字符,else转义成\\uXXXX”的代码,把else分支改成直接输出当前字节:

else { // 原代码:转成 \uXXXX 转义序列 out += "\\u"; // ... 若干代码拼出四位十六进制 // 改成: out += static_cast<char>(i); // 前提是输入必须是合法 UTF-8 字节流 }

注意:这么做的前提是进入Json::Value的字符串已经是合法 UTF-8,否则文件中会出现非法字节。修改后,FastWriter和StyledWriter输出时都会保留原始 UTF-8 中文字节。如果你不想改源码,也可以保留转义,终端程序解析后记忆,但这方案在交付审核时很容易被打回。个人习惯是改一次源码,写个注释,以后一劳永逸。

6. 把乱码问题一次性焊死:自写 UTF8ToGBK 小工具与验证方法

最后放一个我实战里必用的验证工具。当你拿不准一个字符串到底是 UTF-8 还是 GBK 时,不要靠肉眼猜,把十六进制字节打出来,一眼就能分辨。

#include <stdio.h> #include <string.h> // 打印字符串每个字节的十六进制,用于判断编码 void PrintHex(const char* s) { if (s == NULL) return; const unsigned char* p = (const unsigned char*)s; while (*p) { printf("%02X ", *p); ++p; } printf("\n"); }

判断规则很简单:常用汉字的 UTF-8 编码,第一个字节在E4到E9之间,比如“测”是E6 B5 8B;GBK 编码的汉字,第一个字节在D0到D9之间,比如“测”是B2 E2。如果你在输出里看到一串E6 B5 8B E8 AF 95,那几乎可以肯定是 UTF-8;看到B2 E2,说明还是 GBK 原样。

配合一个完整测试:

int main() { // 模拟“测试”两个字从 GBK 到 UTF-8 再回 GBK 的完整回路 std::string utf8 = GbkToUtf8("测试"); PrintHex(utf8.c_str()); // 期望 E6 B5 8B E8 AF 95 std::string gbk = Utf8ToGbk(utf8); printf("显示:%s\n", gbk.c_str()); // 期望“测试” return 0; }

验证点有三个。第一,PrintHex(utf8.c_str())的输出必须是E6 B5 8B E8 AF 95,只要以E开头,说明 UTF-8 转换对了。第二,printf显示的是“测试”而不是乱码,说明Utf8ToGbk转换正确,控制台代码页也是正常的 GBK。第三,把这个字符串用fwrite写进文件,再用十六进制编辑器打开,文件头不能有EF BB BF,内容必须是E6 B5 8B E8 AF 95,这就证明整个链路从内存到磁盘都干净了。

另外有个控制台技巧:在main开头调用SetConsoleOutputCP(CP_UTF8);,然后直接printfUTF-8 字符串,Windows XP 以上配合 TrueType 字体也能显示中文。但我不建议把它当常规方案,因为你的代码越过了 GBK 层,一旦输出到日志文件或对话框,又绕回乱码那条路。验证阶段用一下可以,生产代码里还是老老实实转 GBK。

我处理这类老项目时,固定动作是先把utf8_gbk.h放到公共头文件目录,再写一个json_utils封装层,业务代码绝对不允许直接碰Json::Value里的字符串。每次新同事抱怨乱码,我就让他把十六进制打出来看,三分钟定位问题。这套流程用在 VC6.0 上,至少能减少 80% 的编码扯皮,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询