3.3 对字符串进行操作

有用C语言编程经验的人,都习惯用函数族对char型数组进行写入、查找、修改和复制等操作。对于char型数组的处理,标准C语言库函数中有两个方面不太尽如人意。首先,这些函数分为两族(family),组织得十分松散:无格式(plain)族,以及那些在随后的操作中需要提供计算字符个数的函数族。C语言提供的用于处理char型字符数组的那些库函数的函数名列表不但冗长,而且充满了模糊不清、晦涩难懂的名字,其中大部分的名字叫人读不出来,这些都让虔诚的用户很吃惊。虽然这些函数的参数类型及个数颇为一致,但想要用好这些函数,程序员必须对函数命名和参数传递的细节等慎之又慎。

标准C语言的char型数组工具中存在着其固有的第2个误区,那就是它们都显式地依赖一种假设:字符数组包括一个空结束符。若由于疏忽或是其他差错,这个空结束符被忽略或重写,这个小小的差错就会使C语言的char型数组处理函数几乎不可避免地操作其已分配空间之外的内存,有时会带来灾难性的后果。

C++提供的string类对象,在使用的便利性和安全性上都有很大的提高。为了实际的字符串处理操作,在string类中,不同名的成员函数的数量几乎跟C语言库中的函数一样多,但是由于有重载,使string类的功能更加强大。这些特征再加上C++命名机制理性化以及明智地使用了默认参数,使string类比起C语言库的char型数组函数更便于使用。

3.3.1 追加、插入和连接字符串

C++字符串有几个颇具价值而且最便于使用的特色,其中之一就是:无需程序员干预,它们可根据需要自行扩充规模。这不仅使得字符串处理代码更加可靠,同时也几乎完全消除了令人生厌的“内务处理”琐事—跟踪字符串的存储边界。比方说,创建一个字符串对象并且将其初始化成一个由50个‘X’组成的字符串,然后再存进50个“Zowie”,这个字符串对象自己会自动重新分配足够的存储空间来适应数据的增长。如果代码处理的字符串改变了长度,但程序员并不知道改变的幅度,也许只有这时读者才能最真切地感受到C++字符串的优越性。此外,当字符串增长时,字符串成员函数append()和insert()很明显地重新分配了存储空间:

3.3 对字符串进行操作 - 图1

3.3 对字符串进行操作 - 图2

下面是来自特定编译器的输出:

3.3 对字符串进行操作 - 图3

这个例子证实了,即使可以安全地避免分配及管理string对象所占用的存储空间的工作,C++string类也提供了几个工具以便监视和管理它们的存储规模。注意到改变分配给字符串的存储空间的规模是多么轻松了吧。size()函数返回当前在字符串存储的字符数,它跟length()成员函数的作用是一样的。capacity()函数返回当前分配的存储空间的规模,也即在没有要求更多存储空间时,字符串所能容纳的最大字符数。reserve()函数提供一种优化机制,它按照程序员的意图,预留一定数量的存储空间,以便将来使用;capacity()返回的值不小于最近一次调用reserve()所使用的值。如果要生成的新字符串的规模比当前的字符串大或者说是需要截短原字符串,resize()函数就会在字符串的末尾追加空格。(resize()的一个重载可以指定一个不同的填充字符。)

string类的成员函数为数据分配存储空间的确切方式取决于C++类库的实现。在使用C++类库的某种实现来测试上述例子时,读者会发现,当系统进行存储空间再分配遇到偶数字(word)(即,全整数(full-integer))的边界时,会隐含增加一个字节。为什么会这样呢?string类的设计者曾做过不懈的努力让char型数组和C++字符串对象可以混合使用,为此,在这种特定的实现中,StrSize.cpp报告的存储容量数字,意味着预留出一个字节以便很容易地容纳空结束符(用char型数组表示一个字符串时,该字符串的最后一个表示串结束的字符)的插入。

3.3.2 替换字符串中的字符

insert()函数使程序员放心地向字符串中插入字符,而不必担心会使存储空间越界,或者会改写插入点之后紧跟的字符。存储空间增大了,原有的字符会很“礼貌地”改变其存储位置,以便安置新元素。但有时这并不是程序员所希望的。如果希望字符串的大小保持不变,就应该使用replace()函数来改写字符。replace()有很多的重载版本,最简单的版本用了3个参数:一个参数用于指示从字符串的什么位置开始改写;第二个参数用于指示从原字符串中剔除多少个字符;另外一个是替换字符串(它所包含的字符数可以与被剔除的字符数不同)。举例如下:

3.3 对字符串进行操作 - 图4

3.3 对字符串进行操作 - 图5

tag串首先插入到s串中(注意:在函数调用中的第1个参数值指示的插入点之前进行插入,并且在tag串后添加一个额外的空字符),接着进行查找和替换。

在调用replace()前程序员应检查是否会找到什么。前面的例子用一个char*来进行替换操作,replace()还有一个重载版本,用一个string来进行替换操作。下面的例子更完整地演示了replace()函数:

3.3 对字符串进行操作 - 图6

如果replace找不到要查找的字符串,它返回string:npos。数据成员npos是string类的一个静态常量成员,它表示一个不存在的字符位置。[1]

当有新字符复制到现存的一串序列的元素中间时,replace()并不增加string的存储空间规模,这一点与insert()不同。但是,replace()必要时也会增加存储空间,例如当所做的“替换”会使原字符串扩充到超越当前分配的存储边界时。举例如下:

3.3 对字符串进行操作 - 图7

3.3 对字符串进行操作 - 图8

对replace()的调用使“替换”超出了原有序列的边界,这与追加操作是等价的。注意,此例中replace()扩展了相应的串序列的规模。

读者可能会不辞劳苦地研读本章,试图找到相对简单的题目,如用一个字符替换字符串中各处出现的另一不同字符。一旦找到前面这些关于替换的材料,读者就会认为找到了答案,然后就开始学习貌似很复杂的材料,如替换字符组和计数等。难道string类就没有一种方法用一个字符替换字符串中各处出现的另一个字符吗?

借助如下的find()和replace()成员函数,可很容易地实现上述函数:

3.3 对字符串进行操作 - 图9

此处使用的find()版本将开始查找的位置作为第2参数,如果找不到则返回string:npos。将变量LookHere表示的位置传送到替换串,这是很重要的,以防字符串from是字符串to的子串。下面的程序测试了replaceAll函数:

3.3 对字符串进行操作 - 图10

3.3 对字符串进行操作 - 图11

大家知道,string类自身并不能解决所有可能出现的问题。许多解决方案都是由标准库[2]中的算法完成的,因为string类几乎可与STL序列等价(借助于前面所说的迭代器)。所有通用算法的工作对象都是容器中某个“范围”内的元素。通常这个范围指的是“从容器前端到末尾”。string对象看上去就像是字符的容器:可用string:begin()得到容器范围的前端,用string:end()得到其末尾。下面的例子显示了如何使用replace()算法将所有单个的字符‘X’替换为‘Y’:

3.3 对字符串进行操作 - 图12

注意,这里调用的replace()并不是string的成员函数。另外,replace()算法将字符串中出现的某个字符全部用另一个字符替换掉,这一点与string:replace()函数不同,因为后者只进行一次替换。

replace()算法的工作对象只是单一的对象(本例中是char对象),它不会替换引用char型数组或string对象。由于string很像一个STL序列,很多其他算法对它也适用,这些算法可以解决string类的成员函数没能直接解决的问题。

3.3.3 使用非成员重载运算符连接

对于一个学习C++string处理的C程序员来说,等待他的最令人欣喜的发现之一就是,借助operator+和operator+=可以如此轻而易举地实现string的合并与追加。这些运算符使合并串的操作在语法上类似于数值型数据的加法运算:

3.3 对字符串进行操作 - 图13

3.3 对字符串进行操作 - 图14

使用operator+和operator+=运算符是合并string数据的一种既灵活又方便的方法。在语句的右边,程序员几乎可以采用任意一种样式对由单字符或多字符构成的分组进行赋值。

[1]它是“无位置”(no position)的缩写,并且是字符串分配算符size_type(默认是std:size_t)所能表示的最大值。

[2]将在第6章详述。