这两天仔细看了下TinyXml的源代码,完美地搞清楚了一些网友和我自己的很多疑问. 鉴于TinyXml的实用性,而且现在不少人在使用,就决定在此做点有意义的事情 ---- 列出使用TinyXml库值得注意的几个地方.
关于TinyXml库的介绍网上有很多资料,大家可以试着搜下,这里我就不多说了,TinyXml很小巧,但它提供了非常丰富的接口,特别适用于存取程序的数据,如果你使用它,相信你会感觉到它的灵活的.
TinyXml下载地址:http://download.csdn.net/detail/hoyt00/3904805
http://sourceforge.net/projects/tinyxml/
以下几点值得注意哦:
1.new出指针而不见delete.
如果你看了TinyXml的文档或者一些网友写的例子程序,你会发现,其中new出了很多对象而不见一个delete,这个问题我当时也感觉特别难以接受,网上也有各种各样的说法,有说"nwe出这么多,内存不泄漏才怪",有说"TinyXml的指针有自销毁功能"等等,这些说法都是错误的,只要你正确的使用TinyXml,无论你new多少(当然在内存的有效使用范围之内)都不会有内存泄漏,而且TinyXml的指针根本没有自销毁功能,可以想像一下,在C++中一个没有经过任何类的包装的指向堆内存的指针变量怎么可能在它自身的生命结束时管得了它所指向的对象? 那它到底是怎么回事?
我们都知道(这里我假设你已经看过TinyXml文档),TinyXml是利用DOM(文档对象模型)来呈现XML文档的,在它眼中XML文档就是一棵树,TiXmlDocument对象就是这棵树的根结点,在一个完整的文档中,除了它,其余结点必须都是它的后代,所以TinyXml用了一个很巧妙的方法来析构每一个结点所对应的对象 ---- 每个结点的析构任务都委托给了它的父亲,这样只要保证父亲被正确析构,或者调用了父亲的Clear函数,它的所有后代都会被正确的析构,所以对整个文档来说只要TiXmlDocument对象被正确析构,那就万无一失了,这棵树会先从叶子销毁,一直到树根. 像这种数据结构,为了保证树的完整性而使用堆内存,和由它自己管理内存也是理所当然的,由此便可得到以下几个结论:
(1)TiXmlDocument对象最好在栈上创建,如果在堆上创建了,那你必须得自己销毁它,千万不要像别的对象一样new出了就不管了.
(2)除了TiXmlDocument对象,树中的别的结点对象,必须是堆上创建的,千万不要把栈上对象的地址链接(LinkEndChild)到树中,因为栈上对象是不能用delete销毁的,当然TinyXml也有对栈上对象插入的方法,以下会说到.
(3) 除了文档结点,new出的所有结点对象必须被链接到一个父亲结点上,才可以不用管对象的delete,而且必须不能管,不然有可能整棵树会被破坏而得不到正确的遍历和析构,如果new出的对象从来没链接到某棵树上,而且将来也不打算链接,无论有没有别的结点链接到它身上,你都必须手动delete它.
(4)不要尝试链接已经被别的结点链接过的指针,很显然,这会造成无法估量的麻烦,不用多说,大家都懂的.
当 然不是所有人都喜欢new,可能他们更喜欢像std::vector那样插入元素的副本而不是交给它一个指针,TinyXml也提供了同样的方式插入结点 ---- Insert函数(InsertEndChild,InsertBeforeChild,InsertAfterChild),因为这些函数插入的是结点的副本(包括所有子结点),所以可以传给它栈上或堆上的对象,但是要注意,如果传入的是堆上对象,那还必须手动delete它,而不像LinkEndChild那样链接了就不能管了,因为树析构的时候析构的是它的副本,而不是它. 我更倾向于使用Link + 堆上对象,而不Insert + 栈上对象,因为这样可以省去创建副本的运行成本,除非要插入到具体的位置,而不是End,当然也可以修改源代码使Link函数也有这种功能.
3.元素的属性不是普通结点.
与文本对象(TiXmlText)不同,属性对象(TiXmlAttribute)在文档树中不是以普通结点形式存在的,也不从TiXmlNode类派生,而是被元素结点的数据成员 ---- 一个TiXmlAttributeSet对象所管理,这个TiXmlAttributeSet对象持有一个环状的TiXmlAttribute对象链表(具体可以参看源代码),所以TiXmlElement对象通过调用Child系列函数是无法得到属性的,相反这件事是通过Attribute系列函数完成的. 访问器(从TiXmlVisitor派生的用户实现的类对象,一种附加的回调机制,具体参看源代码)没有针对TiXmlAttribute对象的接口,而必须在对TiXmlElement对象的实现中处理属性,因此TiXmlAttribute类也没有Accept虚函数.
4.尽量不要在xml中使用中文.
TinyXml只认识UTF-8和ISO 8859-1编码,而不知GB2312为何物,但事实上你以GB2312在文档中写入中文,之后可以正确读取,而且文档在记事本中打开也能显示正确的中文,其实这是种巧合,并不是TinyXml支持GB2312了.
这个问题需要解释下,我已经仔细分析过了,TinyXml的函数有char*类型参数,而没有wchar_t*类型参数,所以直接在程序中向文档写入中文必然是GB2312方式(这里是以VC编译器为例的),这时char*只是指向一块内存块,跟void*一样,这块内存只有用GB2312才能正确解释为中文,因为TinyXml是被设计跨平台的,所以不要指望它会调用WideCharToMultiByte和MultiByteToWideChar来帮你做转换,而以GB2312写入的中文在读取时这些中文字符的码值是不变的,也就是你在准备写入文档时是码值是多少,读取到程序里的值就是多少,而把这个值当作GB2312编码时就是原来写文档时的中文字符了,当把写入的文档在记事本打开时,由于没有utf-8标记字节0xEF 0xBB 0xBF(TinyXml默认不写入这三个字节,稍后再说怎么让它写入),所以记事本把xml文件当作GB2312编码打开,就阴差阳错地把原本错误的字节以正确的中文字符显示了,但终究这篇xml文档是utf-8编码的,那些中文字符本应显示为乱码的,就这样错误的写入,错误的读取,记事本错误的判断,都加到一起就离奇地没有错误了.
但错误还是错误,有一个办法,就是在文档头部加上utf-8标记字节0xEF 0xBB 0xBF,这样记事本就能正确判断文档编码,正确地以utf-8打开,正确地把中文显示为乱码.
基于这几种错误叠加的现象,如果你生成的xml文档只用在你自己特定的程序中而不用在其它软件(比如有时要用别的文本处理软件打开查看内容),那么在文档中存取中文是完全没有问题的,但在别的地方以utf-8打开时中文就是乱码.
保证所有地方都正确的方法是在写入时和读取时用WideCharToMultiByte和MultiByteToWideChar把GB2312编码的中文字串转换为UTF-8编码的中文字串,如此,所有软件都能正确的读取UTF-8编码的中文字符(为了让记事本正确的判断为UTF-8,可以加上utf-8标记字节,虽然它不是标准,但普遍使用).当然还是那句话 ---- 尽量不使用中文和其它非英文字符,除非迫不得已.
另外我在看源代码时,有几个地方也发表下自己的想法:
1.关于explicit关键字
tinystr.h第85行 TIXML_EXPLICIT TiXmlString ( const char * str,size_typelen): rep_(0)
(TIXML_EXPLICIT宏的内容是explicit)explicit关键字的作用是使单参数的构造函数不用作隐式转换,但这个构造函数有两个参数,它根本就没有发生隐式转换的可能,也完全没有必要使用explicit关键字.
2.关于成员函数const和非const
TinyXml的类库中有许多类的函数有const和非const版本,为什么非const版本的函数不把函数体重新写一遍而是用const_cast把const函数的const性质转换掉再调用它呢? 其实函数体也就返回数据成员的那一两句而已.
3.关于默认实参
有些类的重载成员函数们,一个函数的参数表和另一个函数的参数表的前面部分完全一致,为什么不使用默认实参而是参数较少的函数版本调用参数较多的函数版本呢?
4.关于Test项目中的乱码
这也是很多人都疑惑的一个问题 ---- TinyXml库自带Test项目竟然编译不过,这个问题的原因很简单,打开xmltest.cpp文件,定位到第1141行,该行和下面几行有乱码,这又是怎么回事?
其实这几行是作者为测试ISO 8859-1编码(一种单字节编码,编码范围使用了8位的所有取值,也就是0到0xFF)文档而写,意图把ISO 8859-1字符串写入文档对象,再从文档对象读取,只有用ISO 8859-1编码才能正确解释.
但 是编译器以GB2312打开cpp文件时遇到这几个字符就发生了奇怪的事情 ---- 源码变乱码了. 本来乱码没什么的,大不了我输出到xml文件也乱码就是了,但问题是那几个字符不仅使它本身乱,而且它后面的一个asc2字符也有50%的几率跟着遭殃,因为我们都知道用GB2312解码字符串时如果第一个字节大于0x7F,一般情况下会把它和接下来的一个字节当作一个整体字符,当然这个字符可能显示,也可能不显示,无论怎样都不是正确的,所以你会看到那些字串中有用于xml元素的"<"符号而没有">"符号,有的字串甚至都没反引号,这时不要简单的加上反引号,虽然语法正确了,能编译,但xml语法还没纠正,调试时会导致文档对象里的断言失败而退出. 另外我还查看了这几个乱码字符的二进制值:
第1141行 |
"<?" |
二进制值 |
22 3C E4 3E 22 0A |
GB2312 |
"<(\xE4\x3E)" |
ISO 8859-1 |
"<ä>" |
第1142行 |
"C鰊t鋘t咪鳇闹? |
二进制值 |
22 43 F6 6E 74 E4 6E 74 DF E4 F6 FC C4 D6 DC 22 0A |
GB2312 |
"C(\xF6\x6E)t(\xE4\x6E)t(\xDF\xE4)(\xF6\xFC)(\xC4\xD6)(\xDC\x22) |
ISO 8859-1 |
"CöntäntßäöüÄÖÜ" |
第1143行
"</?";
二进制值
22 3C 2F E4 3E 22 3B 0A
GB2312
"</(\xE4\x3E)";
ISO 8859-1
"</ä>";
其中二进制值省略了行首的空白字符,GB2312的括号中两个字节表示被当作的一个整体,ISO 8859-1表示原本用ISO 8859-1编码时显示的字串. 可以看出以ISO 8859-1编码的文本是完全符合C++语法和xml语法的,大家在生成项目时将这块代码注释掉就能通过编译和调试了.
还有刚才提到TinyXml保存文档时默认不写入utf-8标记字节BOM,如果想让它保存时写入这三个字节,可以修改TiXmlDocument类添加一个成员:
- public:
- voidSetMicrosoftBOM(bool_useBOM){useMicrosoftBOM=_useBOM;}
当然也可以修改别的成员函数,比如给SaveFile函数添加一个bool参数_useBOM等等,看个人爱好了.
关于TinyXml的使用就先写到这了,如果有不对的地方,希望大家指出,如果有不明白的地方也可以联系我!
XML也是有这几个对象组成了,一般来说我们经常使用的类如下:
l TiXmlDocument:文档类,它代表了整个xml文件。
l TiXmlDeclaration:声明类,它表示文件的声明部分,如上图所示。
l TiXmlComment:注释类,它表示文件的注释部分,如上图所示。
l TiXmlElement:元素类,它是文件的主要部分,并且支持嵌套结构,一般使用这种结构来分类的存储信息,它可以包含属性类和文本类,如上图所示。
n TiXmlAttribute/TiXmlAttributeSet:元素属性,它一般嵌套在元素中,用于记录此元素的一些属性,如上图所示。
n TiXmlText:文本对象,它嵌套在某个元素内部,
如上图所示。 TinyXml使用文档对象模型(DOM)来解析xml文件,这种模型的处理方式为在分析时,一次性的将整个XML文档进行分析,并在内存中形成对应的树结构,同时,向用户提供一系列的接口来访问和编辑该树结构。这种方式占用内存大,但可以给用户提供一个面向对象的访问接口,对用户更为友好,非常方便用户使用。下面我们依次来介绍各个类的用法。
元素属性
属性一般保存在元素中,它们为使用“=”号连接的两个字符串,左边的表示属性名,等号右边的表示属性值,通常使用字符串、整数和浮点数等数据类型表示。例如,pi = 3.14。 你可以通过如下的函数,返回属性值。
+const std::string* Attribute( const std::string& name ) const;
+const std::string* Attribute( const std::string& name,int* i ) const;
在上面3个函数中,第一个函数使用字符串保存返回的属性值,第二个函数把属性值转换为整数然后返回,第三个函数把属性值转换为浮点数然后返回。不过,第二、三个函数都会以字符串的形式记录属性值,并作为函数的返回值返回。
另外,你也可以使用模板函数:
+template< typename T > int QueryValueAttribute( const std::string& name,T* outValue ) const 来返回特点的属性值,它会根据你传入的参数,自动选择合适数据类型。
另外,本类也提供了如下三个函数让你设置属性,参数的类型和返回函数类似。
+void SetAttribute( const std::string& name,const std::string& _value );
+void SetDoubleAttribute( const char * name,double value );
FirstAttribute和LastAttribute可以让你返回第一个和最后一个属性,它们的函数声明如下:
+TiXmlAttribute* FirstAttribute()
+TiXmlAttribute* LastAttribute()
RemoveAttribute函数可以让你删除指定名称的属性,它的函数声明如下:
+void RemoveAttribute( const std::string& name )
元素函数总结
ValueStr //返回元素名称
SetValue //设置元素名称
Parent //返回父节点对象
FirstChild //返回第一个子节点
LastChild //返回最后一个子节点
IterateChildren //返回下一个子节点
InsertEndChild //在最后一个子节点后插入子节点
InsertBeforeChild //在指定的子节点前插入子节点
InsertAfterChild //在指定的子节点后插入子节点
ReplaceChild //替换指定的子节点
RemoveChild //删除指定的子节点
Clear //删除所有的子节点
PrevIoUsSibling //返回同级中前一个节点
NextSibling //返回同级中后一个节点
NextSiblingElement //返回同级中后一个元素
FirstChildElement //返回第一个子元素节点
Attribute //返回元素中的属性值
QueryValueAttribute //返回元素中的属性值
SetAttribute //设置元素中的属性值
FirstAttribute //返回元素中第一个属性对象
LastAttribute //返回元素中最后一个属性对象
RemoveAttribute //删除元素中指定的属性对象
属性类
属性为名称="值"对,元素可以具有属性值,但名称必须唯一。 你可以通过
+const std::string& NameTStr() const 返回属性名称
+const std::string& ValueStr() const
+int IntValue() const;
+double DoubleValue() const;
+void SetName( const std::string& _name )
+void SetIntValue( int _value );
+void SetDoubleValue( double _value );
+void SetValue( const std::string& _value )
以上函数与元素类中的相关函数类似,这里不重复介绍了。 在元素属性中,通常具有许多属性,你可以通过Next函数返回下一个属性对象的指针,也可以通过PrevIoUs函数获得上一个属性对象的指针。
它们的函数声明如下:
+TiXmlAttribute* Next()
+TiXmlAttribute* PrevIoUs()