简介:这是一份 JsonCpp 源码包,面向希望在 C++ 工程里直接解析、生成 JSON 数据的开发者,核心价值在于省去编译库的配置环节,把源码文件放进项目即可调用。压缩包共 38 个文件,以 14 个头文件和 5 个 C++ 源文件为主,另有 Visual Studio 工程配置、编译中间文件与调试记录,整体仅 1.11MB,轻量且目录结构清楚,适合快速集成到老版本 VS 项目。资源内头文件与实现源码分离,使用者可按需挑选所需部分,不必改动既有工程框架;附带的工程配置也有助于快速搭建测试环境。已有 961 人学习下载。包内除了核心的 JSON 解析、值对象与写出源码,还带有可直接参考的 Visual Studio 工程文件,能帮助理解 JsonCpp 的解析与序列化流程;遇到编译或链接问题时,也可借助工程配置快速核对头文件路径、源文件引用等关键设置,对源码级使用 JsonCpp 的 C++ 开发者比较实用。
1. JsonCpp 源码直用:绕开编译库,把解析器直接塞进项目
很多人拿到 JsonCpp 源码的第一反应是找预编译库或者跑一遍 CMake 生成工程,但当你面对一个老旧的 C++ 项目、ARM 交叉编译环境、或者公司内网不允许随意装依赖时,“编译库”这条路反而最折腾。JsonCpp 本质是一组 cpp/h 文件构成的解析器,源码直用就是把 jsoncpp.cpp 和头文件直接加入你的工程一起编译,不生成 dll/lib,不引入额外依赖,改代码还能直接生效。这份资源适合谁?需要在短时间内把 JSON 解析能力集成进既有工程、又不想学整套 CMake 流程的从业者;也适合喜欢把第三方代码完全掌握在手里的开发者。接下来我把拆包、接入、调参、踩坑的完整过程复现给你。
2. 源码直用前的准备:文件结构、版本与编译边界
2.1 需要哪些文件:JsonCpp 源码树的裁剪清单
JsonCpp 仓库拉下来后不要整个目录拖进项目,里面有很多测试、文档、CMake 脚本,编译时会拖入很多不需要的东西。我通常在 include/json 里保留 json.h、json-forwards.h、json-value.h、json-reader.h、json-writer.h 等头文件,以及 src/lib_json/json_reader.cpp、json_value.cpp、json_writer.cpp,外加一个关键的 jsoncpp.cpp(这是合并后的单文件实现)。注意新版 JsonCpp 的源码结构会有差异,老版本 0.x 和新版 1.x 头文件分布不同,但核心源码文件始终是 reader、value、writer 三兄弟。裁剪后整个源码部分大概 7 个文件,非常适合直接挂进 Visual Studio 或 Makefile 工程。
为什么不做成 .lib?因为很多嵌入式交叉编译工具链对 C++ 异常、RTTI 的开关和你现有项目不一致,预编译库是“黑匣子”,一旦某个宏定义不对,崩溃了连查的地方都没有。源码直用反而可以把 JSON_CPP_USE_EXCEPTION、JSON_USE_IMPLICIT_CONVERSIONS 这些宏直接放在工程预处理里,随时试错。
下面是裁剪后的文件清单参考:
| 文件 | 用途 | 是否必需 |
|---|---|---|
| include/json/json.h | 总入口头文件 | 必需 |
| include/json/json-forwards.h | 前向声明,减少编译依赖 | 必需 |
| include/json/value.h | Json::Value 类定义 | 必需 |
| include/json/reader.h | 解析器定义 | 必需 |
| include/json/writer.h | 写入器定义 | 必需 |
| src/lib_json/json_reader.cpp | 解析逻辑实现 | 二选一 |
| src/lib_json/json_value.cpp | Value 存储实现 | 二选一 |
| src/lib_json/json_writer.cpp | 写入逻辑实现 | 二选一 |
| jsoncpp.cpp | 单文件合并版实现 | 二选一 |
如果是从 GitHub 官方仓库下载,src/lib_json 下通常没有 jsoncpp.cpp,只有三个 cpp 文件,此时用三个 cpp 即可。如果下载的是 amalgamated(合并版)包,里面直接有 jsoncpp.cpp 和单个头文件,这种更省事。但无论哪种,头文件不要从两个地方混用,否则可能出现重复声明或版本错位。
2.2 先搞懂构建原理:编译库和源码直用的本质区别
我把这两者的差别简化为“编译时机”和“ABI 绑定”两个点。编译库方式会在项目的 CMake 中先编译 libjsoncpp.so/windows 的静态库,然后主工程通过 target_link_libraries 链接;而源码直用是把 jsoncpp.cpp 编入主工程的目标文件,随后和业务代码一起链接。由于 JsonCpp 的头文件里大量使用 inline 和模板,如果封装成动态库,头文件版本和库版本出现偏差,就会出现链接错误或者运行期访问违例。源码直用彻底消除了这种“版本不一致的 ABI 问题”。
你不能把 src/lib_json 下的所有 cpp 一股脑拖进来,因为某些版本里已经包含了 jsoncpp.cpp 的全部内容,重复编会符号重定义。如果从 GitHub 下载的源码在 src/lib_json 里只有三个 cpp,那 jsoncpp.cpp 就是可选的;如果下载的是 amalgamated 版本,文件叫 jsoncpp.cpp,那只需加这一个 cpp 和 include/json 下的头文件。
还有一个容易忽略的点:JsonCpp 的头文件对 C++ 标准有硬性要求。新版代码用了 std::unique_ptr、std::move 等 C++11 特性,如果你的工程还停留在 C++98,就得换用旧版本源码,或者手动补几个智能指针。我遇到过一个项目用 VS2008 编译,默认 _MSC_VER 不支持 C++11 的某些语法,最后降级到 0.10.6 版本源码才通过。所以拿到源码后第一件事是看头文件开头的#if defined(__cplusplus) 判断,确认你的编译器能通过。
2.3 环境准备与验证:先跑通最小读写
在加入项目之前,先用命令行把源码本身验证一遍。我一般会建一个最小测试目录,只放 jsoncpp.cpp、json.h、main.cpp,用 g++ 编译:
g++ -std=c++11 -I./include main.cpp jsoncpp.cpp -o test_json这里的 -std=c++11 是 JsonCpp 新版本的最低要求,老版本 0.7.x 用 c++03 也能编,但 1.9 之后很多函数用到了 override 和 move 语义,建议直接开 c++11。如果编译失败,先看是不是 -I 路径写错,或头文件里引用了其它内部头文件。接着最小测试程序:
#include "json/json.h" #include <iostream> int main() { Json::Value root; Json::Reader reader; std::string text = "{\"hello\":1}"; bool ok = reader.parse(text, root); std::cout << ok << " " << root["hello"].asInt() << std::endl; return 0; }parse 的第一个参数是内容,第二个是输出根节点,返回布尔值表示解析是否成功。这里没有打开异常,所以失败不抛异常而是返回 false,适合先确认源码能否正常编进程序。如果你要用新版 API,也可以这样写:
#include "json/json.h" #include <sstream> #include <iostream> int main() { std::string text = "{\"hello\":1}"; Json::Value root; Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); std::string errs; const char* begin = text.c_str(); const char* end = begin + text.size(); bool ok = reader->parse(begin, end, &root, &errs); if (ok) { std::cout << "hello=" << root["hello"].asInt() << std::endl; } return 0; }为什么用 CharReaderBuilder?这是 1.x 推荐的方式,它支持通过 builder["collectComments"] = false 关闭注释收集,还能自定义严格模式。而老式 Json::Reader 在新版本中标记为 deprecated,但源码里仍然兼容,初学者可能会看到两套 API 混搭的代码,容易晕。我的建议是:既然源码直用,就用新版 API,以后升级不用改代码。
3. 把 JsonCpp 源码塞进你的项目:两种主流接入方式
3.1 方式一:直接把源码文件加入工程
这是最简单的路子。拿 Visual Studio 举例,在解决方案资源管理器里找到你的项目,右键“添加现有项”,把 jsoncpp.cpp 和需要的头文件加进来,然后在项目的预处理器定义里加上 JSON_USE_IMPLICIT_CONVERSIONS(如果要用隐式转换)等宏。之后在任何业务 cpp 里写 #include "json/json.h" 就能用了。
使用 Makefile 时,把 jsoncpp.cpp 加进 OBJ 列表即可。我习惯单独维护一个 jsoncpp_src 目录,避免把第三方源码和业务代码混在一起。比如:
CXX = g++ CXXFLAGS = -std=c++11 -Wall -I./third_party/jsoncpp/include SRCS = main.cpp network.cpp third_party/jsoncpp/src/lib_json/json_reader.cpp \ third_party/jsoncpp/src/lib_json/json_value.cpp \ third_party/jsoncpp/src/lib_json/json_writer.cpp OBJS = $(SRCS:.cpp=.o) all: my_app my_app: $(OBJS) $(CXX) $(OBJS) -o $@ clean: rm -f $(OBJS) my_app这里注意,Makefile 里如果用了旧版本源码,可能需要加上 -DJSON_NOEXCEPTION=0 之类的宏。CXXFLAGS 中的 -I 一定要指向 include 目录的上一级,这样代码里的 #include "json/json.h" 才有效。如果 -I 指到 include/json,那么你的 include 语句得改成 #include "json.h",但内部头文件之间引用是 "json/json-forwards.h",这样就会找不到,所以最稳妥是 -I/.../include。
3.2 方式二:用 CMake 的 add_subdirectory 集成
如果项目本身就基于 CMake,源码直用更规范的做法是把 JsonCpp 源码目录作为子目录加入。前提是你裁剪后的 JsonCpp 目录里有一个最小 CMakeLists.txt,或者在主工程里直接用绝对路径。
add_library(jsoncpp STATIC ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_reader.cpp ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_value.cpp ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/src/lib_json/json_writer.cpp ) target_include_directories(jsoncpp PUBLIC ${CMAKE_SOURCE_DIR}/third_party/jsoncpp/include ) target_compile_definitions(jsoncpp PUBLIC JSON_USE_IMPLICIT_CONVERSIONS=1 )这种方式的好处是把 JsonCpp 隔离成一个目标,主工程只要 target_link_libraries(my_app jsoncpp) 即可。但注意 add_subdirectory 是另一条路,它是用 JsonCpp 自带的 CMakeLists.txt。如果下载的源码包里有官方 CMakeLists,直接在主工程写 add_subdirectory(third_party/jsoncpp) 也行,但官方写法会生成 jsoncpp_lib 等目标,名字可能和你预想的不同,建议先 build 一次看输出。参数说明:STATIC 表示静态库,但我们目的不是产出 lib,而是让 CMake 把它编进主工程;PUBLIC 包含路径会对使用方传递。
如果官方 CMakeLists 里又开了 BUILD_SHARED_LIBS 选项,生成的目标可能是动态库,和“源码直用”的初衷相悖。我在实际项目中更倾向自定义一个 add_library,不依赖官方 CMake,因为官方脚本还会链接 Windows 的 ws2_32 等系统库,搞复杂了看着晕。
3.3 头文件路径与宏定义:不踩 Namespace 坑
JsonCpp 的声明全部在 Json 命名空间内,头文件包含路径决定你的 include 写法。如果 -I 指向 include 目录,那么代码用 #include "json/json.h";如果直接把 include/json 加进包含路径,则用 #include "json.h"。这两种写法混用最容易被坑,因为某些旧版本头文件之间用相对路径互相引用,改 include 搜索顺序可能导致找不到 json-forwards.h。
宏方面,先记两个最常用的:
- JSON_NOEXCEPTION:禁用异常,解析出错时用 errorMessage() 拿错误信息。这个宏定义后,JsonCpp 内部不会抛出异常,但你的代码必须检查每个操作是否成功,否则遇到非法 JSON 时会出现未定义行为。
- JSON_USE_IMPLICIT_CONVERSIONS:把 Json::Value 和 int/double/bool 做隐式转换,如果你的代码经常写 int x = root["age"]; 就得开这个宏。如果不定义,必须用 asInt() 显式转换。
还有一个宏 JSON_USE_EXCEPTION,老版本里用来开启异常,新版本已经改名。我在源码直用时的经验是:默认不开 JSON_NOEXCEPTION,让异常开着,这样访问不存在的 key 时能立刻暴露错误;等程序稳定后再考虑关掉。因为 JsonCpp 的 operator[] 如果越界访问,异常模式会告诉你哪一行,而静默模式可能返回一个默认构造的 Value,后续 asInt() 得到 0,问题被掩盖。
4. 实战:用 JsonCpp 源码完成一个配置文件的解析与回写
4.1 目标与场景:用 JsonCpp 读写程序配置
在实际嵌入式项目中,JSON 配置常被拿来替代 ini 文件,因为支持嵌套。这里我们做一个模拟项目X的配置读写:程序需要从 config.json 读取服务器地址、端口、日志等级和重试次数,然后修改日志等级后保存回文件。用 JsonCpp 源码直用,不依赖任何外部库。
这个场景很典型:程序启动时读取配置,运行中可能修改配置,退出前写回。如果使用编译库方式,每次构建都要依赖一个 .lib 文件,换环境就得重新编库;源码直用则把 json_reader.cpp 直接编进主程序,交叉编译时只要工具链支持 C++11,就不会有额外负担。
4.2 代码实现:解析、修改、保存
先定义配置文件内容(config.json):
{ "server": "192.168.1.10", "port": 8080, "log": { "level": "info", "max_size_mb": 16 }, "retry": 3 }接下来是解析与回写的完整代码:
#include "json/json.h" #include <fstream> #include <iostream> #include <memory> bool loadConfig(const std::string& path, Json::Value& root) { std::ifstream ifs(path); if (!ifs.is_open()) { std::cerr << "open failed: " << path << std::endl; return false; } Json::CharReaderBuilder builder; builder["collectComments"] = false; std::string errs; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); bool ok = reader->parse( static_cast<std::istreambuf_iterator<char>>(ifs), static_cast<std::istreambuf_iterator<char>>(), &root, &errs); if (!ok) { std::cerr << "parse err: " << errs << std::endl; } return ok; } bool saveConfig(const std::string& path, const Json::Value& root) { std::ofstream ofs(path); if (!ofs.is_open()) return false; Json::StreamWriterBuilder builder; builder["indentation"] = " "; std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); writer->write(root, &ofs); return true; } int main() { Json::Value cfg; if (!loadConfig("config.json", cfg)) return 1; int port = cfg["port"].asInt(); std::string level = cfg["log"]["level"].asString(); std::cout << "port=" << port << " log.level=" << level << std::endl; cfg["log"]["level"] = "debug"; cfg["retry"] = 5; if (!saveConfig("config_out.json", cfg)) return 1; return 0; }这里用了 CharReaderBuilder/StreamWriterBuilder 这套新 API,而不是老版的 Reader/Writer。原因是从 JsonCpp 1.0 开始官方推荐 builder 模式,它允许你通过 builder["属性"]="值" 设置特性,比如 collectComments 控制是否把注释收集到 Value 里。parse 使用 istreambuf_iterator 来读取整个流,避免了用 FileReadStream 的繁琐。注意 JSONCPP_STRING 是 JsonCpp 内部对 std::string 的宏封装,在老版本里可能是 std::string,直接写 std::string 也可以,但用这个宏兼容自定义字符串类型。
builder["indentation"] = " "是设置输出缩进为两个空格。StreamWriterBuilder 还能设置其它属性,比如 "emitUTF8" 用于控制非 ASCII 字符的输出方式,默认 true 按 UTF-8 原样输出;"precision" 控制浮点输出精度,默认是足够多的有效数字。这些属性都可以通过 builder["key"] 传值,但必须写对字符串键名,写错不会报错,只是用默认值。
4.3 内存与性能调优点:写入缩进与 float 精度
保存配置时默认的输出是压缩格式,所有人挤一行,不方便人工查看。StreamWriterBuilder 的 indentation 可以指定缩进字符串,我这里设为两个空格,也可以设成 "\t"。如果要输出紧凑格式,直接空字符串。还有一个新手容易踩的精度坑:Json::Value 内部保存 double,如果原始 JSON 里写 0.10,解析后 asDouble 可能变成 0.1,回写时 rtValue 会把 0.1 写成 0.1 而非 0.10,这对金额或科学计算可能无法接受。解决方法是回写前用自定义类型或字符串保存原始值,或者在 Value 里存 string 而不是 double。
在实际项目中,我经常看到有人把整型也存成 double,然后在 asInt 时发现精度丢失。JsonCpp 的 Value 类型有 int、uint、int64、uint64、double 几种,写入时如果 root["port"] = 8080; 默认按 int 保存,但如果你先写了一个 double 再改 int,可能类型混乱。最好的做法是构造值的时候显式调用 Json::Value(8080) 或直接赋值,避免混合运算导致类型变化。
5. 避坑与常见问题:源码直用时的五个血泪点
5.1 坑一:符号重复定义——多文件加入导致的链接错误
现象:链接期报错,大量 “json_value.cpp.obj : error LNK2005: xxx already defined in jsoncpp.cpp.obj” 或者 g++ 下多个重复符号。
原因:同一份源码既有 jsoncpp.cpp 又把 lib_json 下的三个 cpp 全部加入工程,导致同一个类的实现被编译了两次。有些下载包版本同时提供单文件版和分离版,新手容易两个都加。
解决:检查工程项目里是否同时包含 jsoncpp.cpp 和 src/lib_json/*.cpp,只保留一组。如果是从 GitHub 主仓库拉的源码,src/lib_json 下本身没有 jsoncpp.cpp,这种直接用三个 cpp 即可。如果是从 amalgamated 包下载,只有 jsoncpp.cpp,那就用它,不要再去单独加三个 cpp。我一般会在工程文件里搜索 “json_reader.cpp”,如果发现有两处路径不同,就要立刻停下来确认。
5.2 坑二:版本不一致——新旧 API 混用
现象:代码里用了 Json::Reader 和 Json::Writer,编译通过但运行时行为怪;或者用 json/autolink.h 却找不到符号。
原因:不同版本 JsonCpp 的 API 差异很大。0.6 时代没有 CharReaderBuilder,1.9 时代又把 Reader 标记为 deprecated。如果从网上随手下的示例代码用了新 API,而源码包是旧版,直接翻车。
解决:统一版本。我一般用 1.9.x 源码包,并把代码里所有 Reader/Writer 换成 CharReaderBuilder/StreamWriterBuilder。如果必须用老 API,就在 include 之前定义宏 JSONCPP_DEPRECATED,但这不是长久之计,最好跟着新版改。换版本时,还要注意头文件里是否增加了新的宏定义,比如 JSONCPP_OP_EXPLICIT,这个宏在不同版本里控制 explicit 关键字,老编译器不认识会报错。
5.3 坑三:大小写敏感的头文件路径
现象:在 Windows 上编译没问题,切到 Linux 后报错 “fatal error: json/JSON.h: No such file or directory”,或者在某些旧工程里 include “json/value.h” 说找不到。
原因:Windows 文件系统不区分大小写,而 Linux 区分。JsonCpp 官方头文件是 json/json.h,全部小写;但很多教程写 json/Json.h,或者项目里缓存了旧头文件名。
解决:统一头文件路径为小写。且在剪裁源码时,保留 include/json 目录下的原始文件名,不要复制到别的目录后改扩展名。我在项目里一般会全局搜索 #include "json/" 确认全部是小写。还有一个细节:有些版本已经把头文件从 include/json 移动到了 include/jsoncpp/json,如果你下载的资源来自某个特定整合包,include 路径可能需要写 jsoncpp/json/json.h,这时候不要死板地坚持 json/。
5.4 坑四:中文编码与 UTF-8 BOM
现象:解析含有中文的 JSON 文件,输出乱码;或者在 Windows 下用记事本编辑保存后,jsoncpp 解析报错。
原因:文件保存成了 GBK,或者带 BOM 的 UTF-8。JsonCpp 解析器按 UTF-8 处理,GBK 中文会被当作非法的 UTF-8 序列。
解决:用 Notepad++ 或 VS Code 把文件转成 UTF-8 无 BOM,并且程序读入时统一按 UTF-8 处理。如果必须在运行时处理 GBK,需要先转码再传给 JsonCpp。另外,Visual Studio 的编译器在处理字符串字面量时存在本地代码页问题,在代码里写 std::string s = "中文"; 最终 s 的编码取决于源文件保存编码,建议源文件统一保存成带签名 UTF-8 或纯 UTF-8。
解析出错时的错误信息位置很重要。我用 CharReaderBuilder 时,parse 失败后 errs 里会包含行号和列号,比如 "line 1:0: undocumented token"。这个 errs 可能是一长串字符串,但实际有用的就第一行。我调试时习惯把 errs 完整打出来,不要只打 errs.c_str(),因为内部可能有多个错误累积。
5.5 坑五:异常开关——禁用异常后怎么处理
现象:定义 JSON_NOEXCEPTION 后,解析格式错误时程序崩溃或静默失败,找不到错误信息。
原因:JsonCpp 内部很多地方依赖异常来报告错误,比如 Value::operator[] 访问不存在的 key 时,默认会抛异常。如果关掉了异常,它只能走 assert 或返回空值,行为改变。
解决:首选不定义 JSON_NOEXCEPTION,一直启用异常;在嵌入式环境实在不能用异常时,代码里必须提前用 isMember() 或 isNull() 检查 key 是否存在,并用 parse 的返回值和 errorMessage() 处理解析错误。我的习惯是:用 isMember 检查所有必要字段,否则程序直接返回退出,绝不能假设 key 一定存在。
举个例子,配置文件中如果 server 字段缺失,直接访问 cfg["server"] 会得到一个 null Value,asString() 返回空字符串,后续连接时可能莫名其妙失败。更好的写法是:
if (!cfg.isMember("server") || !cfg["server"].isString()) { std::cerr << "config missing server" << std::endl; return 1; }isMember 和 isString 是两类不同检查:isMember 判断 key 是否存在,isString 判断值类型。有些新手只用 isMember,但 true 对应的可能是数字类型,asString 一样会得到空串,必须两个都验。
6. 进阶技巧:把 JsonCpp 源码封装成自己的工具模块
把第三方源码直接编进项目后,下一步就是减少业务代码对 JsonCpp API 的直接依赖。我一般会再封装一个 util 头文件,让所有 Json 操作走自己的薄接口。
6.1 封装一个简单的 Json 工具类
// json_util.h #pragma once #include "json/json.h" #include <string> #include <optional> namespace app { bool parseJson(const std::string& text, Json::Value& root); std::optional<std::string> getString(const Json::Value& root, const std::string& key); int getInt(const Json::Value& root, const std::string& key, int def); std::string dumpJson(const Json::Value& root, bool pretty); }实现里用 CharReaderBuilder 解析、StreamWriterBuilder 输出,这样业务层永远只面对 std::string 和处理好的 Value。遇到缺字段时 optional 返回空,业务侧不再关心抛异常的细节。这个封装对团队协作很有用:把 ConfigManager 类的一次性解析逻辑集中在 load 函数里,后续配置文件格式变化只改一处。
具体实现时,parseJson 只需要几行:
bool parseJson(const std::string& text, Json::Value& root) { Json::CharReaderBuilder builder; std::unique_ptr<Json::CharReader> reader(builder.newCharReader()); std::string errs; return reader->parse(text.data(), text.data() + text.size(), &root, &errs); }这段代码里我没有把 errs 返回出去,如果调用方想知道失败原因,可以重载一个带 errs 参数的版本。dumpJson 里则通过 pretty 参数切换缩进:
std::string dumpJson(const Json::Value& root, bool pretty) { Json::StreamWriterBuilder builder; builder["indentation"] = pretty ? " " : ""; std::ostringstream oss; std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter()); writer->write(root, &oss); return oss.str(); }注意 string compaction 模式下,indentation 设为 "" 即可。StreamWriter 默认会在写完最后一个字符后输出换行,如果你不需要末尾换行,可以在写入后把字符串的末尾 '\n' 去掉。
6.2 快速验证脚本与后续维护习惯
拿到任何 JsonCpp 源码包,我建议先做一个 10 分钟可跑的 smoke test,就是第 2 章那个最小程序。把它放在 demo 目录,编译运行确认输出符合预期后,再去干别的。之后每次把源码升级不敢偷懒,强制走一遍这个验证,再跑到项目里跑回归。从那以后,我每次裁剪源码和换版本都会执行一遍这个流程,有效避免了新版本 API 迁移带来的隐性问题。希望帮到你。
本文还有配套的精品资源,点击获取