VC6集成jsoncpp:编译适配与中文乱码解决

发布时间:2026/11/2 7:05:43
VC6集成jsoncpp:编译适配与中文乱码解决

简介:这份面向VC6.0开发者的JSONCPP实战资料包,聚焦老式C++工程中JSON读写与中文字符防乱码问题,适合需要在VC6.0环境维护、迁移或复现旧项目的开发者参考。压缩包共72个文件、约3.77MB,以24个头文件、9个cpp源文件和多个inl内联实现为主体,同时保留工程配置、编译中间文件、说明文档与可执行示例,完整呈现一套可编译运行的调用案例。资料还包含源码使用说明、注意事项与必看提示,覆盖控制台和对话框两种示例形态,便于按目录结构定位头文件、内联实现与工程配置。内容围绕jsoncpp 0.5.0源码集成、UTF-8编码处理、JSON解析与序列化验证展开,并配有排错提示,便于对照排查中文乱码、编译链接和运行环境等常见问题。目前已有591人学习下载,可作为VC6.0下JSONCPP落地的参考样本,帮助读者快速理解源码组织方式、测试工程结构与编码处理思路。

1. 为什么还在 VC6 里调 JSONCPP:一份能直接编过的老工程配置

维护老系统的工程师几乎都遇到过同一个场景:主程序还在 VC6.0 上跑,界面是 MFC 对话框,业务代码几十万行,短时间没人敢动编译器,但对端接口已经悄悄从key=value换成了返回 JSON 的 HTTP 服务。这时候最顺手的方案就是把 jsoncpp 源码拉下来,直接编进现有工程。问题在于,最新的 jsoncpp 是为 C++11 写的,拖进 VC6 会一次报出几十个错误;就算勉强编过,把中文塞进去再读出来,屏幕上又是一堆问号。这份案例解决的正是这两件事:让 jsoncpp 在 VC6 下干净地编过,以及让中文在 GBK、UTF-8、MFC 控件之间来回传输时不丢字节。适合两类人:一类是接手老工程、必须在不换编译器的前提下加 JSON 能力的人;另一类是正在做数据对接,被乱码折腾过一轮、想弄清问题根源的人。

2. 让 JSONCPP 在 VC6 下编过:源码挑选与三处缺失补齐

VC6 自带的是 1998 年前后的编译器,对 C++ 标准的支持停留在一个很早期的阶段:模板偏特化勉强能用,std::string是写时复制的老实现,<stdint.h>根本不存在,std::locale的 facet 也只有残缺的几类。jsoncpp 本身不复杂,但它的头文件里用了一堆 VC6 没有的东西。所以这一步不是「配置一下就能过」,而是「先选对源码,再补对东西」。

2.1 先做减法:哪一份源码在 VC6 下必然编不过

选源码有个很快的判别方法:打开json_value.cpp,搜三个词:nullptr、unique_ptr、override。只要出现任何一个,这份源码在 VC6 下就没有抢救价值,直接换。常见做法是找 1.x 之前的老分支,那批代码还在用std::auto_ptr和裸指针,VC6 能接受。具体拿哪一版,以你下载的包里 README 写的为准,我这边只讲判断标准,不替你指定版本号。

老分支的目录结构通常很干净,需要加入工程的其实只有三个实现文件:

相对路径作用是否加入工程
json/json_reader.cpp解析器,把文本变成Json::Value是
json/json_value.cpp值类型与字符串处理是
json/json_writer.cpp序列化,把Json::Value变成文本是
json/json_valueiterator.inl迭代器模板实现,被头文件包含否,不要单独加
include/json/*.h公共头文件否,只配包含路径

把.inl文件拖进工程是新手最容易犯的错,它会以单独的编译单元被编一遍,然后报一堆重复定义。判断原则很简单:被#include的.inl/.h不进工程,只有.cpp进。

2.2 补 VC6 缺的三样东西:stdint.h、snprintf 与 locale facet

第一样:stdint.h。老分支里如果出现#include <stdint.h>或者直接用了int64_t,VC6 是找不到的。别去改源码,在 include 目录里自己放一个最小实现,让它优先被搜到就行:

/* include/stdint.h —— 只为 VC6 补的最小实现,不要拿去当通用标准库 */ #ifndef JSONCPP_VC6_STDINT_H #define JSONCPP_VC6_STDINT_H 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 的 64 位整数是编译器扩展关键字 */ typedef unsigned __int64 uint64_t; #endif

这段代码的关键在__int64。VC6 没有long long,64 位整数只能用微软的扩展关键字,unsigned __int64同理。另外命名空间不要写std,写了反而会和以后的升级冲突。放在工程自己的 include 目录里、并且把该目录排在系统目录之前,VC6 就会优先用你这份。

第二样:snprintf。VC6 的 CRT 里没有符合 C99 的snprintf,只有_snprintf,而且它在截断时不一定补\0。如果老分支的 writer 里用了snprintf,最省事的改法是加一个兼容宏:

/* 放在自己工程的公共头文件里,别改 jsoncpp 源码 */ #if defined(_MSC_VER) && _MSC_VER < 1300 #include <stdio.h> #define snprintf _snprintf #endif

用宏而不是改源码,是为了以后换版本时能一眼看出哪些是本地补丁。_snprintf截断不补零这个行为在拼长字符串时是真会出问题的,后面第 5 章还会碰到它。

第三样:locale。jsoncpp 序列化 double 时走的是std::ostringstream加std::locale::classic()。VC6 的 locale 实现里,数字格式化相关的 facet 支持很不完整,运气不好会在运行期抛异常,或者在链接期报 facet 相关的未定义符号。我一般的处理是把 writer 里做数字转字符串的那几行,换成本地的一个sprintf("%.17g")封装——反正 VC6 下也没人指望ostringstream的格式化有多标准。

提示:所有对 jsoncpp 源码的修改,都在文件头加一行注释写明改了什么、为什么改。半年后回来排查的人大概率是你自己。

2.3 工程设置与第一次编译通过的最小验证

工程设置按顺序走这几步:

  1. Project → Settings → C/C++ → Category: Preprocessor,在Additional include directories里填 jsoncpp 的include目录和刚放stdint.h的目录,两个路径用英文逗号分隔。
  2. 选中那三个.cpp,Project → Settings → C/C++ → Category: Precompiled Headers,选Not using precompiled headers。老工程通常带stdafx.h,jsoncpp 的源码里没这行,一起编会直接报「无法找到预编译头」。
  3. Code Generation里的Use run-time library,必须和主工程完全一致(都是Debug Single-Threaded或者都是Multithreaded DLL)。这一项不一致的后果不是编译错误,而是链接期一堆LNK2005,或者更糟:编过了,运行时释放内存时崩。
  4. 警告等级调到 3 以上,会出现大量C4786(调试信息被截断)。这纯粹是 VC6 对长模板名字的抱怨,在公共头文件里关掉:#pragma warning(disable:4786)。

配置完先用一段最小代码验证编译链通了,别一上来就往业务里塞:

#include <iostream> #include <cstring> #include "json/json.h" #pragma warning(disable:4786) int main() { Json::Reader reader; Json::Value root; const char* text = "{\"code\":0,\"msg\":\"ok\"}"; /* 老分支的 parse 有 (begin,end,value) 这个重载,新分支已经去掉了 */ if (!reader.parse(text, text + strlen(text), root, false)) { std::cout << reader.getFormatedErrorMessages() << std::endl; return 1; } std::cout << root["msg"].asString() << std::endl; return 0; }

两个细节值得注意。一是parse的第四个参数collectComments传false,注释收集对内存有额外开销,生产代码里没必要开。二是getFormatedErrorMessages()这个方法名在 jsoncpp 里确实少一个t,不是笔误,照着写就行;它返回的是带行号列号的完整错误描述,比getFormatedErrorMessages之外那些只返回 bool 的接口有用得多。这段跑通,说明编译器、头文件路径、CRT 三样都对上了,可以进入下一步。

3. 中文不乱码的真相:JSONCPP 只搬字节,编码在边界上转

很多人以为 jsoncpp 有个「中文模式」开关,打开就不乱码了。没有这回事。Json::Value内部存的就是std::string,你把什么字节塞进去,它就存什么字节,序列化时原样吐出来。它不做任何编码识别和转换。所以「解析中文防乱码」这件事,本质上不是 jsoncpp 的功能,而是你在数据进出 jsoncpp 的那两个边界上,自己做编码转换。

3.1 一条必须提前定死的约定:磁盘和内存都走 UTF-8

乱码的根源几乎都是同一条:同一份字节被两个不同编码的解释器读了两遍。VC6 的源文件默认存成 ANSI(简体中文环境下就是 GBK),你在代码里写"中文",编译进二进制的字节是0xD6 0xD0 0xCE 0xC4。把这四个字节塞进Json::Value再写到文件里,文件的物理内容还是这四个字节,但文件本身没有任何地方声明自己是 GBK。对端用 UTF-8 解析器一读,就是两个替换字符。反过来一样:服务器返回 UTF-8 的 JSON,asString()拿到的是 UTF-8 字节,直接SetWindowText送进 MFC 的 A 接口,而 GDI 按当前 ANSI 代码页解释,又是一屏问号。

我一般采用这条约定:磁盘上统一 UTF-8,内存里统一 UTF-8,只在和最外层(MFC 控件、控制台、老式配置文件)打交道时转成 GBK。这样定了之后,转换点只有两处,代码里搜得到,不用满工程去猜某个字符串到底什么编码。最怕的是有人图省事,「这里反正是中文,直接塞进去」,混上几次之后就没有人能说清哪个变量是什么编码了。

还有一点,jsoncpp 在解析时会处理\uXXXX转义,把\u4e2d解成 UTF-8 的三字节序列。这是它的标准行为,不需要你额外做什么。但老分支对代理对(surrogate pair)的支持不完整,带 emoji 或者其他增补平面字符的 JSON 进去可能出来是坏的。对接方如果有这类内容,提前在协议上约定用转义原始串透传,别指望老库能还原。

3.2 两个转换函数,以及 CP_ACP 这个陷阱

转换靠 Win32 的两组 API:MultiByteToWideChar和WideCharToMultiByte,先转宽字符再转目标编码,中间那层wchar_t是 UTF-16,正好当桥。VC6 带<windows.h>就能用,不需要额外依赖。

#include <windows.h> #include <string> #include <vector> /* GBK -> UTF-8 */ std::string GbkToUtf8(const std::string& src) { if (src.empty()) return std::string(); /* 明确写 936,不要用 CP_ACP,理由见下文 */ int wlen = ::MultiByteToWideChar(936, 0, src.c_str(), (int)src.size(), NULL, 0); if (wlen <= 0) return std::string(); std::vector<wchar_t> wbuf(wlen, L'\0'); ::MultiByteToWideChar(936, 0, src.c_str(), (int)src.size(), &wbuf[0], wlen); int ulen = ::WideCharToMultiByte(CP_UTF8, 0, &wbuf[0], wlen, NULL, 0, NULL, NULL); if (ulen <= 0) return std::string(); std::vector<char> ubuf(ulen, '\0'); ::WideCharToMultiByte(CP_UTF8, 0, &wbuf[0], wlen, &ubuf[0], ulen, NULL, NULL); return std::string(&ubuf[0], ulen); } /* UTF-8 -> GBK,参数顺序反过来,逻辑完全对称 */ std::string Utf8ToGbk(const std::string& src) { if (src.empty()) return std::string(); int wlen = ::MultiByteToWideChar(CP_UTF8, 0, src.c_str(), (int)src.size(), NULL, 0); if (wlen <= 0) return std::string(); std::vector<wchar_t> wbuf(wlen, L'\0'); ::MultiByteToWideChar(CP_UTF8, 0, src.c_str(), (int)src.size(), &wbuf[0], wlen); int alen = ::WideCharToMultiByte(936, 0, &wbuf[0], wlen, NULL, 0, NULL, NULL); if (alen <= 0) return std::string(); std::vector<char> abuf(alen, '\0'); ::WideCharToMultiByte(936, 0, &wbuf[0], wlen, &abuf[0], alen, NULL, NULL); return std::string(&abuf[0], alen); }

几个参数值得说清楚。936是简体中文的代码页,写死它而不写CP_ACP,是因为CP_ACP取的是「当前系统默认 ANSI 代码页」——同一份程序拿到英文版系统上跑(代码页 1252),转换就会静默失败,产出乱码或者直接返回空串,而且不报错。这种问题在开发机上永远复现不了,一到客户现场就炸。写死 936 的代价是这套代码只处理简体中文,对这份资源的使用场景来说,这个取舍是划算的。

第二个细节是缓冲区用std::vector而不是std::string。VC6 的std::string是写时复制实现,&s[0]拿到的是内部共享缓冲的指针,后面的写入操作有机会触发重新分配,指针就悬空了。虽然在这个场景下刚构造完的串是独占的、理论上安全,但用vector能省掉一整类解释成本,我宁可多写两行。

第三个细节是返回值用std::string(&buf[0], len)而不是靠\0结尾构造。JSON 字符串里理论上不含\0,但从 API 层面按长度构造更严格,转换过程中如果混进了不可见字符也不会被悄悄截断。

3.3 读写封装:加载、保存、BOM 清理

把转换和 jsoncpp 拼起来,落成两个函数,业务代码只调这两个,不直接碰Json::Reader:

/* 从文件加载 JSON。fileIsGbk 为 true 时先把整串转成 UTF-8 再解析 */ bool LoadJsonFile(const char* path, Json::Value& out, std::string& err, bool fileIsGbk) { FILE* fp = fopen(path, "rb"); /* 必须 rb,理由见第 5 章 */ if (fp == NULL) { err = "open failed"; return false; } std::string raw; char buf[4096]; size_t n; while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) raw.append(buf, n); fclose(fp); /* 去掉 UTF-8 BOM,否则解析器第一行必报语法错 */ if (raw.size() >= 3 && (unsigned char)raw[0] == 0xEF && (unsigned char)raw[1] == 0xBB && (unsigned char)raw[2] == 0xBF) raw.erase(0, 3); if (fileIsGbk) raw = GbkToUtf8(raw); Json::Reader reader; if (!reader.parse(raw, out, false)) /* 第三个参数 false:不收集注释 */ { err = reader.getFormatedErrorMessages(); return false; } return true; }

这段代码里,fread的循环写法是刻意的:不能只读一次,因为文件可能比缓冲区大;也不能用while (!feof(fp)),那个模式会多读一次、把最后一块数据重复拼进去,是老 C 代码里最常见的缺陷。raw.append(buf, n)用长度重载而不是strcat,是为了让 JSON 内容里出现的\0(虽然合法 JSON 里不会有,但坏数据里可能有)不会提前截断。

fileIsGbk这个参数看着别扭,但它反映现实:老系统的配置文件八成是 GBK 存的,新接口返回的八成是 UTF-8,两边都得吃得下。加个显式开关,比在函数里猜编码可靠得多——猜编码的代码,最后一定会猜错。

保存方向同理,先序列化成 UTF-8 字节,需要落 GBK 文件时再转一次,然后以wb打开写出。到这里,中文在整条链路上就有了明确的编码归属:进 jsoncpp 之前是 UTF-8,在 jsoncpp 里是 UTF-8,出来到界面之前才变 GBK。

4. 序列化输出:写出浏览器和脚本都能读的 JSON 文件

解析只是半条链路。老系统对外提供数据时,同样要生成 JSON。这一步的坑不在语法,在格式细节和文件 IO 上,而且这些问题在开发机上往往看不出来,到了对端才暴露。

4.1 StyledWriter 与 FastWriter 的选择

jsoncpp 提供两种 writer。StyledWriter输出带缩进和换行的可读格式,FastWriter输出压成一行,体积小、解析快。选择标准不是「哪个高级」,而是「这份 JSON 给谁看」:配置文件、需要人工排查的日志、给对方开发联调用的样例,用StyledWriter;走网络传输、走消息队列、单次几千条的高频数据,用FastWriter。

Json::Value root; root["code"] = Json::Value(0); root["msg"] = Json::Value(GbkToUtf8("保存成功")); Json::Value list(Json::arrayValue); for (int i = 0; i < 3; ++i) { Json::Value item; item["id"] = Json::Value(i + 1); item["name"] = Json::Value(GbkToUtf8("项目")); list.append(item); } root["data"] = list; Json::StyledWriter writer; std::string utf8Text = writer.write(root);

有两个地方容易出意外。第一,Json::Value的构造函数有多个重载,root["id"] = 1和root["id"] = Json::Value(1)在大多数情况下结果一样,但把int直接赋给Value时,某些老分支会走bool重载,导致整数1变成true。显式包一层Json::Value(int)能避开这个歧义,代价只是多敲几个字符。第二,FastWriter的输出末尾会带一个\n,如果你的代码要拿这个字符串去做 MD5 或者拼进 HTTP body,得先erase掉;StyledWriter不带这个尾巴。这一条不写在文档里,但对接签名校验时经常会咬人。

数组的构造要显式指定Json::arrayValue。不指定的话,一个空的Value序列化出来是null,不是[],对端拿到null再去做遍历就直接崩了。

4.2 文件必须以二进制模式打开

这一条是 VC6 时代最经典的问题,也是现在很多年轻工程师没听过的。C 运行时的文本模式("w"、"r")在 Windows 上会做两件事:写的时候把\n翻译成\r\n,读的时候反过来;更要命的是读的时候遇到0x1A(Ctrl+Z)会当成文件结束符,后面的内容全部丢掉。

JSON 是 UTF-8 文本,理论上不会出现0x1A,但实际数据里什么都有可能——某个字段里嵌了一段二进制摘要,或者用户输入里带了控制字符。一旦命中,读文件就会在中途静默停止,fread返回值变小,而你的代码如果没检查,就会拿着半截字符串去解析,报一个「* Line 217, Column 1」之类的语法错,然后你对着一个看起来完全正常的文件怀疑人生。

所以读写 JSON 文件,一律"rb"和"wb",没有例外。同理,\n的翻译也不要让它发生:JSON 里的换行属于数据内容,被悄悄改成\r\n会让字节长度变,签名和校验和全对不上。

bool SaveJsonFile(const char* path, const Json::Value& root, bool asGbk) { Json::StyledWriter writer; std::string text = writer.write(root); /* 内部一律 UTF-8 */ if (asGbk) text = Utf8ToGbk(text); FILE* fp = fopen(path, "wb"); /* 二进制模式,别用 w */ if (fp == NULL) return false; size_t written = fwrite(text.data(), 1, text.size(), fp); fclose(fp); return written == text.size(); }

fwrite之后要比较返回值,这一点在写网络盘或者磁盘满的时候真能救一次事故。fclose的返回值也该看,缓冲区里的数据是在fclose时才真正落盘的,它失败了你前面所有的检查都白做。老代码里大量的fclose(fp);后面直接return true;,我见过不止一次因此丢数据的。

4.3 double 输出与 setlocale 的暗坑

jsoncpp 序列化浮点数时,最终会走到sprintf或者ostringstream的数字格式化,而这两者都受 C 运行时的 locale 影响。如果程序里任何一处调用了setlocale(LC_ALL, "")——MFC 程序初始化时、某个第三方控件加载时、甚至某个同事为了正确显示日期随手加的一行——小数点符号就可能从.变成,。

后果是产出的 JSON 长这样:{"rate":3,14}。这在 JSON 语法里是两个元素,解析器会在,处断掉,报语法错。而且这个 bug 有极强的环境依赖性:开发机上没人调setlocale,一切正常;客户现场某个模块调了,全线崩。排查的时候你会盯着 JSON 生成代码看半天,完全想不到问题出在 locale 上。

防御方式有两个,建议都做。一是在序列化前后显式锁住数字 locale:

#include <locale.h> const char* old = setlocale(LC_NUMERIC, NULL); /* 先存旧值 */ setlocale(LC_NUMERIC, "C"); /* 小数点强制为 . */ std::string text = writer.write(root); setlocale(LC_NUMERIC, old); /* 还原,别影响别人 */

只锁LC_NUMERIC不锁LC_ALL,是因为你只关心小数点,动其他类别会影响日期、货币格式,反而制造新问题。二是所有对外输出的 JSON,在发出去之前做一次「合法性自检」——用一个独立的Json::Reader把它再解析一遍,解析不过就不发。多花的那点 CPU,比半夜被叫起来查接口强。

另外还有一个精度问题值得提前和对端对齐。jsoncpp 输出 double 默认用 17 位有效数字,0.1会写成0.10000000000000001。这个值是精确的、可逆的,但在文本上看着别扭,而且对端如果用单精度 float 接收再比较,会发现自己存的0.1和你给的字符串不相等。要么在协议上约定「浮点字段按字符串传」,要么约定统一保留几位小数,别让两边各自猜。

5. 避坑与排查:五条踩过的记录

下面这五条,是我在不同项目里真实撞过的。每一条都按「现象 → 原因 → 解决」写,方便你照着比对。

5.1 现象:解析第一行就报 Syntax error

现象:文件用记事本打开看着完全正常,第一行是{,但reader.parse返回 false,错误信息是* Line 1, Column 1 Syntax error: value, object or array expected。

原因:文件开头有 UTF-8 BOM,三个字节EF BB BF。Windows 记事本另存为 UTF-8 时默认加 BOM,而 JSON 规范不允许 BOM,jsoncpp 的解析器会把它当非法字符。因为 BOM 是不可见字符,用任何文本编辑器看都是「什么都没有」。

解决:在 parse 之前统一去掉开头三字节,就是 3.3 节那段判断逻辑。另一种做法是让文件生产方保证不带 BOM,但老系统里文件往往是人工用记事本改过的,靠约定不如靠代码兜住。顺带说一句,用十六进制编辑器打开文件看前三个字节,是排查这一类问题的标准动作,比盯着文本编辑器看有效得多。

5.2 现象:中文变成问号,或者原样显示 \u4e2d

现象:解析出来的字符串送进 MFC 的SetWindowText,界面上全是???;或者反过来,序列化出去的中文在对端变成\u4e2d\u6587这样的转义序列,对端要求的是明文。

原因:前一种情况是编码没转,UTF-8 字节被按 GBK 解释;后一种情况是对端用了严格模式或者做了二次转义,把原本已经是 UTF-8 的字节再按 ASCII 处理了一遍。还有一种隐蔽的:源文件被误存成 UTF-8 无 BOM,VC6 编辑器打开时显示乱码,你照着「乱码」改了一遍,编译进二进制的字节就成了「乱码的 UTF-8」,比原来更乱。

解决:把 3.1 节那条约定贯彻到底,进出 jsoncpp 都以 UTF-8 为准,界面层统一走一次Utf8ToGbk。源文件编码也要固定:VC6 工程的.cpp一律存 GBK,不要混。判断源文件是不是 GBK,用十六进制看「中文」两个字的字节应该是D6 D0 CE C4,如果是E4 B8 AD E6 96 87,那就是 UTF-8,得转回去。

5.3 现象:double 序列化出 3,14

现象:开发机上一切正常,客户现场生成的 JSON 对端解析失败,抓下来一看,浮点字段是{"rate":3,14}。

原因:程序某处调了setlocale(LC_ALL, ""),CRT 的数字格式化跟着系统区域设置走,小数点变成逗号。这个调用可能来自 MFC 初始化、某个 COM 组件、或者一个完全不相关的日期格式化函数。

解决:按 4.3 节的做法,序列化前后用setlocale(LC_NUMERIC, "C")夹住,结束再还原。另外在代码里全局搜一遍setlocale,把所有LC_ALL改成具体类别,能减少这类连锁反应。彻底一点的做法是重写 writer 的数字输出部分,不走 CRT 的格式化,但那样维护成本高,一般不值得。

5.4 现象:文件读到一半就截断

现象:一个 200KB 的 JSON 文件,fread只读到 80KB 就返回 0,后面内容丢了,解析报「* Line 512, Column 1」之类的错。

原因:用"r"文本模式打开的,文件中间某个字符串字段里含有0x1A字节,被 CRT 当成 EOF 标记。

解决:改成"rb"。同时检查读文件那段代码有没有写while (!feof(fp)),这个模式会多循环一次,把最后一块缓冲区重复 append 进去,症状是文件末尾多出一段乱码、解析报错位置在最后一行。正确的写法就是 3.3 节那种while ((n = fread(...)) > 0)。

5.5 现象:Debug 版正常,Release 版一进去就崩

现象:同样的代码,Debug 编译运行完全正常,Release 一跑就访问违例,崩溃位置在 jsoncpp 内部,调用栈看起来毫无道理。

原因:CRT 混用。jsoncpp 被编成了静态库(或者单独一个 lib),用的是/MTd,主工程用的是/MD,两边各自持有一份堆管理器。jsoncpp 里 new 出来的std::string缓冲区,被主工程的delete释放,堆结构当场损坏。Debug 版没崩,是因为调试堆的填充模式和释放检查更宽松,把这个错误掩盖了。

解决:Project → Settings → C/C++ → Code Generation → Use run-time library,把所有涉及到的工程和库设成完全一致。如果是第三方提供的 lib 改不了,就把 jsoncpp 源码直接编进主工程,不产生独立的 lib。另外 Release 下把优化关掉(/Od)再跑一遍,如果崩溃消失,基本可以确定是这类内存管理问题而不是逻辑错误。

6. 上线前的一次 round-trip 自检,和我现在的固定习惯

编码问题有个讨厌的特性:它在你看的那条路径上可能是对的,换个入口就错。所以我现在的习惯是,任何涉及中文的 JSON 读写,上线前必须跑一次 round-trip 自检——把一组边界样本写出去,再读回来,逐字节比对。

样本不用多,覆盖住就行:纯 ASCII、常见中文、带全角标点的中文、带引号和反斜杠的转义字符、空字符串、超长字符串(超过 4KB 缓冲区,专门测 3.3 节那个循环)、以及一个\u转义形式的中文。写出去再读回来,用==比原始串。这一步能同时验证编码转换、BOM 处理、文件模式和缓冲区拼接四件事,跑一次不到一毫秒。

比==更狠的一招是十六进制比对。把原始串和回读串各 dump 一遍,肉眼确认「中文」是D6 D0 CE C4(GBK)还是E4 B8 AD E6 96 87(UTF-8),别靠肉眼看着「显示正常」就下结论——很多控件做了容错,乱码也能显示出个大概,把人骗过去。

/* 一个够用的自检骨架,别用断言,失败要能继续跑完所有样本 */ bool RoundTripCheck() { const char* samples[] = { "ascii", "\xd6\xd0\xce\xc4", /* GBK 的"中文" */ "\xe4\xb8\xad\xe6\x96\x87", /* UTF-8 的"中文" */ "quote\" and backslash\\", "" }; for (int i = 0; i < sizeof(samples) / sizeof(samples[0]); ++i) { std::string utf8 = GbkToUtf8(samples[i]); /* 统一成内部编码 */ Json::Value root; root["v"] = Json::Value(utf8); Json::FastWriter w; std::string out = w.write(root); Json::Reader r; Json::Value back; if (!r.parse(out, back, false)) return false; if (back["v"].asString() != utf8) return false; } return true; }

还有一个我坚持了很多年的习惯:Input和Output两个方向的转换函数,绝对不在同一个函数体里混用。也就是说,所有进入 jsoncpp 的字符串必须已经转好,所有从 jsoncpp 出来的字符串在离开模块前必须转完。这个约束听着啰嗦,但它把「某个变量到底是什么编码」这个问题,从「随时可能冒出来的悬案」变成了「看一眼函数边界就知道」。代价是多写几行转换调用,换来的是不用再靠猜。

最后一句经验之谈:这套东西之所以值得单独整理成一份可运行的案例,不是因为它有多难,而是因为它把散落在编码、编译器、文件 IO、locale 四个角落的坑凑齐了。任何一个没踩过,都得花半天到一天去定位。从那次 Release 崩溃之后,我每接一个老工程的 JSON 需求,都强制先把 round-trip 自检跑通再写业务代码。希望帮到你。

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