购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.2 字符串

字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种。第一种来自C语言,常被称为C-风格字符串(C-style string)。本章将首先介绍它,然后介绍另一种基于string类库的方法。

存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都位于自己的数组元素中。字符串提供了一种存储文本信息的便捷方式,如提供给用户的消息(“请告诉我您的瑞士银行账号”)或来自用户的响应(“您肯定在开玩笑”)。C-风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0,其ASCII码为0,用来标记字符串的结尾。例如,请看下面两个声明:

这两个数组都是char数组,但只有第二个数组是字符串。空字符对C-风格字符串而言至关重要。例如,C++有很多处理字符串的函数,其中包括cout使用的那些函数。它们都逐个地处理字符串中的字符,直到到达空字符为止。如果使用cout显示上面的cat这样的字符串,则将显示前7个字符,发现空字符后停止。但是,如果使用cout显示上面的dog数组(它不是字符串),cout将打印出数组中的8个字母,并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。由于空字符(实际上是被设置为0的字节)在内存中很常见,因此这一过程将很快停止。但尽管如此,还是不应将不是字符串的字符数组当作字符串来处理。

在cat数组示例中,将数组初始化为字符串的工作看上去冗长乏味—使用大量单引号,且必须记住加上空字符。不必担心,有一种更好的、将字符数组初始化为字符串的方法—只需使用一个用引号括起的字符串即可,这种字符串被称为字符串常量(string constant)或字符串字面值(string literal),如下所示:

用引号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它(参见图4.2)。另外,各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字符(如果在运行程序清单4.1中的程序时发现,必须使用关键字static来初始化数组,则初始化上述char数组时也必须使用该关键字)。

当然,应确保数组足够大,能够存储字符串中所有字符—包括空字符。使用字符串常量初始化字符数组是这样的一种情况,即让编译器计算元素数目更为安全。让数组比字符串长没有什么害处,只是会浪费一些空间而已。这是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理。C++对字符串长度没有限制。


警告:

在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内。


图4.2 将数组初始化为字符串

注意,字符串常量(使用双引号)不能与字符常量(使用单引号)互换。字符常量(如'S')是字符串编码的简写表示。在ASCII系统上,'S'只是83的另一种写法,因此,下面的语句将83赋给shirt_size:

但"S"不是字符常量,它表示的是两个字符(字符S和\0)组成的字符串。更糟糕的是,"S"实际上表示的是字符串所在的内存地址。因此下面的语句试图将一个内存地址赋给shirt_size:

由于地址在C++中是一种独立的类型,因此C++编译器不允许这种不合理的做法(本章后面讨论指针后,将回过头来讨论这个问题)。

有时候,字符串很长,无法放到一行中。C++允许拼接字符串字面值,即将两个用引号括起的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。因此,下面所有的输出语句都是等效的:

注意,拼接时不会在被连接的字符串之间添加空格,第二个字符串的第一个字符将紧跟在第一个字符串的最后一个字符(不考虑\0)后面。第一个字符串中的\0字符将被第二个字符串的第一个字符取代。

要将字符串存储到数组中,最常用的方法有两种—将数组初始化为字符串常量、将键盘或文件输入读入到数组中。程序清单4.2演示了这两种方法,它将一个数组初始化为用引号括起的字符串,并使用cin将一个输入字符串放到另一个数组中。该程序还使用了标准C语言库函数strlen( )来确定字符串的长度。标准头文件cstring(老式实现为string.h)提供了该函数以及很多与字符串相关的其他函数的声明。

程序清单4.2 strings.cpp

下面是该程序的运行情况:


程序说明

从程序清单4.2中可以学到什么呢?首先,sizeof运算符指出整个数组的长度:15字节,但strlen( )函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度。另外,strlen( )只计算可见的字符,而不把空字符计算在内。因此,对于Basicman,返回的值为8,而不是9。如果cosmic是字符串,则要存储该字符串,数组的长度不能短于strlen(cosmic)+1。

由于name1和name2是数组,所以可以用索引来访问数组中各个字符。例如,该程序使用name1[0]找到数组的第一个字符。另外,该程序将name2[3]设置为空字符。这使得字符串在第3个字符后即结束,虽然数组中还有其他的字符(参见图4.3)。


该程序使用符号常量来指定数组的长度。程序常常有多条语句使用了数组长度。使用符号常量来表示数组长度后,当需要修改程序以使用不同的数组长度时,工作将变得更简单—只需在定义符号常量的地方进行修改即可。

图4.3 使用\0截短字符串

程序strings.cpp有一个缺陷,这种缺陷通过精心选择输入被掩盖掉了。程序清单4.3揭开了它的面纱,揭示了字符串输入的技巧。

程序清单4.3 instr1.cpp

该程序的意图很简单:读取来自键盘的用户名和用户喜欢的甜点,然后显示这些信息。下面是该程序的运行情况:

我们甚至还没有对“输入甜点的提示”作出反应,程序便把它显示出来了,然后立即显示最后一行。

cin是如何确定已完成字符串输入呢?由于不能通过键盘输入空字符,因此cin需要用别的方法来确定字符串的结尾位置。cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。

这个例子的实际结果是,cin把Alistair作为第一个字符串,并将它放到name数组中。这把Dreeb留在输入队列中。当cin在输入队列中搜索用户喜欢的甜点时,它发现了Dreeb,因此cin读取Dreeb,并将它放到dessert数组中(参见图4.4)。

图4.4 使用cin读取字符串输入时的情况

另一个问题是,输入字符串可能比目标数组长(运行中没有揭示出来)。像这个例子一样使用cin,确实不能防止将包含30个字符的字符串放到20个字符的数组中的情况发生。

很多程序都依赖于字符串输入,因此有必要对该主题做进一步探讨。我们必须使用cin的较高级特性,这将在第17章介绍。

每次读取一个单词通常不是最好的选择。例如,假设程序要求用户输入城市名,用户输入New York或Sao Paulo。您希望程序读取并存储完整的城市名,而不仅仅是New或Sao。要将整条短语而不是一个单词作为字符串输入,需要采用另一种字符串读取方法。具体地说,需要采用面向行而不是面向单词的方法。幸运的是,istream中的类(如cin)提供了一些面向行的类成员函数:getline( )和get( )。这两个函数都读取一行输入,直到到达换行符。然而,随后getline( )将丢弃换行符,而get( )将换行符保留在输入序列中。下面详细介绍它们,首先介绍getline( )。

1.面向行的输入:getline( )

getline( )函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用cin.getline( )。该函数有两个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。如果这个参数为20,则函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。getline( )成员函数在读取指定数目的字符或遇到换行符时停止读取。

例如,假设要使用getline( )将姓名读入到一个包含20个元素的name数组中。可以使用这样的函数调用:

这将把一行读入到name数组中—如果这行包含的字符不超过19个。(getline( )成员函数还可以接受第三个可选参数,这将在第17章讨论。)

程序清单4.4将程序清单4.3修改为使用cin.getline( ),而不是简单的cin。除此之外,该程序没有做其他修改。

程序清单4.4 instr2.cpp

下面是该程序的输出:

该程序现在可以读取完整的姓名以及用户喜欢的甜点!getline( )函数每次读取一行。它通过换行符来确定行尾,但不保存换行符。相反,在存储字符串时,它用空字符来替换换行符(参见图4.5)。

图4.5 getline( )读取并替换换行符

2.面向行的输入:get( )

我们来试试另一种方法。istream类有另一个名为get( )的成员函数,该函数有几种变体。其中一种变体的工作方式与getline( )类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但get并不再读取并丢弃换行符,而是将其留在输入队列中。假设我们连续两次调用get( ):

由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符便是换行符。因此get( )认为已到达行尾,而没有发现任何可读取的内容。如果不借助于帮助,get( )将不能跨过该换行符。

幸运的是,get( )有另一种变体。使用不带任何参数的cin.get( )调用可读取下一个字符(即使是换行符),因此可以用它来处理换行符,为读取下一行输入做好准备。也就是说,可以采用下面的调用序列:

另一种使用get( )的方式是将两个类成员函数拼接起来(合并),如下所示:

之所以可以这样做,是由于cin.get(name,ArSize)返回一个cin对象,该对象随后将被用来调用get( )函数。同样,下面的语句将把输入中连续的两行分别读入到数组name1和name2 中,其效果与两次调用cin.getline( )相同:

程序清单4.5采用了拼接方式。第11章将介绍如何在类定义中使用这项特性。

程序清单4.5 instr3.cpp

下面是程序清单4.5中程序的运行情况:

需要指出的一点是,C++允许函数有多个版本,条件是这些版本的参数列表不同。如果使用的是cin.get(name,ArSize),则编译器知道是要将一个字符串放入数组中,因而将使用适当的成员函数。如果使用的是cin.get( ),则编译器知道是要读取一个字符。第8章将探索这种特性—函数重载。

为什么要使用get( ),而不是getline( )呢?首先,老式实现没有getline( )。其次,get( )使输入更仔细。例如,假设用get( )将一行读入数组中。如何知道停止读取的原因是已经读取了整行,而不是数组已填满呢?查看下一个输入字符,如果是换行符,说明已读取了整行;否则,说明该行中还有其他输入。第17章将介绍这种技术。总之,getline( )使用起来简单一些,但get( )使得检查错误更简单些。可以用其中的任何一个来读取一行输入;只是应该知道,它们的行为稍有不同。

3.空行和其他问题

当getline( )或get( )读取空行时,将发生什么情况?最初的做法是,下一条输入语句将在前一条getline( )或get( )结束读取的位置开始读取;但当前的做法是,当get( )(不是getline( ))读取空行后将设置失效位(failbit)。这意味着接下来的输入将被阻断,但可以用下面的命令来恢复输入:

另一个潜在的问题是,输入字符串可能比分配的空间长。如果输入行包含的字符数比指定的多,则getline( )和get( )将把余下的字符留在输入队列中,而getline( )还会设置失效位,并关闭后面的输入。

第5、6章和第17章将介绍这些属性,并探讨程序如何避免这些问题。

混合输入数字和面向行的字符串会导致问题。请看程序清单4.6中的简单程序。

程序清单4.6 numstr.cpp

该程序的运行情况如下:

用户根本没有输入地址的机会。问题在于,当cin读取年份,将回车键生成的换行符留在了输入队列中。后面的cin.getline( )看到换行符后,将认为是一个空行,并将一个空字符串赋给address数组。解决之道是,在读取地址之前先读取并丢弃换行符。这可以通过几种方法来完成,其中包括使用没有参数的get( )和使用接受一个char参数的get( ),如前面的例子所示。可以单独进行调用:

也可以利用表达式cin>>year返回cin对象,将调用拼接起来:

按上述任何一种方法修改程序清单4.6后,它便可以正常工作:

C++程序常使用指针(而不是数组)来处理字符串。我们将在介绍指针后,再介绍字符串这个方面的特性。下面介绍一种较新的处理字符串的方式:C++ string类。 yulBazwkSCbnPurCFC/xA0h/Y2+6W9RZUZzod8T8+yLU/EKG1kKqHDYdReZTrDu9

点击中间区域
呼出菜单
上一章
目录
下一章
×