
简介这是一份可直接运行的 TinyXML C 解析示例工程面向需要在 C 项目中快速集成 XML 读写能力的开发者。包内包含完整的 Visual Studio 解决方案与源码文件演示了 TinyXML 的文档加载、节点遍历、属性读写及保存输出等核心接口的典型用法并附带一个客户端调用示例同时覆盖解析错误判断与自动内存管理要点能直观展示该库在配置解析、数据交换等常见场景中的集成方式核心实现与接口头文件齐备便于对照学习。资源共 30 个文件以 C 源文件与头文件、Visual Studio 工程配置文件及调试编译产物为主整体约 7.27MB压缩包内含解决方案文件、编译日志与调试符号目录结构清晰便于直接打开工程查看运行效果。已有 217 人学习下载适合希望避开复杂 XML 框架、快速上手轻量级 XML 解析方案的 C 学员或项目开发者也可作为入门级 XML 库的代码参考。1. 处理C项目里的XML为什么我最后选了TinyXML如果你的C项目里需要读一个XML配置文件或者要把游戏存档、界面布局、协议数据做成XML格式头一个绕不开的问题就是“用什么库”。用正则手搓解析遇到嵌套节点和实体转义就是给自己埋雷上重量级库又觉得配置解析这点事不值得拉那么大的依赖。TinyXML注意是tinyxml c不是TinyXML2就是在这个尴尬位置上的老牌选择整个库就几个.cpp和.h文件拷进工程直接编译没有动态库也没有其他依赖API风格接近DOM非常适合“读配置、改配置、写配置”这类中小型XML场景。它解决的痛点很具体让你用十来行代码把XML文件变成一棵能遍历和修改的内存树然后再用同样简单的接口把改动写回磁盘。适合谁适合需要快速落地、又不想在第三方依赖上花太多时间的C开发者也适合学习DOM模型时拿来做精简案例。这篇笔记我会从解析模型讲到读写实战再把这些年踩过的编码、属性格式、内存管理的坑一并列出来尽量让你少走弯路。2. TinyXML的解析模型先理解DOM树再写代码不然就是边写边猜2.1 四类节点与两个加载入口TinyXML的解析结果是一棵DOM树。根不是文件本身而是一个XMLDocument对象下面挂着节点。节点有四种基本类型XMLDeclaration声明头就是?xml version1.0?那行、XMLElement元素节点比如config、XMLText纯文本内容、XMLComment注释。实际用的时候你九成时间都在跟XMLElement打交道。加载XML有两个常见入口老版本经常用TiXmlDocument::ParseFile但我习惯先用LoadFile因为返回值能直接告诉我是文件层面的失败还是解析层面的失败#include tinyxml.h int main() { // 1. 创建文档对象 TiXmlDocument doc; // 2. 加载文件失败会返回false if (!doc.LoadFile(config.xml)) { // 此时doc.ErrorDesc()能看到具体错误描述 fprintf(stderr, 加载失败: %s\n, doc.ErrorDesc()); return -1; } // 3. 成功时拿根元素 TiXmlElement* root doc.RootElement(); if (!root) { fprintf(stderr, 没有根元素\n); return -1; } // 4. 输出根元素名 printf(根元素: %s\n, root-Value()); return 0; }LoadFile内部会完成文件读取和字节流解析两步所以返回值非零不代表XML语法一定合法也可能只是文件名写错或没有读权限。代码里第2步后的doc.ErrorDesc()很关键能区分“文件不存在”和“第几行第几个字符出错”两类错误这是我最先强调的一个调试手段。2.2 加载字符串不只是LoadFile一种路子实际工程里XML不一定来自文件可能是网络上收到的字符串或是把加密后的配置解出来的缓冲区。这时LoadFile不够用得走字符串入口const char* xmlStr server host127.0.0.1 port8080/; TiXmlDocument doc; doc.Parse(xmlStr); // 注意解析的是内存字符串 if (doc.Error()) { printf(解析字符串出错错误ID%d位置%d行\n, doc.ErrorId(), doc.ErrorRow()); return -1; } TiXmlElement* root doc.RootElement(); const char* host root-Attribute(host); int port 0; root-Attribute(port, port); printf(host%s, port%d\n, host, port);Parse的参数是const char*它会直接拿这段内存做解析不会拷贝一份出去所以调用方要保证字符串在文档对象存活期间是有效的。这一点写网络通信模块时特别容易踩把收到的std::string的c_str()丢给Parse然后string对象析构了等你要读属性时里面的指针已经指向非法内存。2.3 遍历节点从根往下的三种走法DOM树建好之后遍历是最常用的操作。TinyXML没有迭代器但提供了三种指针移动接口。第一种是FirstChildElement搭配NextSiblingElement适合找同级的元素兄弟第二种是FirstChild搭配NextSibling这是节点级遍历会把文本节点、注释节点也带上第三种是FirstChildElement一直往下走适合找深层元素。// 从根元素开始遍历它的直接子元素只找元素跳过文本节点 TiXmlElement* child root-FirstChildElement(); while (child) { printf(子元素: %s\n, child-Value()); // 对每个子元素再遍历它的属性 const TiXmlAttribute* attr child-FirstAttribute(); while (attr) { printf( 属性 %s %s\n, attr-Name(), attr-Value()); attr attr-Next(); } child child-NextSiblingElement(); }NextSiblingElement后面的括号里不带参数表示“找下一个同级元素无论元素名是什么”。如果你只想找叫item的兄弟节点就传名字NextSiblingElement(item)它会帮你跳过中间不匹配的元素节点。这里有个细节FirstChild返回的是TiXmlNode*你要访问元素专属接口时得自己ToElement()转换而FirstChildElement直接返回TiXmlElement*在“只想处理元素”的场景里更顺手。我用下来最大的感受是只要你不是在做文本提取就别用FirstChild少一次类型判断就少一处空指针风险。3. 读XML配置提取属性与文本的完整套路3.1 提取属性Attribute家族函数的四张脸XML里属性是信息密度最高的部分比如数据库连接串、窗口大小、开关标志全塞在config的attribute里。TinyXML的Attribute函数有好几个重载版本用对了能省掉手写字符串转换的功夫。// 第一个返回字符串属性不存在时返回NULL const char* name elem-Attribute(name); if (name) { printf(name: %s\n, name); } // 第二个转int找不到属性或转失败时返回false int width 100; if (!elem-Attribute(width, width)) { printf(width不是合法整数已用默认值100\n); } // 第三个转double返回bool double ratio 0.5; bool ok elem-Attribute(ratio, ratio); // 第四个只判断有没有这个属性 if (elem-Attribute(enabled)) { printf(enabled存在值是什么不重要\n); }第二个重载是bool Attribute(const char* name, int* dest)它做的事情比我一开始想象的多先找到属性节点再调用sscanf做字符串转整型失败时返回false但不会改动dest的值。所以调用之前给变量赋默认值是规范动作。需要注意字符串型属性也有可能做成NULL取值前千万要判空——我见过有同事直接const char* v elem-Attribute(version); printf(%s, v);运行时直接崩溃原因就是那个节点根本没有version属性。3.2 提取文本内容GetText与FirstChild的取舍元素节点里夹着的文本常见写法是title配置文件/title取这个“配置文件”四字GetText是最省事的TiXmlElement* titleElem root-FirstChildElement(title); if (titleElem) { const char* text titleElem-GetText(); if (text) { printf(标题文本: %s\n, text); } }GetText内部会去找第一个TiXmlText子节点返回它的Value()。它的一个隐患是如果元素下不只有文本还夹着其他子元素或注释GetText依然只返回第一个文本节点不会帮你拼出全部文本。比如这种结构description 这是一段 b加粗/b 的说明 /descriptionGetText只会返回“这是一段”后面“的说明”被b隔开了它取不到。这时候要拿完整文本就得自己遍历子节点拼接或者干脆要求上游改格式把文本集中存放。我实际处理这种半结构化文本时更倾向于用递归去拼而不是依赖GetText的取巧结果。3.3 遍历同级兄弟节点NextSiblingElement带参数的行为差异解析配置时经常遇到重复的同名节点比如存放坐标点的point x1 y2/连续出现。这时控制循环的NextSiblingElement就是关键了。TiXmlElement* pt root-FirstChildElement(point); while (pt) { int x 0, y 0; pt-Attribute(x, x); pt-Attribute(y, y); printf(点(%d, %d)\n, x, y); pt pt-NextSiblingElement(point); }带参数point的版本会从当前位置的下一个兄弟开始找逐节点跳过非point的元素直到找到匹配或到末尾返回NULL。不带参数的版本返回任意元素名的下一个兄弟节点两种写法在循环里的行为差异很容易混淆。我个人的习惯是凡是要遍历同名列表一律带名字只有在“无论什么元素都处理一遍”的时候才用无参版本。这样至少能省掉一半“莫名其妙丢了一个节点”的排查时间。4. 改XML并写回修改、新增、删除的落地代码4.1 修改属性与文本SetAttribute和SetValue的边界读配置只是基本功工具类程序通常还要把修改后的结果写回文件。修改属性值的接口是SetAttribute它有几个重载各有各的坑TiXmlElement* node root-FirstChildElement(server); // 修改已有属性字符串版本 node-SetAttribute(host, 192.168.1.10); // 修改已有属性整数版本内部转成字符串 node-SetAttribute(port, 9090); // 修改文本内容必须拿到XMLText子节点再调用 TiXmlNode* child node-FirstChild(); if (child child-ToText()) { child-ToText()-SetValue(新文本内容); }SetAttribute如果遇到属性名不存在的情况会创建新属性节点挂在元素下面不是报错。这个特性有时很方便比如动态增加配置项但它有个副作用——如果你把属性名拼写错了程序不会提示而是默默创建一个新属性旧值原封不动留着。所以改完最好把整个节点打印出来确认一下属性数量别让“你以为改了”变成“完全没改”。SetValue是TiXmlNode共有的接口元素也可以用但那时改的是元素名。要改元素夹着的文本务必先取ToText()拿到文本节点再调SetValue。如果这个元素下没有文本节点比如写成server/这种自闭合FirstChild()会返回NULL直接解引就崩了。我遇到这类需求更稳妥的做法是先把子节点清掉再新增一个文本节点后面写节点时你会看到这个模式。4.2 新增子节点与保存文件LinkEndChild的职责新增节点是配置工具最核心的需求比如往白名单里加一条记录。整个过程分三件事创建元素、填充属性、挂接父节点。// 1. 创建新元素 TiXmlElement* newItem new TiXmlElement(item); newItem-SetAttribute(id, 1001); newItem-SetAttribute(enabled, true); // 2. 设置文本注意这里不能直接用newItem-SetValue那是改元素名的 TiXmlText* text new TiXmlText(可执行文件); newItem-LinkEndChild(text); // 3. 挂到根节点下 root-LinkEndChild(newItem); // 4. 保存 if (!doc.SaveFile(config.xml)) { fprintf(stderr, 保存失败: %s\n, doc.ErrorDesc()); }LinkEndChild的工作是把这个节点接到父节点的子节点链表末尾同时做两件事设置父子关系指针并把节点所有权转交给父节点。一旦LinkEndChild成功父节点析构时会连带释放它调用方不要再手动delete这个元素否则就是双重释放。你要是自己管理过复杂对象的生命周期就明白这种“所有权转移”语义在XML树里意味着什么——new出来的节点挂上之后你就没有释放义务了但也只有树本身知道它什么时候死。4.3 删除节点与文档清理怎么写才能不崩删除节点比新增更容易翻车。TinyXML的RemoveChild只是把节点从链表中摘下来并不负责释放内存。要想彻底删除并释放标准动作是拿返回的指针手动deleteTiXmlElement* target root-FirstChildElement(temp); if (target) { TiXmlNode* removed root-RemoveChild(target); delete removed; // 手动释放否则内存泄漏 } // 保存 doc.SaveFile(config.xml);反过来如果你只想把节点挪到别的父节点下面就不要delete直接RemoveChild后LinkEndChild到新父节点即可。还有一点TiXmlDocument析构会递归释放所有节点所以你在主流程里new出来的节点如果到析构时还没挂到树上那就是泄漏。常见坑是把只读解析用的TiXmlElement*拿来new——这通常是别的代码里已经存在的节点你直接delete会拖垮整棵树。结论很简单非新建对象不要delete新建对象一旦挂上树也不要delete。5. 中文编码与属性格式三大杀手与排查清单5.1 中文乱码声明与文件编码不匹配TinyXML对编码这件事的处理非常简单粗暴它把输入字节流按UTF-8理解不转码不校验。一旦文件头部声明的编码和实际字节不一致所有中文都会出问题。最常见的是在Windows下用记事本把XML存成了ANSIGBK而文件里写的却是?xml version1.0 encodingUTF-8?TinyXML按UTF-8解析中日韩字符全变乱码。排查这个问题的顺序我一般固定三连先用十六进制工具看文件头几个字节确认有没有EF BB BFUTF-8 BOM再查声明行的encoding属性写的是什么最后用doc.ErrorDesc()看解析时有没有报错。改动方案也明确要么把文件另存为UTF-8编码并且声明保持一致要么改声明为实际编码。但TinyXML内部对非UTF-8支持得非常弱我的建议是统一UTF-8无BOM这是所有跨平台场景下的最小公倍数。5.2 Windows换行符与BOM问题肉眼看不到的陷阱Windows上按回车键产生的换行是\r\nLinux上是\n。TinyXML解析时对待\r\n的策略是把\r当空白字符处理一般不会出错但写文件时如果你用SaveFile它默认按当前平台的换行风格输出。如果同一个配置文件轮番在Windows和Linux上修改保存文件里的换行会变得很脏万一你拿校验工具比对文件肉眼看着一样哈希值就是不一致。这个我会用doc.SetValue约定标准格式但更新一点的是保存前手动设置如果你用TiXmlPrinter来序列化可以控制换行符和缩进格式比默认输出更可控TiXmlPrinter printer; printer.SetIndent(\t); // 用Tab缩进 printer.SetLineBreak(\r\n); // 强制CRLF保持Windows风格 doc.Accept(printer); FILE* fp fopen(out.xml, wb); fwrite(printer.CStr(), 1, printer.Size(), fp); fclose(fp);TiXmlPrinter是从一个叫TiXmlVisitor的访问者模式接口来的Accept方法让文档对象把每个节点“交给”printer去序列化。这样你能精确控制输出格式不再被SaveFile的默认行为牵着走。另外BOM问题带BOM的UTF-8文件TinyXML解析时会把BOM当作非法字符直接报错。如果你控制不了源头文件解析前先检查头三个字节是不是EF BB BF是的话跳过去再Parse。5.3 属性值转义与实体处理游走在XML语法边缘XML规定属性值里不能直接出现裸的和如果配置文件里有一句SQL查询里面全是写的时候必须先转义成amp;。这个转义工作TinyXML在保存时自动做一部分但有个大坑如果你手动用字符串拼接XML内容然后调Parse它不会帮你“再转义一次”而是严格解析。我遇到最伤的一次是把a b当成属性值直接拼进XML字符串TinyXML解析到第二个直接报“entity reference不正确”。解决办法有两个方向。一是构造节点时用SetAttribute传原始串保存时库来转义二是强制走TiXmlPrinter输出它会处理好实体编码。凡是属性值里有、、、这四个字符的都别直接拼字符串。实体这块还有个细节nbsp;这类HTML命名实体在TinyXML里不认只认amp;、lt;、gt;、quot;、apos;这五个XML预定义实体其他一律报错。所以想要非换行空格宁可把Unicode码点转成#160;也别指望nbsp;能过解析。6. 把TinyXML用得更顺手一个常用技巧——从裸指针到智能指针的封装接触TinyXML的时间一长你会发现它的内存管理虽然简单但和现代C混用时还是有些脱节。一个实用技巧是写个小的RAII包装把new TiXmlElement包成std::unique_ptr确保挂树之前异常安全等真正LinkEndChild之后再用release()转移所有权。下面是我在模拟项目X里常用的写法#include memory // 用 unique_ptr 持有临时节点挂树前即使报错也能自动释放 auto newItem std::make_uniqueTiXmlElement(item); newItem-SetAttribute(id, 1002); root-LinkEndChild(newItem.get()); // 挂上树转移所有权 newItem.release(); // 树负责释放unique_ptr放权这段代码的逻辑是LinkEndChild接受的是裸指针它内部把自己挂进子节点链表后这颗节点就成了树的财产release()让unique_ptr放弃对节点的所有权之后释放时机完全交给树。这样在LinkEndChild之前如果抛出异常unique_ptr析构会帮我把节点清掉不会泄漏。从那次内存泄漏排查之后我几乎给所有临时构造的节点都套了这一层尤其是循环里建很多子节点的情况收益显著。在这个基础上还有一个习惯值得养成解析完XML文件后别急着写代码取路径先写一段循环把整棵树的节点名、属性、文本全部打印出来对照原始XML文件确认解析结果。这一步看起来多花五分钟实际能省下后续所有“为什么我这个节点取不到”的疑问。我从用了这两招开始TinyXML相关代码的返工率明显下降希望你也能从这份资源里得到同样的体验少踩几次我踩过的坑。本文还有配套的精品资源点击获取