简介:面向仍在使用 Visual C++ 6.0 的 C++ 开发者与维护旧项目的技术人员,这份案例聚焦 jsoncpp 的源码集成、JSON 解析与序列化,以及中文字符读写时的乱码规避,适合需要在旧项目或教学环境中快速验证 JSON 方案的读者。压缩包共 72 个文件,约 3.77MB,包含 24 个头文件、9 个 cpp 源文件及若干 inl 内联实现,另有工程配置、编译中间文件、说明文档、测试文本和可执行程序,完整保留了 VC6.0 工程的目录组织与构建痕迹。已有 591 人学习下载,说明其在旧版 VC 环境下的参考价值。读者可据此对照完整的对话框/控制台案例,理解源码加入工程、编码设置、解析与写回流程,并结合附带说明文档排查常见配置与乱码问题,减少重复搭建和试错成本。
1. 从一条 fatal error C1083 说起:VC6 下编 jsoncpp 为什么先卡在头文件
周五下午接手一个老系统维护,机器上只装了 VC6.0,需求是给本地配置文件加一套 JSON 读写。手头能用的库不多,jsoncpp 算是最顺手的那个,结果新建工程、把源码拖进去、按 F7,第一条就红:fatal error C1083: Cannot open include file: 'stdint.h': No such file or directory。这不算意外,是必然——VC6.0 是 1998 年的编译器,连 C++98 都只实现了个大概,而 jsoncpp 后来的分支早就按 C++11 起步写了,两边差着十几年。真正折腾人的不是这条 C1083,把它改完之后紧接着冒出 C2039、C2059、LNK2001,最后程序终于跑起来,中文全变成问号,日志里一片\u4e2d\u6587。这篇就按「编译不过 → 中文乱码 → 解析失败」这条真实链路往下拆,每一步给出能照抄的改动和验证办法,适合手上只有 VC6.0、又被 jsoncpp 的中文读写卡住的场景。
2. 编译期就翻车:VC6 编译 jsoncpp 的四类报错与改法
先明确一个心态:这不是「配错了某个选项」,而是两代工具链的硬性差距。VC6.0 的编译器版本号停在一个很早的位置,缺 C99 头文件、缺long long、模板偏特化和两阶段名字查找都不完整,而 jsoncpp 的源码里到处是这些东西。所以不要指望「加个 include 路径就好」,得逐条改。下面四类报错基本覆盖了九成情况,按出现顺序排列,改完一类再编一次,别一次性全改,否则新报错会混在一起看不清是谁引起的。
2.1 fatal error C1083:找不到 stdint.h 与 int64 的坑
这条最好治。stdint.h是 C99 的头,VC6 根本没有。可选的路有两条:一是自己在工程目录下补一个最小可用的stdint.h,二是把 jsoncpp 的 64 位整型支持整体关掉。我一般走第二条,因为老工程里真正需要 64 位整数的 JSON 字段少之又少,关掉能连带砍掉一大片 VC6 不认的宏和函数。
在工程设置里把JSON_NO_INT64加到预处理器定义里,位置在 Project → Settings → C/C++ → Preprocessor → Preprocessor definitions,Debug 和 Release 两个配置都要加。加了之后,源码里那批int64_t、UINT64_C的分支会走空实现,C1083 一般就不再出现了。
如果业务确实绕不开 64 位,那就补头文件:
// 工程目录下新建 stdint.h,并确保它在 VC6 的 INCLUDE 搜索路径里 #ifndef _STDINT_H_VC6_ #define _STDINT_H_VC6_ typedef signed char int8_t; typedef unsigned char uint8_t; typedef short int16_t; typedef unsigned short uint16_t; typedef int int32_t; typedef unsigned int uint32_t; typedef __int64 int64_t; // VC6 没有 long long,只能用 __int64 typedef unsigned __int64 uint64_t; // jsoncpp 内部会用到这两个拼接宏,缺了会在宏展开处报 C2065 #define INT64_C(x) x##i64 #define UINT64_C(x) x##ui64 #endif这段的关键在int64_t的定义:VC6 认__int64但不认long long,直接照抄标准头会报C2632: 'long' followed by 'long' is illegal。另外INT64_C这类宏必须补,因为 jsoncpp 里写常量时用的是INT64_C(2147483648)这种形式,没有它会在宏展开处报未定义标识符。补完之后注意打印格式,__int64在 VC6 的printf家族里用%I64d,不是%lld。
提示:
stdint.h放工程目录只能让当前工程看见,如果同解决方案下多个子工程都要用,放到一个公共 include 目录并加进 Tools → Options → Directories。
2.2 error C2039: 'mbstate_t' 不是 std 成员
紧跟着 C1083 出现的通常是这条,报在json_reader.cpp或json_writer.cpp里,指向std::mbstate_t、std::locale、std::codecvt这一族。原因是 jsoncpp 早期为了处理不同平台的编码,用了一层 locale 相关的封装,而 VC6 的<locale>实现是残缺的,mbstate_t压根没往std命名空间里放。
有两种改法。省事的是在json/config.h或者工程的预处理器里把异常和 locale 相关的开关关掉,让那批代码走进#if的假分支:
// 放在 json/config.h 顶部,或者直接加进工程的预处理器定义 #define JSON_NO_INT64 1 // 顺带解决 2.1 的 int64 问题 // #define JSON_USE_EXCEPTION 1 // 注释掉,改用断言版本,少一大片 try/catch 相关代码 // VC6 的 <locale> 不完整,屏蔽掉依赖它的编码转换封装 #if defined(_MSC_VER) && _MSC_VER < 1300 # define JSON_USE_LOCALE 0 #endif关掉JSON_USE_EXCEPTION之后,出错路径会走JSON_ASSERT,也就是assert,Debug 下弹断言框、Release 下直接往下跑。这在生产环境里其实更危险,所以建议只在编译阶段关,等编过之后再把异常打开、把缺的桩函数补上。我个人的习惯是编译期先关,跑通第一个 demo,再回头处理异常路径——一步到位改十处,出问题时根本分不清是哪处改坏的。
如果关掉宏之后还有零星几条mbstate_t报错,那就是源码里写死了,只能手动注释掉那几个函数体,返回一个空字符串占位。这类函数在 VC6 场景下本来也用不上,因为编码转换我们会自己用 Win32 API 做,见第 3 章。
2.3 error C2059 / C2065:模板写法与命名空间不兼容
这一类最杂,典型长这样:
error C2059: syntax error : '<'error C2065: 'value_type' : undeclared identifiererror C2143: syntax error : missing ';' before '<'
根子在两点。一是 VC6 不支持typename的完整语义,模板里依赖类型的嵌套名字必须显式写typename,而 jsoncpp 有些地方省了;二是 VC6 对std::vector<JSONNode>::iterator这类长嵌套名的解析很脆弱,遇到>>或者模板成员函数特化就容易断句断错。
改法是就地加 typedef,把嵌套名摊平:
// 在报错的文件里,把长嵌套名提前 typedef 出来,绕开 VC6 的模板解析缺陷 typedef std::vector<JSONNode> NodeVector; typedef NodeVector::iterator NodeIterator; typedef NodeVector::const_iterator NodeConstIterator; // 类内使用时改成短名,VC6 就不会在 '<' 上断句失败 for (NodeIterator it = nodes.begin(); it != nodes.end(); ++it) { // ... }改这类报错有个顺序技巧:从第一个报错开始改,改完立刻重编。VC6 的模板报错会连锁,第一条 C2059 往往会让后面几十条都是假的,一次性按报错列表全改,很容易把本来对的代码改坏。另外一个高频点是for循环变量作用域——VC6 里for (int i = 0; ...)的i会泄漏到外层作用域,如果外层还有个同名变量,报的是C2374: redefinition,把内层变量改个名就行。
2.4 LNK2001:库编出来了但对不上
编过之后到了链接阶段,最常见的两条:
LNK2001: unresolved external symbol "public: __thiscall Json::Value::Value(...)" LNK2005: "class Json::Value __cdecl Json::Value::null" already definedLNK2001 说明主工程看见声明但找不到实现,八成是只把.h加进了工程,.cpp没加,或者加进去了但被排除编译(文件图标上有个禁止符号)。在 Workspace 的 FileView 里右键json_value.cpp、json_reader.cpp、json_writer.cpp,确认 Settings 里没勾「Exclude file from build」。
LNK2005 说明同一个符号被定义了两遍,通常是 jsoncpp 的源码被加进了两个工程、又都编成了静态库再链接到一起;或者工程里同时存在两份 jsoncpp 的头文件,一份在工程目录、一份在系统 include 路径,宏定义不一样导致同一个类被展开成两种布局。排查办法是在链接报错里看两个定义分别来自哪个.obj,顺着.obj找源码。
还有一个隐蔽的:运行时库不匹配。jsoncpp 源码和主工程如果一边用/MTd、一边用/MDd,链接时会在operator new、std::string的符号上报一堆 LNK2001,看着像是 jsoncpp 的符号缺失,其实是 CRT 的符号对不上。VC6 里这两处都要改:Project → Settings → C/C++ → Code Generation → Use run-time library,两边设成同一个值,Debug 是 Debug Multithreaded DLL,Release 是 Multithreaded DLL。
| 报错 | 真实原因 | 处理位置 |
|---|---|---|
| C1083 stdint.h | 缺 C99 头 | 补头文件或定义 JSON_NO_INT64 |
| C2039 mbstate_t | locale 支持残缺 | 关 JSON_USE_EXCEPTION、屏蔽 locale 封装 |
| C2059 尖括号 | 模板嵌套解析缺陷 | 提前 typedef,摊平嵌套名 |
| LNK2001 jsoncpp 符号 | .cpp 未参与编译 | FileView 里检查排除标记 |
| LNK2001 满屏 CRT 符号 | 运行时库不一致 | Code Generation 两处统一 |
3. 中文变成问号或一串 u4e2d:GBK 与 UTF-8 在三处错位
编译过了,程序跑起来,读一个带中文的配置文件,输出全变问号。这时候很多人第一反应是「jsoncpp 的 bug」,其实绝大多数情况是编码在三个环节上各说各话:源文件和磁盘文件是 GBK,jsoncpp 内部按 UTF-8 处理,最后写回文件又是 GBK。只要中间有一处没转,链路上就会留下残字或者被替换成?。
3.1 错位发生在哪:三个环节各自的默认编码
先看清三处。第一处是源文件,VC6 保存的文件默认是 ANSI,在中文系统上就是 CP936(GBK),你写std::string s = "中文";,s里装的是D6 D0 CE C4四个字节,不是 UTF-8。第二处是 jsoncpp 内部,它假定输入输出都是 UTF-8,解析时按 UTF-8 规则去解码字节流,序列化时也会把非 ASCII 字符转成\uXXXX。第三处是磁盘上的 JSON 文件,取决于它是谁生成的——可能是别的工具写的 UTF-8,也可能是这段老代码自己写出来的 GBK。
麻烦在于 GBK 的双字节和 UTF-8 的字节范围有重叠,不是所有错位都会立刻报错。比如 GBK 的「中」是D6 D0,D6在 UTF-8 里是两字节序列的首字节,D0恰好落在合法的续字节范围80-BF内,于是解码器不报错,直接把它解成一个完全不相干的码点。这就是为什么有时候不乱码、只是字符变了——比直接报错更难查。真正会报错的是那些第二字节大于BF的汉字,解码器发现续字节非法,jsoncpp 就给你回一个解析失败,位置指在某一行某一列,看着像是 JSON 语法错了,其实是编码错了。
所以排查的第一步永远是确认字节,不是盯着字符串看。判断标准很简单:进入parse之前,那段文本必须是一段合法 UTF-8。要拿到这个状态,就得在读取后插入一次 GBK 到 UTF-8 的转换。
3.2 读文件:GBK 到 UTF-8 的两步转换
Windows 上转编码的标准路线是「多字节 → 宽字符 → 多字节」,两步走,中间用 UTF-16 做跳板。有人会想一步到位用iconv,但 VC6 工程里引入第三方库又是一堆链接麻烦,直接用系统 API 最省事。
#include <windows.h> #include <vector> #include <string> // GBK(CP936) -> UTF-8,失败返回空串 std::string GbkToUtf8(const std::string& gbk) { if (gbk.empty()) return std::string(); // 第一步:按 CP936 把 GBK 字节解成宽字符(内部是 UTF-16LE) int wlen = ::MultiByteToWideChar(936, 0, gbk.c_str(), (int)gbk.size(), NULL, 0); if (wlen <= 0) return std::string(); // 输入里有非法 GBK 字节 std::vector<wchar_t> wbuf(wlen + 1, L'\0'); // 多留一位,方便后续按串处理 ::MultiByteToWideChar(936, 0, gbk.c_str(), (int)gbk.size(), &wbuf[0], wlen); // 第二步:把宽字符按 UTF-8 编回去 int ulen = ::WideCharToMultiByte(CP_UTF8, 0, &wbuf[0], wlen, NULL, 0, NULL, NULL); if (ulen <= 0) return std::string(); std::string utf8(ulen, '\0'); ::WideCharToMultiByte(CP_UTF8, 0, &wbuf[0], wlen, &utf8[0], ulen, NULL, NULL); return utf8; }这段里几个参数值得说清楚。第一个参数936是 GBK 的代码页,写CP_ACP也行,但CP_ACP跟着系统区域走,换一台英文系统机器就变成 CP1252,中文直接丢,所以老工程里我建议写死936。dwFlags传 0 是宽松模式,遇到非法字节会静默替换成?;调试阶段建议传WC_ERR_INVALID_CHARS,让它严格失败并返回 0,再配GetLastError()看是不是ERROR_NO_UNICODE_TRANSLATION(1113),这样能立刻知道输入里有半截汉字。
长度参数用(int)强转是必须的,因为std::string::size()返回的是size_type,在 VC6 里是unsigned int,直接传给int形参会有警告,超长字符串还可能截断成负数。&wbuf[0]和&utf8[0]这种取首地址的写法在 VC6 的 STL 实现里是安全的,因为它底层就是连续数组,但前提是容器非空——上面两个if (empty())的判断不能省,空容器取[0]是未定义行为,Debug 下直接崩在_ASSERT。
注意:
MultiByteToWideChar的cbMultiByte传-1表示「以\0结尾,自动算长度」,但那样会把结尾符也算进去,得到的结果多一个字符。传size()才是精确的字节数。
3.3 写文件:再把 UTF-8 转回 GBK
读取方向转完了,输出方向是镜像操作,参数对调:
// UTF-8 -> GBK(CP936),用于把 jsoncpp 的输出写进老系统的配置文件 std::string Utf8ToGbk(const std::string& utf8) { if (utf8.empty()) return std::string(); // 先解成宽字符,这里用严格模式,遇到坏字节立刻返回 0 int wlen = ::MultiByteToWideChar(CP_UTF8, WC_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), NULL, 0); if (wlen <= 0) { // 常见原因:上游传进来的根本不是 UTF-8,或者被截断了半截 return std::string(); } std::vector<wchar_t> wbuf(wlen, L'\0'); ::MultiByteToWideChar(CP_UTF8, WC_ERR_INVALID_CHARS, utf8.c_str(), (int)utf8.size(), &wbuf[0], wlen); // 再按 CP936 编回去 int glen = ::WideCharToMultiByte(936, 0, &wbuf[0], wlen, NULL, 0, NULL, NULL); if (glen <= 0) return std::string(); std::string gbk(glen, '\0'); ::WideCharToMultiByte(936, 0, &wbuf[0], wlen, &gbk[0], glen, NULL, NULL); return gbk; }严格模式在这里的价值比读取方向更大。反向转换时,如果上游塞进来的是一段「看起来像 UTF-8 但其实是 GBK」的字节,宽松模式会把它硬解成一堆乱码字符再编回 GBK,结果是文件里出现一堆问号,而且不报错,你根本不知道从哪一步开始错的。用WC_ERR_INVALID_CHARS会直接返回 0,函数返回空串,调用方立刻能发现异常。
还有一个容易漏的点:文件打开模式。VC6 里fopen(path, "w")是文本模式,会把\n写成\r\n。这对 JSON 本身没影响(解析器容忍\r\n),但如果文件里有二进制内容或者你在做字节级比对,就会对不上。写 JSON 建议用"wb",读的时候用"rb",把换行转换这件事关掉,字节层面干净很多。
3.4 那一串 u4e2d 到底是乱码还是正常输出
最后解释一个高频误解。看到 JSON 里写着"\u4e2d\u6587",很多人第一反应是「乱码了」。其实这是合法输出——\uXXXX是 JSON 规范里对非 ASCII 字符的标准转义写法,jsoncpp 的valueToQuotedString在序列化时会把码点大于 0x7F 的字符转成这种形式,目的是让 JSON 文本在任何编码环境下都能安全传输。它本身没坏,坏的是接收方:如果下游那个解析器只认字面中文,看见\u4e2d就原样显示,那才叫乱码。
判断方法很简单:把这段\uXXXX拿去任何一个标准 JSON 解析器里跑一遍,能还原成「中文」,说明链路是通的,问题在下游显示层。要让它输出字面中文,有两条路:一是找 jsoncpp 里控制转义的开关(不同分支位置不一样,通常在 writer 的配置里),二是别动库,在写出之前做一次 unescape 处理,把\uXXXX还原成 UTF-8 字节再写文件。
我倾向于后者。改库意味着以后升级或者换人接手都要重新理解一遍改了什么,而一个二十行的 unescape 函数放在业务层,谁看都明白。实现上就是把\u后面四位十六进制解析成码点,再按 UTF-8 规则编成 1 到 3 个字节;注意代理对(\uD800到\uDFFF区间)要合并成 4 字节,中文常用字不在这个区间,但生僻字和 emoji 会踩到。
4. Reader::parse 返回 false:先别怀疑库,按这四步定位脏数据
parse返回false是最让人抓狂的一类,因为它不告诉你原因,只是一个布尔值。实际排查下来,九成以上是数据本身有问题——BOM、尾部逗号、被截断的汉字、注释符号——而不是 jsoncpp 解析能力不行。这一章按我实际的排查顺序排,从「拿到错误位置」到「确认字节合法性」,一步步收窄。
4.1 先把错误行号和列号打出来
不要只判断返回值。jsoncpp 的Reader内部维护了错误信息,只要调用getFormattedErrorMessages()就能拿到带行号列号的描述:
#include <windows.h> #include "json/json.h" // 解析并打印失败位置,注意 collectComments 参数别传 false bool ParseWithDetail(const std::string& text, Json::Value& root) { Json::Reader reader; bool ok = reader.parse(text, root, false); // 第三个参数是 collectComments if (!ok) { // 输出形如 "* Line 3, Column 7 Syntax error: value, object or array expected." std::string msg = reader.getFormattedErrorMessages(); OutputDebugStringA(msg.c_str()); OutputDebugStringA("\n"); } return ok; }拿到行号列号之后,直接把那段文本按行切开,看那一列上是什么字节。经验上,Line 1, Column 1基本就是 BOM 或者文件开头有空白,Syntax error: value, object or array expected出现在中间某列,大概率是前一个值后面少了逗号或者多了逗号。Missing '}' or object member name则通常是嵌套结构没闭合,往上报的行号往上翻几行找未闭合的{。
OutputDebugStringA只在调试器挂着的时候能看到,用 DebugView 之类的工具也能抓。如果是无人值守的服务进程,把msg写进日志文件更实际,别指望事后能复现。
4.2 开头三个字节:BOM 头是头号嫌疑
Line 1, Column 1的报错,第一个要查的是 UTF-8 BOM。Windows 上很多编辑器「另存为 UTF-8」时默认带 BOM,写出来是EF BB BF三个字节,jsoncpp 不认识它,把它当成第一个字符去解析,直接判非法。
剥掉的办法很简单,但有一个必须注意的细节——char的符号性:
// 去掉 UTF-8 BOM(EF BB BF),没有则原样返回 std::string StripBom(const std::string& s) { if (s.size() >= 3 && (unsigned char)s[0] == 0xEF && // 必须转 unsigned char,char 是有符号的 (unsigned char)s[1] == 0xBB && (unsigned char)s[2] == 0xBF) { return s.substr(3); } return s; }为什么强调unsigned char:VC6 里char默认有符号,0xEF存进char是-17,拿它跟字面量0xEF(在比较时会被提升成int的正数 239)比,永远不相等。这个坑我在不同项目里见过至少三次,每次都是对着明明有 BOM 的文件debug半天,最后发现是类型提升搞的鬼。
顺手也要检查文件开头是不是有空白字符。有些配置文件是人工维护的,行首敲了几个空格,jsoncpp 是容忍的,但如果换成某些严格校验工具就会报错。统一在读取后做一次find_first_not_of(" \t\r\n")定位,把前面的空白切掉,能省不少事。
4.3 尾部逗号、注释与不规范的 JSON 扩展
jsoncpp 老版本的 Reader 对 JSON 扩展语法是「部分容忍」的,容忍到什么程度取决于编译开关和分支,同一份代码换个版本行为就可能变。典型场景是尾部逗号:{"a": 1, "b": 2,}这种写法在很多前端工具里能过,但在严格模式的解析器里就是语法错,报Syntax error: array value missing或者类似的描述。
还有一个是注释。// 这是一行说明在 JSON 里是非法的,但配置文件里人工加注释太常见了。jsoncpp 的parse有collectComments参数,但那个参数控制的是「要不要把注释收集起来」,不是「允不允许注释」——很多人把它理解反了,以为传true就能容忍注释,结果还是报错。
判断这类问题最快的办法是交叉验证:把同一份文件丢给任意一个标准 JSON 校验器(本地脚本几行代码就行),如果不通过,那就是数据问题,跟库无关;如果通过而 jsoncpp 不通过,才是库的容忍度问题。我一般在项目里备一个十来行的小脚本专门干这事,遇到解析失败先跑一遍,能立刻把「数据锅」和「库锅」分开。
4.4 被截断的汉字:最难发现的一种脏数据
这一类不报语法错,但会在后面转码时神秘失败。成因是读取文件时缓冲区给小了,比如char buf[1024]而文件有 1025 字节,最后一个字刚好是个 GBK 双字节汉字,只读进来前半个字节D6。这半个字节本身是合法的单字节,parse阶段如果它落在字符串末尾,可能直接被当普通字节收下;等到Utf8ToGbk的时候,WC_ERR_INVALID_CHARS发现它不是合法 UTF-8 序列,直接返回空串,表现就是「整段中文消失」。
定位手段只有一个:十六进制打印。把进入parse之前的那段字节流按%02X打出来,肉眼核对末尾有没有落单的高位字节(首字节在81-FE之间、后面没跟着40-FE的续字节),有的话基本就确诊了。
#include <stdio.h> // 打印前 n 字节的十六进制,用于确认编码和截断 void HexDump(const std::string& s, size_t n) { char cell[8]; if (n > s.size()) n = s.size(); for (size_t i = 0; i < n; ++i) { sprintf(cell, "%02X ", (unsigned char)s[i]); // 同样要转 unsigned char OutputDebugStringA(cell); } OutputDebugStringA("\n"); }读出文件的地方也要跟着查:fread的返回值拿到了没有、有没有被当成「读满即成功」、feof的判断是不是放在了循环外面。这类问题在 VC6 工程里特别多,因为很多老代码是while (!feof(fp))这种写法,最后一次会多读一轮空数据,配合变长内容就容易出边界问题。
5. 避坑清单:VC6 加 jsoncpp 组合下最容易踩的五个坑
下面五条基本都是我或者同事在真实维护里踩过的,按「现象 → 原因 → 解决」写,方便对着现场比。
5.1 Debug 下一切正常,Release 一跑就崩
现象:调试版能完整读写配置,换成 Release 编译,程序在Json::Value的某个operator=上直接异常退出,栈回溯还指不到具体行。 原因:两个配置的运行时库不一致。VC6 的 Debug 版 CRT 和 Release 版 CRT 各有一套堆,跨配置 new 出来的对象在另一套堆上 delete,行为未定义。 解决:Project → Settings 里把 Debug 和 Release 的 Code Generation 都检查一遍,Debug 用 Debug Multithreaded DLL,Release 用 Multithreaded DLL,两边保持同族;如果 jsoncpp 是单独编成静态库的,那个库工程也要改成一样的配置再重编一遍。
5.2 复用同一个 Writer,JSON 越写越长
现象:循环里反复用同一个StyledWriter对象生成字符串,第一次输出正常,第二次开始前面多了一段旧的 JSON,越往后越长。 原因:VC6 的std::ostringstream在str("")清空之后,内部指针状态没有完全复位,下一次写入是追加而不是覆盖。 解决:每次生成都新建 writer 对象,不要复用;如果非复用不可,在每次write之前把内部流对象整个替换掉,而不是调str("")。这条在别的编译器上不一定复现,所以特别容易漏测。
5.3 源码里的全角字符报 C2018
现象:某个.cpp编译报error C2018: unknown character '0xa1',指向的位置看着什么字都没有。 原因:文件里混进了全角空格或者中文标点,在 VC6 里被当成单个字节切分,切出来的半个字符就是非法字符。 解决:把报错行附近的空白全部删掉重敲一遍半角空格;更彻底的办法是用十六进制编辑器打开文件搜A1字节,全角标点的首字节都是A1,一扫就出来。
5.4 明明是字符串,取出来全是空
现象:JSON 里写的是"name": "张三",root["name"].asString()返回空串,不报错也不崩。 原因:这个字段在上游某个环节被写成了非字符串类型(比如被当成数字或者 null),asString()对非字符串类型返回默认值空串,不抛异常。 解决:取值前先isString()判断,不满足就打印type()的值看实际类型;批量取值的地方封装一个带默认值和类型检查的辅助函数,避免每个字段都写一遍 if。
5.5 两份 jsoncpp 头文件混用,链接期符号打架
现象:链接报LNK2005: "class Json::Value Json::Value::null" already defined in xxx.obj,两个 obj 路径完全不同。 原因:工程同时通过「工程目录下的本地副本」和「系统 include 路径」各引了一份 jsoncpp 头,两份的宏定义不同(比如一份开了JSON_NO_INT64、一份没开),同一个类被展开成两种布局,链接时当成两个不同符号,加上静态库里的第三份,直接冲突。 解决:在工程设置里搜一遍所有 include 路径,只保留一处;确认.cpp只加进一个工程,其他工程通过静态库依赖引用。改完记得 Clean 一次再全量重编,VC6 的增量编译对这种头文件改动经常不敏感。
6. 五分钟自查:把编译、编码、解析三段串成一条定位链
遇到这类故障,最怕的是东一榔头西一棒子,改一处编一次,编过了又乱码,乱码修好了解析又失败。我的做法是把它当成三段流水线,每一段有明确的「出口标准」,过了才进下一段,任何一段不过就停在原地查,不往下走。下面这套流程走一遍大概五分钟,能把问题框到具体某一段。
| 步骤 | 动作 | 通过判据 | 不通过时的下一步 |
|---|---|---|---|
| 1 | 检查 C/C++ → Code Generation 的运行时库 | 主工程与 jsoncpp 源码一致 | 统一改成 /MDd 或 /MTd,Clean 后重编 |
| 2 | 用十六进制看输入文件前 3 字节 | 不是 EF BB BF | 走 4.2 的 StripBom |
| 3 | 打印进入 parse 前的字节流前 32 字节 | 全为合法 UTF-8 序列 | 回到 3.2 检查 GbkToUtf8 是否被调用 |
| 4 | 用独立 JSON 校验器跑同一份文本 | 通过 | 说明是数据问题,查尾部逗号和注释 |
| 5 | parse 失败分支打印 getFormattedErrorMessages | 有行号列号 | 检查是否漏调或 collectComments 传错 |
| 6 | 输出文件里的中文 | 字面中文或合法 \uXXXX | 检查 Utf8ToGbk 是否走了严格模式失败分支 |
这套表里我最看重第 3 步。因为编码问题最阴的地方在于它是「静默」的——不像编译错误那样立刻红给你看,它会一路带着错误的字节往下走,直到某个不相关的地方才炸开,然后你回头看栈,发现早就跑偏了。在进入parse之前强制打一次十六进制,相当于在流水线上装了个检测工位,凡是非法字节当场拦下,后面就不用猜了。
再补一个判断技巧:如果同一份 JSON 在开发机上正常、在客户机上失败,先对比两边的系统区域设置。CP_ACP在不同区域设置下解析出来的代码页不一样,中文系统是 936,英文系统是 1252,同样的 GBK 字节在 1252 下会被解成一堆西欧字符,转成 UTF-8 之后完全对不上。所以前面那几处 API 里我坚持写死936而不是CP_ACP,就是为了让行为跟机器无关——老系统部署环境杂,这种「环境相关」的隐式依赖能砍就砍。
还有一个习惯值得养成:给GbkToUtf8和Utf8ToGbk都加上返回空串时的日志,把输入的前 16 字节和GetLastError()一起打出来。这两个函数一旦返回空串,上游往往只是得到一个空字符串,然后parse报「输入为空」这种毫无信息量的错,链路就此断掉。加上日志之后,你至少知道是「转换失败」而不是「文件没读到」,排查范围立刻缩一半。
从那以后我每次接这类老工程,第一件事不是看业务代码,而是把编码链路画成三格图贴在显示器边上——源文件、内存、磁盘,每个转换点标上函数名,然后在这三个转换点的前后各打一次十六进制,确认字节对得上再往下写一行业务逻辑。多花的这十分钟,换回来的是不用在半夜对着Line 1, Column 1猜谜。希望帮到你。
本文还有配套的精品资源,点击获取