返回介绍

16.1 string 类

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

很多应用程序都需要处理字符串。C 语言在 string.h(在 C++中为 cstring)中提供了一系列的字符串函数,很多早期的 C++实现为处理字符串提供了自己的类。第 4 章介绍了 ANSI/ISO C++ string 类,而第 12 章创建了一个不大的 String 类,以说明设计表示字符串的类的某些方面。

string 类是由头文件 string 支持的(注意,头文件 string.h 和 cstring 支持对 C-风格字符串进行操纵的 C 库字符串函数,但不支持 string 类)。要使用类,关键在于知道它的公有接口,而 string 类包含大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。简而言之,string 类包含的内容很多。

16.1.1 构造字符串

先来看 string 的构造函数。毕竟,对于类而言,最重要的内容之一是,有哪些方法可用于创建其对象。程序清单 16.1 使用了 string 的 7 个构造函数(用 ctor 标识,这是传统 C++中构造函数的缩写)。表 16.1 简要地描述了这些构造函数,它首先使用顺序简要描述了程序清单 16.1 使用的 7 个构造函数,然后列出了 C++11 新增的两个构造函数。使用构造函数时都进行了简化,即隐藏了这样一个事实:string 实际上是模板具体化 basic_string<char>的一个 typedef,同时省略了与内存管理相关的参数(这将在本章后面和附录 F 中讨论)。size_type 是一个依赖于实现的整型,是在头文件 string 中定义的。string 类将 string::npos 定义为字符串的最大长度,通常为 unsigned int 的最大值。另外,表格中使用缩写 NBTS(null-terminated string)来表示以空字符结束的字符串——传统的 C 字符串。

表 16.1 string 类的构造函数

构 造 函 数

描 述

string(const char * s)

将 string 对象初始化为 s 指向的 NBTS

string(size_type n, char c)

创建一个包含 n 个元素的 string 对象,其中每个元素都被初始化为字符 c

string(const string & str)

将一个 string 对象初始化为 string 对象 str(复制构造函数)

string( )

创建一个默认的 sting 对象,长度为 0(默认构造函数)

string(const char * s, size_type n)

将 string 对象初始化为 s 指向的 NBTS 的前 n 个字符,即使超过了 NBTS 结尾

template<class Iter> string(Iter begin, Iter end)

将 string 对象初始化为区间[begin, end) 内的字符,其中 begin 和 end 的行为就像指针,用于指定位置,范围包括 begin 在内,但不包括 end

string(const string & str, string size_type pos = 0, size_type n = npos)

将一个 string 对象初始化为对象 str 中从位置 pos 开始到结尾的字符,或从位置 pos 开始的 n 个字符

string(string && str) noexcept

这是 C++11 新增的,它将一个 string 对象初始化为 string 对象 str,并可能修改 str(移动构造函数)

string(initializer_list<char> il)

这是 C++11 新增的,它将一个 string 对象初始化为初始化列表 il 中的字符

程序清单 16.1 str1.cpp

程序清单 16.1 中程序还使用了重载+=运算符,它将一个字符串附加到另一个字符串的后面;重载的=运算符用于将一个字符串赋给另一个字符串;重载的<<运算符用于显示 string 对象;重载的[ ]运算符用于访问字符串中的各个字符。

下面是程序清单 16.1 中程序的输出:

1.程序说明

程序清单 16.1 中的程序首先演示了可以将 string 对象初始化为常规的 C-风格字符串,然后使用重载的<<运算符来显示它:

接下来的构造函数将 string 对象 two 初始化为由 20 个$字符组成的字符串:

复制构造函数将 string 对象 three 初始化为 string 对象 one:

重载的+=运算符将字符串“Oops!”附加到字符串 one 的后面:

这里是将一个 C-风格字符串附加到一个 string 对象的后面。但+=运算符被多次重载,以便能够附加 string 对象和单个字符:

同样,=运算符也被重载,以便可以将 string 对象、C-风格字符串或 char 值赋给 string 对象:

重载[ ]运算符(就像第 12 章的 String 示例那样)使得可以使用数组表示法来访问 string 对象中的各个字符:

默认构造函数创建一个以后可对其进行赋值的空字符串:

第 2 行使用重载的+运算符创建了一个临时 string 对象,然后使用重载的=运算符将它赋给对象 four。正如所预料的,+运算符将其两个操作数组合成一个 string 对象。该运算符被多次重载,以便第二个操作数可以是 string 对象、C-风格字符串或 char 值。

第 5 个构造函数将一个 C-风格字符串和一个整数作为参数,其中的整数参数表示要复制多少个字符:

从输出可知,这里只使用了前 20 个字符(“All's well that ends”)来初始化 five 对象。正如表 16.1 指出的,如果字符数超过了 C-风格字符串的长度,仍将复制请求数目的字符。所以在上面的例子中,如果用 40 代替 20,将导致 15 个无用字符被复制到 five 的结尾处(即构造函数将内存中位于字符串“All's well that ends well”后面的内容作为字符)。

第 6 个构造函数有一个模板参数:

begin 和 end 将像指针那样,指向内存中两个位置(通常,begin 和 end 可以是迭代器——广泛用于 STL 中的广义化指针)。构造函数将使用 begin 和 end 指向的位置之间的值,对 string 对象进行初始化。[begin, end) 来自数学中,意味着包括 begin,但不包括 end 在内的区间。也就是说,end 指向被使用的最后一个值后面的一个位置。请看下面的语句:

由于数组名相当于指针,所以 alls + 6 和 alls +10 的类型都是 char *,因此使用模板时,将用类型 char *替换 Iter。第一个参数指向数组 alls 中的第一个 w,第二个参数指向第一个 well 后面的空格。因此,six 将被初始化为字符串“well”。图 16.1 说明了该构造函数的工作原理。

图 16.1 使用区间的 string 构造函数

现在假设要用这个构造函数将对象初始化为另一个 string 对象(假设为 five)的一部分内容,则下面的语句不管用:

原因在于,对象名(不同于数组名)不会被看作是对象的地址,因此 five 不是指针,所以 five + 6 是没有意义的。然而,five[6]是一个 char 值,所以&five[6]是一个地址,因此可被用作该构造函数的一个参数。

第 7 个构造函数将一个 string 对象的部分内容复制到构造的对象中:

上述语句从 four 的第 8 个字符(位置 7)开始,将 16 个字符复制到 eight 中。

2.C++11 新增的构造函数

构造函数 string(string && str)类似于复制构造函数,导致新创建的 string 为 str 的副本。但与复制构造函数不同的是,它不保证将 str 视为 const。这种构造函数被称为移动构造函数(move constructor)。在有些情况下,编译器可使用它而不是复制构造函数,以优化性能。第 18 章的“移动语义和右值引用”一节将讨论这个主题。

构造函数 string(initializer_list<char> il)让您能够将列表初始化语法用于 string 类。也就是说,它使得下面这样的声明是合法的:

就 string 类而言,这可能用处不大,因为使用 C-风格字符串更容易,但确实实现了让列表初始化语法普遍实用的意图。本章后面将更深入地讨论模板 initializer_list。

16.1.2 string 类输入

对于类,很有帮助的另一点是,知道有哪些输入方式可用。对于 C-风格字符串,有 3 种方式:

对于 string 对象,有两种方式:

两个版本的 getline( ) 都有一个可选参数,用于指定使用哪个字符来确定输入的边界:

在功能上,它们之间的主要区别在于,string 版本的 getline( ) 将自动调整目标 string 对象的大小,使之刚好能够存储输入的字符:

自动调整大小的功能让 string 版本的 getline( ) 不需要指定读取多少个字符的数值参数。

在设计方面的一个区别是,读取 C-风格字符串的函数是 istream 类的方法,而 string 版本是独立的函数。这就是对于 C-风格字符串输入,cin 是调用对象;而对于 string 对象输入,cin 是一个函数参数的原因。这种规则也适用于>>形式,如果使用函数形式来编写代码,这一点将显而易见:

下面更深入地探讨一下 string 输入函数。正如前面指出的,这两个函数都自动调整目标 string 的大小,使之与输入匹配。但也存在一些限制。第一个限制因素是 string 对象的最大允许长度,由常量 string::npos 指定。这通常是最大的 unsigned int 值,因此对于普通的交互式输入,这不会带来实际的限制;但如果您试图将整个文件的内容读取到单个 string 对象中,这可能成为限制因素。第二个限制因素是程序可以使用的内存量。

string 版本的 getline( ) 函数从输入中读取字符,并将其存储到目标 string 中,直到发生下列三种情况之一:

  • 到达文件尾,在这种情况下,输入流的 eofbit 将被设置,这意味着方法 fail( ) 和 eof( ) 都将返回 true;
  • 遇到分界字符(默认为\n),在这种情况下,将把分界字符从输入流中删除,但不存储它;
  • 读取的字符数达到最大允许值(string::npos 和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的 failbit,这意味着方法 fail( ) 将返回 true。

输入流对象有一个统计系统,用于跟踪流的错误状态。在这个系统中,检测到文件尾后将设置 eofbit 寄存器,检测到输入错误时将设置 failbit 寄存器,出现无法识别的故障(如硬盘故障)时将设置 badbit 寄存器,一切顺利时将设置 goodbit 寄存器。第 17 章将更深入地讨论这一点。

string 版本的 operator>>( ) 函数的行为与此类似,只是它不断读取,直到遇到空白字符并将其留在输入队列中,而不是不断读取,直到遇到分界字符并将其丢弃。空白字符指的是空格、换行符和制表符,更普遍地说,是任何将其作为参数来调用 isspace( ) 时,该函数返回 ture 的字符。

本书前面有多个控制台 string 输入示例。由于用于 string 对象的输入函数使用输入流,能够识别文件尾,因此也可以使用它们来从文件中读取输入。程序清单 16.2 是一个从文件中读取字符串的简短示例,它假设文件中包含用冒号字符分隔的字符串,并使用指定分界符的 getline( ) 方法。然后,显示字符串并给它们编号,每个字符串占一行。

程序清单 16.2 strfile.cpp

下面是文件 tobuy.txt 的内容:

通常,对于程序要查找的文本文件,应将其放在可执行程序或项目文件所在的目录中;否则必须提供完整的路径名。在 Windows 系统中,C-风格字符串中的转义序列\表示一个斜杠:

下面是程序清单 16.2 中程序的输出:

注意,将:指定为分界字符后,换行符将被视为常规字符。因此文件 tobuy.txt 中第一行末尾的换行符将成为包含“cottage cheese”的字符串中的第一个字符。同样,第二行末尾的换行符是第 9 个输入字符串中唯一的内容。

16.1.3 使用字符串

现在,您知道可以使用不同方式来创建 string 对象、显示 string 对象的内容、将数据读取和附加到 string 对象中、给 string 对象赋值以及将两个 string 对象连结起来。除此之外,还能做些什么呢?

可以比较字符串。String 类对全部 6 个关系运算符都进行了重载。如果在机器排列序列中,一个对象位于另一个对象的前面,则前者被视为小于后者。如果机器排列序列为 ASCII 码,则数字将小于大写字符,而大写字符小于小写字符。对于每个关系运算符,都以三种方式被重载,以便能够将 string 对象与另一个 string 对象、C-风格字符串进行比较,并能够将 C-风格字符串与 string 对象进行比较:

可以确定字符串的长度。size( ) 和 length( ) 成员函数都返回字符串中的字符数:

为什么这两个函数完成相同的任务呢?length( ) 成员来自较早版本的 string 类,而 size( ) 则是为提供 STL 兼容性而添加的。

可以以多种不同的方式在字符串中搜索给定的子字符串或字符。表 16.2 简要地描述了 find( ) 方法的 4 个版本。如前所述,string ::npos 是字符串可存储的最大字符数,通常是无符号 int 或无符号 long 的最大取值。

表 16.2 重载的 find( ) 方法

方 法 原 型

描 述

size_type find(const string & str, size_type pos = 0)const

从字符串的 pos 位置开始,查找子字符串 str。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos

size_type find(const char * s, size_type pos = 0)const

从字符串的 pos 位置开始,查找子字符串 s。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos

size_type find(const char * s, size_type pos = 0, size_type n)

从字符串的 pos 位置开始,查找 s 的前 n 个字符组成的子字符串。如果找到,则返回该子字符串首次出现时其首字符的索引;否则,返回 string :: npos

size_type find(char ch, size_type pos = 0)const

从字符串的 pos 位置开始,查找字符 ch。如果找到,则返回该字符首次出现的位置;否则,返回 string :: npos

string 库还提供了相关的方法:rfind( )、find_first_of( )、find_last_of( )、find_first_not_of( ) 和 find_last_not_of( ),它们的重载函数特征标都与 find( ) 方法相同。rfind( ) 方法查找子字符串或字符最后一次出现的位置;find_first_of( ) 方法在字符串中查找参数中任何一个字符首次出现的位置。例如,下面的语句返回 r 在“cobra”中的位置(即索引 3),因为这是“hark”中各个字母在“cobra”首次出现的位置:

find_last_of( ) 方法的功能与此相同,只是它查找的是最后一次出现的位置。因此,下面的语句返回 a 在“cobra”中的位置:

find_first_not_of( ) 方法在字符串中查找第一个不包含在参数中的字符,因此下面的语句返回 c 在“cobra”中的位置,因为“hark”中没有 c:

在本章最后的练习中,您将了解 find_last_not_of( )。

还有很多其他的方法,这些方法足以创建一个非图形版本的 Hangman 拼字游戏。该游戏将一系列的单词存储在一个 string 对象数组中,然后随机选择一个单词,让人猜测单词的字母。如果猜错 6 次,玩家就输了。该程序使用 find( ) 函数来检查玩家的猜测,使用+=运算符创建一个 string 对象来记录玩家的错误猜测。为记录玩家猜对的情况,程序创建了一个单词,其长度与被猜的单词相同,但包含的是连字符。玩家猜对字符时,将用该字符替换相应的连字符。程序清单 16.3 列出了该程序的代码。

程序清单 16.3 hangman.cpp

程序清单 16.3 中程序的运行情况如下:

程序说明

在程序清单 16.3 中,由于关系运算符被重载,因此可以像对待数值变量那样对待字符串:

与对 C-风格字符串使用 strcmp( ) 相比,这样简单些。

该程序使用 find( ) 来检查玩家以前是否猜过某个字符。如果是,则它要么位于 badchars 字符串(猜错)中,要么位于 attempt 字符串(猜对)中:

npos 变量是 string 类的静态成员,它的值是 string 对象能存储的最大字符数。由于索引从 0 开始,所以它比最大的索引值大 1,因此可以使用它来表示没有查找到字符或字符串。

该程序利用了这样一个事实:+=运算符的某个重载版本使得能够将一个字符附加到字符串中:

该程序的核心是从检查玩家选择的字符是否位于被猜测的单词中开始的:

如果 loc 是一个有效的值,则可以将该字母放置在答案字符串的相应位置:

然而,由于字母在被猜测的单词中可能出现多次,所以程序必须一直进行检查。该程序使用了 find( ) 的第二个可选参数,该参数可以指定从字符串什么位置开始搜索。因为字母是在位置 loc 找到的,所以下一次搜索应从 loc+1 开始。while 循环使搜索一直进行下去,直到找不到该字符为止。如果 loc 位于字符串尾,则表明 find( ) 没有找到该字符。

16.1.4 string 还提供了哪些功能

string 库提供了很多其他的工具,包括完成下述功能的函数:删除字符串的部分或全部内容、用一个字符串的部分或全部内容替换另一个字符串的部分或全部内容、将数据插入到字符串中或删除字符串中的数据、将一个字符串的部分或全部内容与另一个字符串的部分或全部内容进行比较、从字符串中提取子字符串、将一个字符串中的内容复制到另一个字符串中、交换两个字符串的内容。这些函数中的大多数都被重载,以便能够同时处理 C-风格字符串和 string 对象。附录 F 简要地介绍了 string 库中的函数。

首先来看自动调整大小的功能。在程序清单 16.3 中,每当程序将一个字母附加到字符串末尾时将发生什么呢?不能仅仅将已有的字符串加大,因为相邻的内存可能被占用了。因此,可能需要分配一个新的内存块,并将原来的内容复制到新的内存单元中。如果执行大量这样的操作,效率将非常低,因此很多 C++实现分配一个比实际字符串大的内存块,为字符串提供了增大空间。然而,如果字符串不断增大,超过了内存块的大小,程序将分配一个大小为原来两倍的新内存块,以提供足够的增大空间,避免不断地分配新的内存块。方法 capacity( ) 返回当前分配给字符串的内存块的大小,而 reserve( ) 方法让您能够请求内存块的最小长度。程序清单 16.4 是一个使用这些方法的示例。

程序清单 16.4 str2.cpp

下面是使用某种 C++实现时,程序清单 16.4 中程序的输出:

注意,该实现使用的最小容量为 15 个字符,这比标准容量选择(16 的倍数)小 1。其他实现可能做出不同的选择。

如果您有 string 对象,但需要 C-风格字符串,该如何办呢?例如,您可能想打开一个其名称存储在 string 对象中的文件:

不幸的是,open( ) 方法要求使用一个 C-风格字符串作为参数;幸运的是,c_str( ) 方法返回一个指向 C-风格字符串的指针,该 C-风格字符串的内容与用于调用 c_str( ) 方法的 string 对象相同。因此可以这样做:

16.1.5 字符串种类

本节将 string 类看作是基于 char 类型的。事实上,正如前面指出的,string 库实际上是基于一个模板类的:

模板 basic_string 有 4 个具体化,每个具体化都有一个 typedef 名称:

这让您能够使用基于类型 wchar_t、char16_t、char32_t 和 char 的字符串。甚至可以开发某种类似字符的类,并对它使用 basic_string 类模板(只要它满足某些要求)。traits 类描述关于选定字符类型的特定情况,如如何对值进行比较。对于 wchar_t、char16_t、char32_t 和 char 类型,有预定义的 char_traits 模板具体化,它们都是 traits 的默认值。Allocator 是一个管理内存分配的类。对于各种字符类型,都有预定义的 allocator 模板具体化,它们都是默认的。它们使用 new 和 delete。

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

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

发布评论

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