C++ XML解析实战:CMarkup轻量库核心设计与配置管理应用
1. 项目概述为什么选择CMarkup在C项目里处理XML这事儿听起来简单但真干起来坑可不少。你可能试过用TinyXML觉得它够轻量但文档复杂点、节点一多内存和性能就开始捉襟见肘你也可能被MSXML或者Xerces-C的庞大依赖和复杂的COM接口劝退过为了解析一个配置文件引入一堆动态库项目瞬间变得臃肿不堪。我自己在十多年的项目开发里从嵌入式系统到桌面应用几乎把市面上主流的C XML解析库都用了个遍直到遇到CMarkup才算是找到了一个在“轻量”、“高效”、“易用”这三个维度上达到绝佳平衡点的工具。CMarkup的核心魅力在于它把“简单”做到了极致。它不是一个庞大的框架而是一个单一的头文件Markup.h和源文件Markup.cpp。你不需要配置复杂的构建系统不需要处理烦人的第三方依赖更不用去学习一套全新的、充满设计模式的API。它的设计哲学是“一个类搞定所有XML操作”——创建、解析、查询、修改、保存所有功能都通过CMarkup这个类的方法暴露出来。这种极简主义带来的直接好处就是极低的接入成本和极高的运行时性能。它内部只维护一个代表整个XML文档的字符串和一个用于快速定位节点的索引数组内存开销通常比文档字符串本身还小解析速度在单次遍历中完成对于兆字节级别的文档也能有“blazing performance”如官方所言 blazing fast。这次实战项目我们就来彻底拆解CMarkup。我不会只给你罗列API手册那没意思。我会结合我踩过的无数个坑带你从零开始把一个真实的、包含增删改查的XML处理需求用CMarkup一步步实现出来。你会看到如何避免编码陷阱、如何高效处理大型文档、如何将CMarkup无缝集成到你的项目构建系统中以及当事情不如预期时该如何快速定位和解决问题。无论你是正在为一个C服务端程序寻找配置管理方案还是需要为游戏引擎解析资源描述文件亦或是处理来自网络协议的XML数据包这篇内容都能给你一套可直接“抄作业”的解决方案。2. CMarkup核心设计与思路拆解2.1 哲学字符串即文档索引即导航理解CMarkup的设计思路是高效使用它的前提。它与DOM文档对象模型解析器有本质区别。像MSXML或TinyXML的DOM模式会在内存中构建一棵完整的节点树每个元素、属性、文本都是一个独立的对象并通过指针相互链接。这种方式直观但代价是内存占用高每个对象都有开销创建和销毁的代价也大。CMarkup走了另一条路它只持有原始的XML文本和一个与之平行的索引数组。你可以把整个XML文档想象成一本书的完整内容字符串而索引数组就是这本书的目录记录了每一章、每一节的起始页码在字符串中的位置。当你需要“翻到”某一章时查找节点CMarkup不是去复制这一章的内容创建一个新对象而是直接通过目录索引定位到原文中的位置。这种设计带来了几个关键优势内存效率极高除了文档字符串和索引数组几乎没有额外开销。索引数组通常比文档字符串本身还小。解析速度快构建索引只需要对文档进行一次线性扫描单次遍历复杂度是O(n)。修改灵活所有的修改操作增、删、改都是在原始字符串上进行的“外科手术”。CMarkup会智能地更新受影响的索引保持“目录”与“内容”同步。零外部依赖所有逻辑都封装在一个类里编译后直接成为你程序的一部分部署时没有任何额外的DLL或SO文件需要操心。2.2 关键数据结构与生命周期CMarkup类内部主要维护两个核心数据成员MCD_STR m_strDoc: 存储整个XML文档的字符串。MCD_STR是一个宏定义根据你的编译设置可能是std::string、std::wstring或MFC的CString。这让你能使用自己熟悉的字符串类型。一个内部的索引数组这个数组的每个元素通常包含节点类型元素、注释、声明等、在m_strDoc中的起始位置和长度等信息。这个数组对用户是透明的但它是所有快速定位操作的基石。一个CMarkup对象的典型生命周期如下构造与初始化可以创建一个空对象准备构建新文档或者通过Load/SetDoc方法加载一个已有的XML字符串或文件。导航与定位使用FindElem、IntoElem、OutOfElem、ResetPos等方法在文档树中移动。这里有一个非常重要的概念叫“当前元素位置”很多操作都是基于这个当前位置进行的。读取与查询定位到目标元素后使用GetTagName、GetAttrib、GetData等方法获取节点信息。修改与写入在当前位置可以使用AddElem、AddChildElem、SetAttrib、SetData、RemoveElem等方法修改文档结构或内容。持久化使用GetDoc获取修改后的完整XML字符串或者用Save方法直接写入文件。2.3 与同类库的选型对比为什么是CMarkup而不是别的我们快速对比一下特性/库名CMarkupTinyXML-2pugixmlRapidXMLMSXML设计模式基于字符串索引DOM树DOM树基于字符串的DOMCOM接口的DOM头文件1个(.h) 1个(.cpp)几个头文件单个头文件单个头文件系统组件需引入头文件依赖无纯标准C无无无Windows COM 依赖msxml*.dll内存占用极低字符串索引中高完整节点树中优化过的节点树低节点引用原始字符串高COM对象开销大解析速度快单次遍历慢非常快极快原地解析中等易用性极高API极其简单高高中等API较底层低COM编程繁琐修改支持强大原位修改支持支持支持但需注意内存管理支持Unicode支持完善通过宏定义有限完善依赖源文件编码完善适用场景配置文件、数据交换、需要频繁修改的中小型文档学习、简单解析高性能解析、游戏开发极致性能、只读解析Windows平台、与IE/Office等交互选型心法如果你的需求是读写兼备、文档大小适中、且希望集成简单、代码干净CMarkup是首选。它的API设计让代码读起来就像在描述你的操作意图非常直观。如果需求是超高性能的只读解析比如在游戏每一帧加载资源描述RapidXML或pugixml可能更优。如果项目严重依赖MFC/ATL且仅在Windows平台MSXML或许有生态优势。对于绝大多数服务端、工具软件、嵌入式系统的配置管理和数据序列化需求CMarkup的平衡性是无与伦比的。3. 核心细节解析与实操要点3.1 环境准备与项目集成集成CMarkup到你的项目简单到令人发指。从官网下载的zip包如markup11_5.zip里你真正需要的只有两个文件Markup.h和Markup.cpp。步骤一文件放置我个人的习惯是在项目源码目录下创建一个thirdparty或external文件夹把这类轻量级库都放进去。例如MyProject/ ├── src/ │ ├── main.cpp │ └── ... ├── include/ │ └── ... └── thirdparty/ └── cmarkup/ ├── Markup.h └── Markup.cpp这样做的好处是项目结构清晰并且便于版本管理你可以把整个cmarkup文件夹加入git。步骤二修改编译配置这是唯一需要注意技术细节的地方主要围绕字符串编码和预编译头。字符串类型选择CMarkup通过预处理器宏来适配不同的字符串类型。打开Markup.h你会看到相关的定义。通常你需要在你项目的预处理器定义Preprocessor Definitions中添加一个宏如果你使用STL的std::string推荐跨平台添加MARKUP_STL。如果你使用MFC的CString仅Windows/MFC项目不需要添加特殊宏但需要链接MFC库。如果你使用STL的std::wstringUnicode同样添加MARKUP_STL并确保你的项目编译设置为Unicode字符集。预编译头问题Visual Studio用户必看注意这是最常见的编译错误来源CMarkup的源码设计没有考虑预编译头。如果你在Visual Studio中创建的项目使用了预编译头通常是stdafx.h直接编译Markup.cpp会失败。解决方案在项目解决方案资源管理器中右键点击Markup.cpp-属性Properties-C/C-预编译头Precompiled Headers- 将“预编译头”选项从“使用/Yu”或“创建/Yc”修改为“不使用预编译头Not Using Precompiled Headers”。这一步至关重要非Windows平台的编码转换在Linux/macOS下如果处理非UTF-8或本地编码的XML可能需要定义MARKUP_ICONV或MARKUP_STDCONV并链接iconv库。但对于现代项目我强烈建议内部统一使用UTF-8编码这样可以避免绝大多数编码问题。在CMarkup中只要你的源字符串是UTF-8并定义了MARKUP_STL它就能正确处理。步骤三包含与使用在你的源码文件中直接包含头文件即可开始使用。#include “thirdparty/cmarkup/Markup.h” // 根据你的实际路径调整 int main() { CMarkup xml; // ... 你的XML操作代码 return 0; }不需要额外的命名空间CMarkup类就在全局作用域。3.2 基础API精讲与“当前元素”模型CMarkup的所有操作都围绕着一个核心概念“当前元素Current Element”。你可以把它想象成一个光标或指针指向文档树中的某个元素节点。大部分读取和修改操作都是针对这个“当前位置”进行的。关键导航方法ResetPos(): 将“当前元素”重置到文档根节点之前。这是开始遍历前的标准操作。FindElem(“TagName”): 从当前位置向后或第一次调用时从开头查找指定标签名的下一个兄弟元素。找到后当前位置移动到该元素。它不进入子节点。IntoElem(): 如果当前位置是一个元素节点调用此方法会将“当前元素”移动到这个元素的内部接下来FindElem就会在这个元素的子节点中查找。这是进入子树的关键。OutOfElem(): 与IntoElem()相反从当前子树中退出到父级元素。FindChildElem(“TagName”): 这是一个便利方法相当于先调用IntoElem()然后调用FindElem(“TagName”)但只在直接子节点中查找一次。一个经典的遍历模式CMarkup xml; xml.Load(“config.xml”); // 加载文档 xml.ResetPos(); // 重置位置到文档开始 // 假设我们要找所有 Server 节点 while (xml.FindElem(“Server”)) { // 当前元素现在是 Server std::string name xml.GetAttrib(“name”); std::string ip xml.GetAttrib(“ip”); // 进入 Server 内部查找它的子节点 Port xml.IntoElem(); if (xml.FindElem(“Port”)) { int port atoi(xml.GetData().c_str()); } xml.OutOfElem(); // 处理完这个Server的子节点退出来准备查找下一个兄弟Server }这个模式清晰展示了如何结合FindElem、IntoElem和OutOfElem来遍历任意深度的XML树。数据获取方法GetTagName(): 获取当前位置元素的标签名。GetAttrib(“AttrName”): 获取指定属性的值。如果属性不存在返回空字符串。注意属性值总是以字符串形式返回需要自己转换类型。GetData(): 获取当前元素的文本内容。对于类似Port8080/Port的元素GetData()返回”8080″。GetSubDoc(): 这是一个强大但容易被忽略的方法。它获取当前元素包括其开始标签、内容、结束标签的完整XML字符串。非常适合用于提取文档片段或者进行深拷贝。3.3 创建与修改文档的实战技巧创建新文档通常从创建一个空的CMarkup对象开始。构建一个完整的配置文档CMarkup xml; // 1. 添加XML声明可选但推荐 xml.AddElem(“?xml version\”1.0\” encoding\”UTF-8\”?”); // 2. 添加根元素 xml.AddElem(“Configuration”); xml.IntoElem(); // 进入Configuration // 3. 添加子元素和属性 xml.AddElem(“AppSettings”); xml.IntoElem(); xml.AddElem(“Title”, “My Application”); // 带文本内容的元素 xml.AddElem(“Version”, “2.1.0”); xml.AddElem(“LogLevel”, “INFO”); xml.OutOfElem(); // 4. 添加另一个复杂节点 xml.AddElem(“Database”); xml.IntoElem(); xml.AddElem(“Connection”); xml.SetAttrib(“type”, “mysql”); xml.SetAttrib(“host”, “localhost”); xml.SetAttrib(“port”, “3306”); // 在Connection内添加子元素 xml.IntoElem(); xml.AddElem(“Username”, “admin”); xml.AddElem(“Password”, “secret”); // 注意密码不应明文存储此处仅为示例 xml.OutOfElem(); // 退出Connection xml.OutOfElem(); // 退出Database xml.OutOfElem(); // 退出Configuration std::string strDoc xml.GetDoc(); // strDoc 现在包含了格式化的XML字符串AddElem方法有两个重载一个只接受标签名创建空元素另一个接受标签名和文本内容。SetAttrib用于设置属性。修改现有文档修改的核心是先定位再操作。// 接上文假设xml已经加载了上述文档 xml.ResetPos(); // 找到LogLevel元素并修改其值 if (xml.FindElem(“LogLevel”)) { xml.SetData(“DEBUG”); // 将INFO改为DEBUG } // 找到Database/Connection并添加一个超时属性 xml.ResetPos(); if (xml.FindElem(“Database”)) { xml.IntoElem(); if (xml.FindElem(“Connection”)) { xml.SetAttrib(“timeout”, “30”); } } // 删除一个元素找到Version并删除 xml.ResetPos(); if (xml.FindElem(“Version”)) { xml.RemoveElem(); // 删除当前元素 }重要心得RemoveElem()删除的是当前元素。删除后当前位置会移动到被删除元素的前一个位置如果存在所以连续的删除操作需要小心最好在每次删除后重新定位或使用FindElem的循环条件。SetData()会覆盖元素内所有的文本和子节点。如果你只想追加文本需要先获取旧数据拼接后再设置。修改操作是直接作用于内部的m_strDoc字符串的。这意味着频繁的插入删除特别是在大文档开头可能引发字符串的大量内存移动。对于性能要求极高的场景需注意这一点。4. 实战构建一个应用配置管理器让我们用一个更贴近实际的例子来串联所有知识为一个C后台服务实现一个配置管理器。这个配置管理器需要从config.xml读取配置在内存中提供访问接口并支持运行时修改和保存。4.1 需求分析与类设计假设我们的配置文件结构如下?xml version”1.0″ encoding”UTF-8″? ServiceConfig General ServiceNameDataProcessor/ServiceName LogLevelINFO/LogLevel MaxThreads8/MaxThreads /General Network ListenPort9000/ListenPort BindAddress0.0.0.0/BindAddress Backlog128/Backlog /Network Modules Module enabled”true” NameInputFeeder/Name Path/usr/lib/modules/input.so/Path /Module Module enabled”false” NameAdvancedFilter/Name Path/usr/lib/modules/filter.so/Path /Module /Modules /ServiceConfig我们需要一个ConfigManager类它应该在初始化时加载并解析config.xml。提供类型安全的Get/Set方法如GetInt,GetString,GetBool。支持获取模块列表等复杂结构。提供Save()方法将内存中的修改写回文件。4.2 核心实现代码拆解以下是ConfigManager的核心实现片段ConfigManager.h#pragma once #include string #include vector #include unordered_map #include “Markup.h” // 引入CMarkup struct ModuleInfo { std::string name; std::string path; bool enabled; }; class ConfigManager { public: ConfigManager(const std::string configPath); bool Load(); bool Save(); // 类型安全的获取方法 std::string GetString(const std::string key, const std::string defaultValue “”); int GetInt(const std::string key, int defaultValue 0); bool GetBool(const std::string key, bool defaultValue false); // 设置方法 void SetString(const std::string key, const std::string value); void SetInt(const std::string key, int value); void SetBool(const std::string key, bool value); // 获取复杂结构 std::vectorModuleInfo GetModules(); void UpdateModule(const std::string name, const ModuleInfo info); private: bool FindElementByPath(const std::string path); std::string m_configPath; CMarkup m_xmlDoc; // 核心CMarkup对象 bool m_isLoaded; // 一个简单的路径缓存避免频繁遍历可选优化 // std::unordered_mapstd::string, int m_elementPosCache; };ConfigManager.cpp (关键部分)#include “ConfigManager.h” #include sstream #include algorithm ConfigManager::ConfigManager(const std::string configPath) : m_configPath(configPath), m_isLoaded(false) { } bool ConfigManager::Load() { if (!m_xmlDoc.Load(m_configPath.c_str())) { // Load失败可能是文件不存在尝试创建默认配置 m_xmlDoc.AddElem(“?xml version\”1.0\” encoding\”UTF-8\”?”); m_xmlDoc.AddElem(“ServiceConfig”); // … 此处可初始化默认配置结构 return Save(); // 保存默认配置 } m_isLoaded true; return true; } bool ConfigManager::Save() { return m_xmlDoc.Save(m_configPath.c_str()); } // 一个辅助函数使用路径查找元素如 “General/LogLevel” bool ConfigManager::FindElementByPath(const std::string path) { m_xmlDoc.ResetPos(); std::istringstream iss(path); std::string token; while (std::getline(iss, token, ‘/’)) { if (!m_xmlDoc.FindElem(token.c_str())) { return false; // 路径中的某一级没找到 } // 如果不是最后一级需要进入元素内部 if (iss.peek() ! EOF) { m_xmlDoc.IntoElem(); } } return true; // 成功定位到目标元素 } std::string ConfigManager::GetString(const std::string key, const std::string defaultValue) { if (!m_isLoaded !Load()) return defaultValue; if (FindElementByPath(key)) { // 如果找到的元素有子元素GetData()可能为空。这里假设目标就是叶子文本节点。 // 更健壮的做法可以检查是否存在子元素。 std::string data m_xmlDoc.GetData(); return data.empty() ? defaultValue : data; } return defaultValue; } int ConfigManager::GetInt(const std::string key, int defaultValue) { std::string strVal GetString(key, “”); if (strVal.empty()) return defaultValue; try { return std::stoi(strVal); } catch (const std::exception) { return defaultValue; } } // 设置值先定位如果不存在则创建路径上的所有元素 void ConfigManager::SetString(const std::string key, const std::string value) { if (!m_isLoaded) Load(); // 简化实现假设key是类似 “General/LogLevel” 的路径 // 这里需要更复杂的逻辑来确保路径存在为节省篇幅展示核心思路 // 1. 尝试查找元素 // 2. 如果找到SetData // 3. 如果没找到从根节点开始按路径一级级AddElem如果不存在 // 这是一个很好的练习可以让你深入理解CMarkup的导航和修改API。 // 示例假设我们知道元素一定存在比如通过Load保证了结构 if (FindElementByPath(key)) { m_xmlDoc.SetData(value.c_str()); } else { // 更完整的实现需要在这里创建缺失的节点 // 例如分割key从根节点开始逐级FindElem找不到就AddElem然后IntoElem } } // 获取模块列表 std::vectorModuleInfo ConfigManager::GetModules() { std::vectorModuleInfo modules; if (!m_isLoaded !Load()) return modules; m_xmlDoc.ResetPos(); if (m_xmlDoc.FindElem(“Modules”)) { m_xmlDoc.IntoElem(); while (m_xmlDoc.FindElem(“Module”)) { ModuleInfo info; info.enabled std::string(m_xmlDoc.GetAttrib(“enabled”)) “true”; m_xmlDoc.IntoElem(); if (m_xmlDoc.FindElem(“Name”)) { info.name m_xmlDoc.GetData(); } if (m_xmlDoc.FindElem(“Path”)) { // FindElem会继续在当前层级查找 info.path m_xmlDoc.GetData(); } m_xmlDoc.OutOfElem(); // 退出当前Module元素 modules.push_back(info); } } return modules; }这个ConfigManager展示了如何将CMarkup封装成一个更友好、类型安全的接口。FindElementByPath函数是一个关键工具它模拟了XPath的部分功能让配置项的访问更加直观。4.3 性能优化与内存管理思考对于配置管理器这类场景性能通常不是瓶颈因为配置文件通常很小。但如果你用CMarkup处理几百KB甚至几MB的XML数据比如数据交换就需要考虑一些优化点避免频繁的ResetPos()和全局查找ResetPos()会将内部索引重置到文档开头。如果你需要反复访问文档中某个特定区域最好在首次定位后记录下大致的位置虽然CMarkup不直接暴露索引但你可以记录父元素的路径或者设计你的访问模式以减少从头查找的次数。谨慎使用GetSubDoc()进行大量片段提取GetSubDoc()会生成新的字符串。如果频繁调用且片段很大可能会带来不必要的内存拷贝开销。考虑是否真的需要完整的片段字符串还是只需要其中的部分数据。大文档修改策略在超大文档的头部附近频繁插入或删除元素会导致内部字符串的大量内存移动。如果可能尽量将频繁变动的部分放在文档尾部。或者考虑将大文档拆分成多个小文档分别处理。编码转换开销如果源文件编码与你的程序内部编码如UTF-8不同CMarkup在Load时需要进行转换。确保源文件使用统一的、高效的编码推荐UTF-8 without BOM。索引重建任何修改操作AddElem,RemoveElem,SetAttrib等都可能触发受影响部分的索引重建。这是一个O(n)操作n是受影响部分的长度。对于超大文档批量修改比多次零星修改更高效。内存管理心得 CMarkup对象本身很小主要内存占用就是m_strDoc字符串。当CMarkup对象离开作用域时其析构函数会清理所有资源。这里没有手动内存管理符合RAII原则非常安全。唯一需要注意的是如果你用GetDoc()获取了文档字符串的MCD_STR如std::string副本这个副本的生命周期由你负责。5. 常见问题与排查技巧实录即使CMarkup如此简单在实际使用中还是会遇到一些典型问题。下面是我总结的“避坑指南”。5.1 编译与链接问题问题1Visual Studio编译错误 “error C1010: 在查找预编译头时遇到意外的文件结尾”现象在VS中编译Markup.cpp时报此错误。原因项目使用了预编译头stdafx.h但Markup.cpp没有包含它。解决如前所述右键Markup.cpp- 属性 - C/C - 预编译头 - 选择“不使用预编译头”。问题2链接错误提示找不到iconv相关函数现象在Linux/macOS下编译链接阶段报错undefined reference tolibiconv_open’等。原因你的XML文档或程序内部字符串编码需要转换CMarkup试图使用iconv库但项目没有链接该库。解决在Markup.h开头或你的编译器预定义中明确指定使用标准C库转换添加宏定义MARKUP_STDCONV。这通常适用于现代Linux/macOS系统且源文件是UTF-8。如果必须用iconv确保安装了开发包如libiconv-dev并在链接命令中加入-liconv。问题3宽字符Unicode构建问题现象在Unicode项目中使用字符串处理混乱或编译出错。解决确保你的项目字符集设置正确在VS中是“使用Unicode字符集”。如果你使用STL定义MARKUP_STL宏。CMarkup会自动将MCD_STR定义为std::wstring。所有传递给CMarkup API的字符串字面量需要使用_T()或L前缀例如xml.FindElem(_T(“Server”))或xml.FindElem(L”Server”)。5.2 运行时逻辑错误问题4FindElem找不到元素但明明存在排查检查大小写XML标签是大小写敏感的。Server和server是不同的。检查命名空间如果XML包含命名空间如ns:ServerFindElem(“Server”)是找不到的。你需要使用完整的带命名空间的标签名或者使用CMarkup的FindElem(“ns:Server”)。CMarkup对命名空间的支持是直接的字符串匹配。理解查找范围FindElem只在当前层级查找兄弟元素。如果你在根节点FindElem(“Port”)是找不到ServerPort8080/Port/Server里的Port的你必须先FindElem(“Server”)然后IntoElem()再FindElem(“Port”)。确认当前位置在循环或复杂操作后用GetTagName()打印一下当前位置或者调用ResetPos()重新开始。问题5GetData()返回空但元素明明有文本排查元素内容可能是子元素不是文本。例如ItemNameTest/Name/Item在Item位置调用GetData()返回空因为它的内容是子元素Name。你需要IntoElem()后对Name元素调用GetData()。文本包含空白字符。GetData()返回的是元素内部的文本节点。如果文本前后有换行、空格它们会被包含。可以使用trim函数处理返回值。编码问题。如果文本包含非ASCII字符且编码处理不当可能会显示为空或乱码。确保整个流程文件、程序内部、CMarkup编译设置使用统一的UTF-8编码。问题6修改后保存文件内容乱了或丢失部分数据排查检查文件权限程序是否有权写入目标文件检查Save的返回值bool bSuccess xml.Save(“file.xml”);如果返回false检查错误Windows下可用GetLastErrorLinux下看errno。操作顺序导致位置错乱这是最常见的原因。比如你在一个循环中删除元素while (xml.FindElem(“Item”)) { if (someCondition) { xml.RemoveElem(); // 删除当前Item // 危险删除后当前位置变了下一次循环的FindElem可能跳过下一个Item。 } }正确做法在删除后通常需要将位置重置到上一个位置或者使用一个向前的迭代方式。更安全的方法是先收集要删除的元素的路径或索引然后在另一个循环中删除。内存中的文档对象m_strDoc已损坏在极端情况下如多线程同时修改未加锁内部字符串可能损坏。确保对同一个CMarkup对象的操作是线程安全的通常需要外部加锁。5.3 高级应用与扩展处理CDATA节 CMarkup完全支持CDATA。使用AddElem创建元素时如果文本中包含特殊字符如,CMarkup会自动进行转义。如果你需要显式地添加CDATA节可以使用AddChildElem并配合SetData但更直接的方法是在构建XML字符串时手动包含![CDATA[ ... ]]然后使用SetDoc加载。CMarkup在解析时会正确识别并保持CDATA节。处理注释和处理指令FindElem也可以查找注释”!”和处理指令如”?”。例如xml.FindElem(“!”)会找到下一个注释节点然后GetData()可以获取注释内容。但通常我们不需要直接操作这些节点。性能测试建议 如果你关心性能可以写一个简单的测试程序用CMarkup加载一个大的XML文件比如几MB记录解析时间然后进行一些查找和修改操作再记录保存时间。与你的业务需求进行对比。在我的经验中对于1MB左右的配置文件CMarkup的解析和操作都在毫秒级完全不是瓶颈。最后再分享一个我个人的小技巧对于非常复杂的、需要频繁进行随机访问和查询的XML文档虽然CMarkup的线性查找FindElem是O(n)但通常n同级节点数并不会太大。如果确实遇到性能问题一个简单的优化是在第一次加载后自己用std::map或std::unordered_map建立标签名到元素索引可以用GetDoc()返回的字符串中的偏移量来近似表示的缓存这样可以实现近似O(1)的查找。但这增加了复杂性仅在确有必要时使用。绝大多数情况下CMarkup本身的简洁和高效已经足够完美。