3.4 字符串的查找

string成员函数中的find族是用来在给定字符串中定位某个或某组字符的。下面是find族成员及其一般用法:

3.4 字符串的查找 - 图1

find()的最简单应用就是在string对象中查找一个或多个字符。这个重载的find()函数使用一个参数用来指示要查找的字符(子串),还有另一可选的参数用来表示从字符串的何处开始查找子串。(默认的开始查找位置是0。)把find放在循环体内,可以很容易地从头至尾遍历一个字符串,重复查找字符串中所有可能出现的与指定字符或字符组匹配的子串。

下面的程序使用Eratosthenes筛选法查找小于50的素数。这种方法从数字2开始,标记所有2(3,5,……)的倍数为非素数,对其他后选素数重复该过程。SieveTest的构造函数对sieveChars进行初始化,设置其字符序列(array)的初始大小,并且用‘P’来填充每个成员。

3.4 字符串的查找 - 图2

3.4 字符串的查找 - 图3

find()函数在string内部进行搜索,检测多次出现的一个字符或字符组,find first not of()查找其他的字符或子串。

string类中没有改变字符串大小写的函数,但借助于标准C语言的库函数toupper()和tolower()(这两个函数一次只改变一个字符的大小写),可很容易地创建这类函数。下面的例子演示了忽略了大小写的查找:

3.4 字符串的查找 - 图4

3.4 字符串的查找 - 图5

3.4 字符串的查找 - 图6

upperCase()和lowerCase()两个函数的流程形式相同:它们先复制参数string对象,接着改变其大小写。程序Find.cpp并不是解决大小写敏感问题的最佳方案,所以在讲到string的比较时将会再次讨论它。

3.4.1 反向查找

如果需要在一个string对象中从后往前进行查找(用“后进/先出”的顺序查找数据),可以使用字符串成员函数rfind():

3.4 字符串的查找 - 图7

3.4 字符串的查找 - 图8

字符串成员函数rfind()从后往前遍历字符串,查找并且报告与其匹配字符(组)所在的序列排列(array)下标,若不成功则报告string:npos。

3.4.2 查找一组字符第1次或最后一次出现的位置

使用find_first_of()和find_last_of()成员函数可以很方便地实现一些小的功能,比如从字符串的头尾两端删除空白字符。注意,它并不触动原字符串,而是返回一个新字符串:

3.4 字符串的查找 - 图9

第1次条件判断是为了检查string是否为空;如果为空,则直接返回原字符串的1个拷贝,不再进行其他判断。注意,一旦找到结束点,函数就会使用开始点的位置和计算出来的子串长度作为参数调用string类的构造函数,用来创建1个基于原字符串的新的string对象。

对这样一个通用工具进行的测试需要十分彻底:

3.4 字符串的查找 - 图10

读者可以看到,在strings型数组中字符型数组自动转换成了string对象。读者可以使用这个数组提供测试案例,检查string两端的空格和制表符是否删除了,以及确定string中间的空格和制表符是否保留了下来。

3.4.3 从字符串中删除字符

使用erase()成员函数删除字符串中的字符是简单而有效的。这个函数有两个参数:一个参数表示开始删除字符的位置(默认值是0);另一个参数表示要删除多少个字符(默认值是string:npos)。如果指定删除的字符个数比字符串中剩余的字符还多,那么剩余的字符将全部被删除(所以调用不含参数的erase()函数将删除字符串中的所有字符)。有时,删除一个HTML文件中的标记(tag)与特殊字符是很有用的,这样就可以得到类似于浏览器中所显示的文本文件,仅仅作为纯文本文件。下面这个例子用erase()来完成这个工作:

3.4 字符串的查找 - 图11

3.4 字符串的查找 - 图12

这个例子甚至能够删除跨越多行[1]的HTML标记。这归功于静态标志inTag,一旦发现了开始标记,此逻辑标志就会被设为true,无论相应的结束标记是否与这个开始标记在同一行。所有形式的erase()都包括在stripHTMLFlags()函数中。[2]在这里所用的getline()版本是在<string>头文件中声明的(全局)函数,这个函数使用起来很方便,因为它可以在其string参数中存储任意长度的一行文本。在使用istream:getline()时不必考虑所用字符序列(array)维数的大小。注意,此程序使用了本章开始时介绍的replaceAll()函数。下一章将采用字符串流来构造一个更加优秀的解法。

3.4.4 字符串的比较

字符串的比较与数字的比较有其固有的不同。数字有恒定的永远有意义的值。为了评定两个只符串的大小关系,必须进行字典比较(lexical comparison)。字典比较的意思是,当测试一个字符看它是“大于”还是“小于”另一个字符时,实际比较的是它们的数值表示,而这些数值表示是由当前所使用的字符集的校对序列来决定的。通常,这种校对序列是ASCII校对序列,它给英语的可打印字符分配的数值为从32到127范围内的连续十进制数字。在ASCII校对序列中,序列表中第一个“字符”是空格,然后是几个常用标点符号,再往后是大小写字母。遵照字母表的编排,比较靠前的字符的ASCII码值都低于比较靠后的字符。知道了这些细节,了解和记忆以下事实就更容易了:当字典比较报告字符串s1“大于”字符串s2时,也即两者相比较时遇到第1对不同的字符时,字符串s1中第1个不同的字符比字符串s2中同样位置的字符在ASCII表中的位置更靠后。

C++提供了多种字符串比较方法,它们各具特色。其中最简单的就是使用非成员的重载运算符函数:operator==、operator!=、operator>、operator<、operator>=和operator<=。

3.4 字符串的查找 - 图13

3.4 字符串的查找 - 图14

重载的比较运算符不但能进行字符串全串比较还能进行字符串的个别字符元素的比较。

在下面的例子中,注意在比较运算符左右两边的自变量类型的灵活性。为了高效率地运行,对于字符串对象、引用文字和指向C语言风格的字符串的指针等的直接比较,string类不创建临时string对象,而是采用重载运算符进行。

3.4 字符串的查找 - 图15

c_str()函数返回一个const char,它指向一个C语言风格的具有“空结束符”的字符串,此字符串与string对象的内容等价。当想将一个字符串传送给一个标准C语言函数时,比如atoi()或<cstring>头文件中定义的任一函数,const char可派得上用场。不过,将c_str()的返回值作为非const参数应用于任一函数都是错误的。

在字符串的运算符中,不会找到逻辑非(!)或逻辑比较运算符(&&和||)。(也不会找到重载版的C语言逐位(二进制数位)运算符&、|、^或~。)重载字符串类的非成员比较运算符被限定在一个可以清晰地、无二义性地应用于多个字符或字符组的子集中。

compare()成员函数能够提供远比非成员运算符集更复杂精密的比较手段。它提供的那些重载版本,可以比较:

·两个完整的字符串。

·一个字符串的某一部分与另一字符串的全部。

·两个字符串的子集。

下面的例子用来比较两个完整的字符串:

3.4 字符串的查找 - 图16

本例中swap()函数所做的工作,顾名思义是:交换其自身对象和参数的内容。为了对一个字符串或两个字符串中的字符子集进行比较,可加上两个参数,一个参数定义开始比较的位置,另一个参数定义字符子集要考虑的字符个数。例如,可以使用下面这个compare()函数的重载版:

3.4 字符串的查找 - 图17

举例如下:

3.4 字符串的查找 - 图18

在以往的例子中,如果涉及字符串中的个别字符,教材中都使用C语言风格的数组索引语法。C++中的字符串类提供一种s[n]表示法的替代方法:at()成员函数。如果不出现意外事件,在C++中这两种索引机制产生的结果是一样的:

3.4 字符串的查找 - 图19

3.4 字符串的查找 - 图20

然而,数组索引下标表示[]与at()之间有一个重要的不同点。如果程序员想引用一个超过边界的数组元素,at()将会友好地抛出一个异常,而普通的[]下标语法将让程序员自行决策:

3.4 字符串的查找 - 图21

有责任心的程序员不会去用有冒险性的索引,程序员希望能够从自动边界检查中受益。使用at()代替[],就有机会从容地修复由于引用了不存在的数组元素而产生的错误。在一个测试编译器上执行这个程序,得到的输出结果是:

3.4 字符串的查找 - 图22

at()成员抛出的是一个out_of_range类对象,它(最终)派生于std:exception。程序可在一个异常处理器中捕获该对象,并采取适当的补救措施,比如重新计算越界下标或扩充数组。采用string:operator[]()不会有那样的保护性,它的危险性等同于C语言中对char型数组的处理。[3]

3.4.5 字符串和字符的特性

本章前面的程序Find.cpp可能导致读者提出下面这个显而易见的问题:为什么对大小写不敏感的比较没有成为标准string类的一部分?对此问题的回答揭示了关于C++字符串对象真实性质的有趣背景。

读者可以考虑一下,字符有“大小写”到底意味着什么。希伯来语、波斯语和日本汉字并不使用大小写的概念,即对这些语言来说大小写没有什么意义。这似乎是说,如果有方法将一些语言指定为“全大写”或“全小写”,就能够设计出通用的解决方案。但是,某些采用“大小写”概念的语言,同时也用可区别的标记改变了特殊字符的意义,如:西班牙语中的变音符号,法语中的抑扬符号,还有德语中的元音变音。因此,任何试图全面解决此问题的大小写敏感的分类整理方案,最终都会变得非常复杂直至不能再进行下去。

虽然通常将C++string看成一个类,但事实并非如此。需要说明一下,basic_string<>模板是一种更通用的工具,而string类型只是其更专门化的版本。请看string在标准C++头文件里的声明:[4]

3.4 字符串的查找 - 图23

要了解字符串类的本质,请看basic_string<>模板:

3.4 字符串的查找 - 图24

本教材将在第5章中详细讨论模板(比第1卷第16章要详细得多)。但现在,只需注意一下string类型是通过使用char实例化basic_string模板而创建的。在basic_string<>模板声明内部,下面的一行:

3.4 字符串的查找 - 图25

告诉读者基于basic_string<>模板的类的行为,是由基于char_traits<>模板的某个类指定的。因此,basic_string<>模板产生的是面向字符串的类,此类的操作对象是除了char以外的类型(比如宽字符(wide character))。为了达到这一目的,char_traits<>模板控制多种字符集的内容和校对行为,而这些字符集用的是字符比较函数eq()(相等),ne()(不等)和lt()(小于)。basic_string<>字符串的比较函数就依赖于这些函数。

这就是为什么字符串类不包含对大小写不敏感的成员函数的原因:因为那不属于它的本职工作。为了改变字符串类比较字符的方式,必须提供不同的char_traits<>模板,因为它定义了对个别字符进行比较的成员函数的行为。

可以用此信息构造一种忽略大小写的新类型的string类。首先,定义一个从现存模板中继承的一种对大小写不敏感的新的char_traits<>模板。其次,仅重写需要更改的成员,使其能逐个字符进行大小写不敏感比较(除了之前提及的3个对字符进行词典比较的成员函数之外,还会为char_traits提供函数find()和compare()的新的实现)。最后,我们将用typedef定义一个基于basic_string的新类,但使用对大小写不敏感的ichar_traits模板作为第2个参数:

3.4 字符串的查找 - 图26

3.4 字符串的查找 - 图27

该程序提供了一个typedef命名的istring类,这样该类就能在各方面像普通的string类一样工作,除了在进行比较的时候不考虑大小写。为了方便起见,程序也提供了一种重载的operator<<(),以便打印istring。举例如下:

3.4 字符串的查找 - 图28

3.4 字符串的查找 - 图29

它只是一个很小的也没有什么实用价值的例子。为使istring完全等价于string,还得创建其他必要的函数以便支持新的istring类型。

通过下面的typedef,<string>头文件提供宽字符串类:

3.4 字符串的查找 - 图30

在宽字符流(wide stream)(代替ostream的wostream,也在<iostream>中定义)和头文件<cwctype>(<cctype>的宽字符版本)中,也体现出对宽字符串的支持。运用这些,再加上标准库里char_traits中的wchar_t说明,就可以完成ichar_traits的宽字符版本:

3.4 字符串的查找 - 图31

3.4 字符串的查找 - 图32

如同读者所见,这基本上是一个要求在源代码中的适当位置放置一个‘w’的练习。测试程序如下所示:

3.4 字符串的查找 - 图33

遗憾的是,某些编译器对宽字符仍然没有提供足够的支持。

[1]为了简化说明,这一版本不考虑嵌套标记,例如注释。

[2]使用数学方法来引发一些对erase()的调用,在此是很有吸引力的。由于某些情况下其操作数之一是string:npos(可能得到的最大无符号整型变量),整型溢出就可能发生,进而会搞垮整个算法。

[3]鉴于上述安全原因,C++标准制定委员会正考虑一个议案来对string:operator[]进行重新定义,以便在C++0x中使其与string:at()等价。

[4]读者实现时可定义这里的所有3个模板参数。由于最后两个模板参数有默认值,那样一个声明与在此写的内容是等价的。