返回介绍

4.2 字符串

发布于 2024-10-08 23:14:01 字数 9519 浏览 0 评论 0 收藏 0

字符串是存储在内存的连续字节中的一系列字符。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++编译器不允许这种不合理的做法(本章后面讨论指针后,将回过头来讨论这个问题)。

4.2.1 拼接字符串常量

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

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

4.2.2 在数组中使用字符串

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

程序清单 4.2 string.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 截短字符串

4.2.3 字符串输入

程序 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 章介绍。

4.2.4 每次读取一行字符串输入

每次读取一个单词通常不是最好的选择。例如,假设程序要求用户输入城市名,用户输入 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.2.5 混合输入字符串和数字

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

程序清单 4.6 numstr.cpp

该程序的运行情况如下:

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

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

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文