C++ chapter3 标准库类型
除了这些在语言中定义的类型外,C++标准库还定义了许多更高级的抽象数据类型(abstract data type)。之所以说这些标准库类型是更高级的,是因为其中反映了更复杂的概念;之所以说它们是抽象的,是因为我们在使用时不需要关心它们是如何表示的,只需指定这些抽象数据类型支持哪些操作就可以了。
两种最重要的标准库类型是string
和vector
。string
类型支持长度可变的字符串,vector
可用于保存一组指定类型的对象。说它们重要,是因为它们在C++定义的级别类型基础上作了一些改进。
另一种标准库类型提供了更方便和合理有效的语言级的抽象设施,它就是bitset
类。通过这个类可以把某个值当作位的集合来处理。与位操作符相比,bitset
类提供操作位更直接的方法。
命名空间的using声明
在前面看到的程序都是通过直接说明名字来自std
命名空间,来引用标准库中的名字。这些名字都使用了::
操作符,该操作符是作用域操作符。它的含义是右操作数的名字可以在左操作数的作用域中找到。因此,std::cin
的意思是说所需名字cin
是在命名空间std
中定义的。显然,这样这样非常麻烦。
C++提供了更简洁的方式来使用命名空间成员。using
声明。
使用using
声明可以在不需要加前缀namespace_name::
的情况下访问命名空间中的名字。
1 |
|
一旦使用了using
声明,我们就可以直接引用名字,而不需要再引用该名字的命名空间:
1 |
|
1.每个名字都需要一个using
声明
一个using
声明一次只能作用于一个命名空间成员。using
声明可用来明确指定在程序中用到的命名空间中的名字,如果希望使用std
中的几个名字,则必须为要用到的每个名字都提供一个using
声明。
1 |
|
2.使用标准库类型的类定义
有一种情况下,必须总是使用完全限定的标准库名字:在头文件中。理由是头文件的内容会被预处理器复制到程序中。用#include
包含文件时,相当于头文件中的文本将称为我们编写的文件的一部分。如果在头文件中放置using
声明,就相当于在包含该头文件的每个程序中都放置了同一using
声明,不论该程序是否需要using
声明。
注意:在编译我们提供的实例程序前,读者一定要注意在程序中添加适当的
#include
和using
声明。
标准库string类型
string
类型支持长度可变的字符串,C++标准库将负责管理与存储字符相关的内存,以及提供各种有用的操作符。标准库string
类型的目的就是满足对字符串的一般应用。
与其他的标准库类型一样,用户程序要使用string
类型对象,必须包含相关头文件。如果提供了合适的using
声明,那么编写处理的程序将会变得简短些:
1 |
|
string对象的定义和初始化
string
标准库支持几个构造函数。构造函数是一个特殊成员函数,定义如何初始化该类型的对象下表列出了几个string
类型常用的构造函数。当没有明确指定对象初始化式时,系统将使用默认构造函数。
几种格式化string对象的方式 | |
---|---|
string s1; |
默认构造函数,s1为空串 |
string s2(21); |
将s2初始化为s1的一个副本 |
string s3("value") ; |
将s3初始化为一个字符串字面值 |
string s4(n,'c') ; |
将s4初始化为字符’c’的n个副本 |
警告:标准库string类型和字符串字面值
因为历史原因以及未来与C语言兼容,字符串字面值与标准库string类型不是同一种类型。
string对象的读写
1 |
|
从标准输入读取string
,并将读入的串存储在s中。string
类型的输入操作符:
- 读取并忽略开头所有的空白字符(入空格,换行符,制表符)。
- 读取字符直至再次遇到空白字符,读取终止。
输入和输出操作的行为与内置类型操作符级别类似。
1.读入未知数目的string
对象
和内置类型的输入操作符一样,string
的输入操作符也会返回所读的数据流。因此,可以把输入操作作为判断条件。
下面的程序将从标准输入读取一组string
对象,然后在标准输出上逐行输出:
1 |
|
2.用getline
读取整行文本
这个函数接受两个参数:一个输入流对象和一个string
对象。getline
函数从输入流的下一行读取,并保存读取的内容到string
中,但不包括换行符。和输入操作符不一样的是,getline
并不忽略开头的换行符。只要getline
遇到换行符,即便它是输入的第一个字符,getline
也将停止读入并返回。如果第一个字符就是换行符,则string
参数将被置为空string
。
getline
函数将istream
参数作为返回值,和输入操作符一样也把它用作判断条件。
1 |
|
由于getline函数返回时丢弃换行符,换行符将不会存储在
string
对象中。
string对象的操作
string操作 | |
---|---|
s.empty() |
如果s为空字符串,则返回true,否则返回false |
s.size() |
返回s中字符的个数 |
s[n] |
返回s位置为n的字符,位置从0开始计数 |
s1+s2 |
把s1和s2连接成一个新字符串,返回新生成的字符串 |
s1=s2 |
把s1内容替换为s2的副本 |
v1==v2 |
比较v1与v2的内容,相等则返回true,否则返回false |
!=,<,<=, |
保持这些操作符惯有的含义 |
>和>= |
|
1.string 的size 和empty 操作 |
string
对象的长度指的是string
对象中字符的个数,可以通过size
操作获取:
1 |
|
了解string
对象是否为空是有用的。一种方法是将size
与0
进行比较:
1 |
|
本例中,程序员并不需要知道string
对象中有多少个字符,只想知道size
是否为0
。用string
的成员函数empty()
可以直接回答这个问题:
1 |
|
empty()
成员函数将返回bool
值,如果string
对象为空则返回true
,否则返回false
。
2.string::size_type
类型
从逻辑上来讲,size()
成员函数似乎应该返回整型数值,或为无符号整数。
但实际上,size
操作返回的是string::size_type
类型的值。我们需要对这种类型做一些解释。
string
类类型和许多其他库类型都定义了一些配套类型(companion type)。通过这些配套类型,库类型的使用就能与机器无关(machine-independent)。size_type
就是这些配套类型中的一种。它定义为与unsigned
型(unsigned int 或 unsigned long)具有相同的含义,而且可以保证足够大能够存储任意string
对象的长度。未来使用由string
类型定义的size_type
类型,程序员必须加上作用域操作符来说明使用的size_type
类型是由string
类定义的。
注意:任何存储
string
的size
操作结果的变量必须为string::size_type
类型。
3.string
关系操作符
string
类定义了几种关系操作符用来比较两个string
值的大小。这些操作符实际上是比较每个string
对象的字符。
string
对象比较操作是区分大小写的,即同一个字符的大小写形式被认为是两个不同的字符。在多数计算机上,大写的字符位于小写字母之前:任何一个大写字母都小于任意的小写字母。
==
操作符比较两个string
对象,如果它们相等,则返回true
。两个string
对象相等是指它们的长度相同,且含有相同的字符。标准库还定义了!=
操作符来测试两个string
对象是否不等。
关系操作符<,<=,>,>
分别用于测试一个string
对象是否小于、小于或等于、大于、大于或等于另一个string
对象:
1 |
|
关系操作符比较两个string
对象时采用了和(大小写敏感的)字典排序相同的策略;
- 如果两个
string
对象长度不同,且短的string
对象与长的string
对象的前面部分相匹配,则短的string
对象小于长的string
对象。 - 如果两个
string
对象的字符不同,则比较第一个不匹配的字符。则substr小于phrase,而slang则大于substr或phrase。1
2
3string substr="hello";
string phrase="hello world";
string slang="hiya";
4.string
对象的赋值
string
对象,可以把一个string
对象赋值给另一个string
对象:
1 |
|
5.两个string
对象相加
string
对象的加法被定义为连接(concatenation)。也就是说,两个(或多个)string
对象可以通过使用加操作符+
或者复合赋值操作符+=连接起来。给定两个string
对象:
1 |
|
6.和字符串字面值的连接
上面的字符对象s1
和s2
直接包含了标点符号。也可以通过将string
对象和字符串字面值混合得到同样的结果:
1 |
|
当进行string
对象和字符串字面值混合连接操作时,+
操作符的左右操作数必须至少有一个是string
类型的:
1 |
|
7.从string
对象获取字符string
类型通过下标操作符([]
)来访问string
对象中的单个字符。下标操作符需要取一个size_type
类型的值,来标明要访问的位置。这个下标中的值通常被称为 “下标” 或 “索引(index)”。
注解:
string
对象的下标从0开始。如果s
是一个string
对象且s
不空,则s[0]
就是字符串的第一个字符,s[1]
就表示第二个字符(如果有的话),而s[s.size()-1]
则表示s
的最后一个字符。
引用下标时如果超出下标作用范围就会引起溢出错误。
1 |
|
8.下标操作可用作左值
和变量一样,string
对象的下标操作返回值也是左值。因此,下标操作可以放于赋值操作符的左边或右边。通过下面循环把str
对象的每一个字符置为*
:
9.计算下标值
任何可产生整型值的表达式都可用作下标操作符的索引。
1 |
|
虽然任何整型数值都可作为索引,但索引的实际数据类型却是unsigned
类型string::size_type
。
建议:前面讲过,一个用
string::size_type
类型的变量接受size
函数的返回值。在定义用作索引的比哪里时,出于同样的道理,string
对象的索引变量最好也用string::size_type
类型。
在使用下标索引string
对象时,必须保证索引值 “在上下界范围内”。“在上下界范围内” 就是指索引值是一个赋值为size_type
类型的值,其取值范围在0
到string
对象长度减1之间。使用string::size_type
类型或其他unsigned
类型作为索引,就可以保证索引值不小于0
,只要索引值是unsigned
类型,就只需要检测它是否小于string
对象的长度。
注意:标准库不要去检查索引值,所用索引的下标越界是没有定义的,这样往往会导致严重的运行时错误。
string对象中字符的处理
适用于string
对象的字符(或其它char
值)。
这些函数都在cctype头文件中定义。
cctype定义的函数 | |
---|---|
isalnum(c) |
如果c是字母或数字,则为true。 |
isalpha(c) |
如果c是字母,则为true。 |
iscntrl(c) |
如果c是控制字符,则为true。 |
isdigit(c) |
如果c是数字,则为true。 |
isgraph(c) |
如果c不是空格,但可打印,则为true。 |
isprint(c) |
如果c是可打印的字符,则为true。 |
ispunct(c) |
如果c是标点符号,则为true。 |
isspace(c) |
如果c是空白字符,则为true。 |
isupper(c) |
如果c是大写字母,则为true。 |
isxdigit(c) |
如果c是十六进制数,则为true。 |
tolower(c) |
如果c是大写字母,则返回其小写字母形式,否则之间返回c。 |
toupper(c) |
如果c是小写字母,则返回其大写字母形式,否则之间返回c。 |
表中的大部分函数是测试一个给定的字符是否符号条件,并返回一个int
值作为真值。如果测试失败,则该函数返回0,否则返回一个(无意义的)非0值,表示被测字符符号条件。
表中的这些函数,可打印的字符是指那些可用显示表示的字符。空白字符则是空格、制表符、垂直制表符、回车符、换行符和进纸符的任意一种:标点符号则是除了数字、字母或(可打印的)空白字符(如空格)以外的其他可打印字符。
和返回真值的函数不同的是,tolower
和toupper
函数返回的是字符,返回实参字符本身或返回该字符相应的大小写字符。我们可用使用
标准库vector类型
vector
是同一种类型的对象的集合,每个对象都有一个对应的整数索引值。和string
对象一样,标准库将负责管理与存储元素相关的内存。我们把vector
称为容器,是因为它可以包含其他对象,一个容器中的所有对象都必须是同一种类型的。
使用vector
之前,必须包含相应的头文件。
1 |
|
vector
是一个类模板(class template)。使用模板可以编写一个类定义或函数定义,而用于多个不同的数据类型。因此,我们可以定义保存string
对象的vector
,或保存int
值的vector
,又或是保存自定义的类类型对象的vector。
声明从类模板产生的某种类型的对象,需要提供附加信息,信息的种类取决于模板。以vector
为例,必须说明vector
保存何种对象的类型,通过将类型放在类模板名称后面的尖括号中来指定类型:
1 |
|
和其他变量定义一样,定义vector
对象要指定类型和一个变量的列表。上面的第一个定义,类型是vector<int>
,该类型即是含有若干int
类型对象的vector
,变量名为ivec
。第二个定义的变量名是Sales_vec
,它保存的元素是Sales_item类型的对象。
注意:
vector
不是一种数据类型,而只是一个类模板,可用来定义任意多种数据类型。vector
类型的每一种都指定了其保存原始的类型。因此,vector<int>
和vector<string>
都是数据类型。
vector对象的定义和初始化
vector
类定义了好几种构造函数,用来定义和初始化vector
对象.
几种初始化 vector 对象的方式 | |
---|---|
vector<T> v1 ; |
vector 保存类型为T的对象,默认构造函数,v1为空 |
vector<T> v2(v1) ; |
v2 是 v1 的一个副本 |
vector<T> v3(n,i) ; |
v3 包含 n 个值为 i 的元素 |
vector<T> v4(n) ; |
v4 含有值初始化的元素的 n 个副本 |
1.创建确定个数的元素
若要创建非空的vector
对象,必须给出初始化元素的值。当把一个vector
对象复制到另一个vector
对象时,新复制的vector
中每一个元素都初始化为原vector
中相应元素的副本。但这两个vector
对象必须保存同一种元素类型:
1 |
|
可以用元素个数和元素值对vector
对象进行初始化。构造函数用元素个数来决定vector
对象保存元素的个数,元素值指定每个元素的初始值:
1 |
|
关键概念:vector对象动态增长
vector
对象(以及其他标准库容器对象)的重要属性就在于可以在运行时高效地添加元素。
注意:虽然可以对给定元素个数的
vector
对象预先分配内存,但更有效的方法是先初始化一个空vector
对象,然后再动态地增加元素。
2.值初始化
如果没有指定元素的初始化式,那么标准库将自行提供一个元素初始值进行值初始化(value initializationd)。这个由库生成的初始值将用来初始化容器中的每个元素,具体值为何,取决于存储在vector
中元素的数据类型。
如果vector
保存内置类型(如int
型)的元素,那么标准库将用0值创建元素初始化式:
1 |
|
如果vector
保存的是含有构造函数的类类型(如string
)的元素,标准库将用该类型的默认构造函数创建元素初始化式:
1 |
|
还有第三种可能性:元素类型可能是没有定义任何构造函数的类类型。这种情况下,标准库仍产生一个带初始值的对象,这个对象的每个成员进行了值初始化。
vector对象的操作
vector
标准库提供了许多类似于string
对象的操作,下表列出了几种最重要的vector
操作。
vector 操作 |
|
---|---|
v.empty() |
如果 v 为空,则返回 true,否则返回 false。 |
v.size() |
返回 v 中元素的个数。 |
v.push_back(t) |
在 v 的末尾增加一个值为 t 的元素。 |
v[n] |
返回 v 中位置为 n 的元素。 |
v1=v2 |
把 v1 的元素替换为 v2 中元素的副本。 |
v1==v2 |
如果 v1 与 v2 相等,则返回 false。 |
!=, < , <=, >, >= |
保持这些操作符惯有的含义。 |
1.vector
对象的size
empty
和size
操作类似于string
类型的相关操作。成员函数size
返回相应vector
类定义的size_type
的值。
注解:使用
size_type
类型时,必须指出该类型是在哪里定义的。vector
类型总是包括vector
的元素类型:vector<int>::size_type
2.向vector
添加元素
push_back()
操作接受一个元素值,并将它作为一个新的元素添加到vector
对象的后面,也就是 “插入(push)” 到vector
对象的 “后面(back)”:
1 |
|
3.vector
的下标操作
vector
中的对象是没有命名的,可以按vector
中对象的位置来访问它们。通常使用下标操作符来获取元素。vector
的下标操作类似于string
类型的下标操作。
vector
的下标操作符接受一个值,并返回vector
中该对应位置的元素。vector
元素的位置从 0 开始。
1 |
|
和string
类型的下标操作符一样,vector
下标操作的结果作为左值,因此可以像循环体中所做的那样实现写入。另外,和string
对象的下标操作类似,这里用size_type
类型作为vector
下标的类型。
4.下标操作不添加元素
初学C++的程序员可能会认为vector
的下标操作符可以添加元素,其实不然:
1 |
|
这里 ivec 是空的vector
对象,而且下标只能用于获取已存在的元素。
正确写法:
1 |
|
注意:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加到任何元素。
迭代器简介
除了使用下标来访问vector
对象的元素外,标准库还提供了另一种访问元素的方法:使用迭代器(iterator)。迭代器是一种检查容器内元素并遍历元素的数据类型。
标准库为每一种标准容器(包括vector
)定义了一种迭代器类型。迭代器类型提供了比下标操作更通用化的方法:所有的标准库容器都定义了相应的迭代器类型,而只有少数的容器支持下标操作。因为迭代器对所有的容器都适用,现代C++程序更倾向于适用迭代器而不是下标操作访问容器元素,即使对支持下标操作的vector
类型也是这样。
1.容器的 iterator类型
每种容器类型都定义了自己的迭代器类型,如vector
:
1 |
|
这条语句定义了一个名为 iter 的变量,它的数据类型是由vector<int>
定义的iterator
类型。每个标准库容器类型都定义了一个名为iterator
的成员,这里的iterator
与迭代器实际类型的含义相同。
2.begin和end操作
每种容器都定义了一对名为begin
和end
的函数,用于返回迭代器。如果容器中有元素的化,由begin
返回的迭代器指向第一个元素:
1 |
|
上述语句把 iter 初始化为名为begin
的vector
操作返回的值。假设vector
不空,初始化后,iter
即指该元素为ivec[0]
。
由end
操作返回的迭代器指向vector
的 “末端元素的下一个”。通常称为超出末端迭代器(off-the-end iterator),表明它指向了一个不存在的元素。如果vector
为空,begin
返回的迭代器与end
返回的迭代器相同。
注解:由
end
操作返回的迭代器并不指向vector
中任何实际的元素,相反,它只是起一个哨兵(sentinel)的作用,表示我们已处理完vector
中所有元素。
3.vector迭代器的自增和引用运算
迭代器类型定义了一些操作来获取迭代器所指向的元素,并允许程序员将迭代器从一个元素移动到另一个元素。
迭代器类型可使用解引用操作符(*
操作符)来访问迭代器所指向的元素:
1 |
|
解引用操作符返回迭代器当前所指向的元素。假设iter
指向vector
对象ivec
的第一个元素,那么*iter
和ivec[0]
就是指向同一个元素。上面这个语句的效果就是把这个元素的值赋为0;
迭代器使用自增操作符向前移动迭代器指向容器中下一个元素。从逻辑上说,迭代器的自增操作和int
型对象的自增操作类似。对int
对象来说,操作结果就是把int
型值 “加1”,而对迭代器对象则是把容器中的迭代器 “向前移动一个位置”。因此,如果iter
指向第一个元素,则++iter
指向第二个元素。
注解:由于
end
操作返回的迭代器不这些任何元素,因此不能对它进行解引用或自增操作。
4.迭代器的其他操作
另一对可执行于迭代器的操作就是比较:用==
或!=
操作符来比较两个迭代器,如果两个迭代器对象指向同一个元素,则它们相等,否则就不相等。
**5.迭代器应用的程序示例
假设已声明了一个vector<int>
型的ivec
变量,要把它所有元素值重置为0,可以用下标操作来完成:
1 |
|
用迭代器来编写循环:
1 |
|
for
循环首先定义了iter
,并将它初始化为指向itec
的第一个元素。for
循环的条件测试itec
是否于end
操作返回的迭代器不等。每次迭代iter
都自增1,这个for
循环的效果是从ivec
第一个元素开始,顺序处理vector
中的每一个元素。最后,iter
将指向ivec
中的最好一个元素,处理完最好一个元素后,iter
再增加1,就会与end
操作的返回值相等,在这种情况下,循环终止。
for
循环体内的语句用解引用操作符来访问当前元素的值。和下标操作符一样,解引用操作符的返回值是一个左值,因此可以对它进行赋值来改变它的值。上述循环的效果就是把ivec
中所有元素都赋值为0。
通过上述对代码的详细分析,可以看出这段程序与用下标操作符的版本达到相同的操作效果:从vector
的第一个元素开始,把vector
中每个元素都置为0.
6.const_iterator
前面的程序用vector::iterator
改变vector
中的元素值。每种容器类型还定义了一种名为const_iterator
的类型,该类型只能用于读取容器内元素,但不能改变其值。
当我们对普通iterator
类型解引用时,得到对某个元素的非const
引用。而如果我们对const_iterator
类型解引用时,则可以得到一个指向const
对象的引用,如同任何常量一样,该对象不能进行重写。
例如,如果 text 是vector<string>
类型,程序员想要遍历它,输出每个元素,可以这样编写程序:
1 |
|
迭代器的算术操作
除了一次移动迭代器的一个元素的增量操作符外,wector
迭代器(其他标准库容器迭代器很少)也支持其他的算术操作。这些操作称为迭代器算术操作(iterator arithmetic),包括:
iter+n
iter1-iter2
注意:任何改变
vector
长度的操作都会使已存在的迭代器失效。例如,在调用push_back
之后,就不能再信赖指向vector
的迭代器的值了。
标准库bitset类型
有些程序要处理二进制位的有序集,每个位困难包含0(关)值或1(开)值。位是用来保存一组项或条件的yes/no
信息(有时也称标志)的简洁方法。标准库提供的bitset
类简化了位集的处理。要使用bitset
类就必须包含相关的头文件。
1 |
|
bitset对象的定义和初始化
下标列出了bitset
的构造函数。类似于vector
,bitset
类是一种类模板;而与vector
不一样的是bitset
类型对象的区别仅在于其长度而不在其类型。在定义bitset
时,要明确bitset
含有多少位,须在尖括号内给出它的长度值:
1 |
|
给出的长度值必须是常量表达式。正如这里给出的,长度值必须定义为整型字面值常量或是已用常量值初始化的整型的const
对象。
这条语句把bitvec
定义为含有32个位的bitset
对象。喝vector
的元素一样,bitset
中的位是没有命名的,程序员只能按位置来访问它们。位集合的位置编码从0开始,因此,bitvec
的位序是从0到31。以0位开始的位串是低阶位(low-order bit),以31位结束的位串是高阶位(high-order bit)。
初始化bitset对象的方法 | |
---|---|
bitset<n> b; |
b有n位,每位都为0 |
bitset<n> b(u); |
b是unsigned long 型u的一个副本 |
bitset<n> b(s); |
b是string 对象s中含有的位串的副本 |
bitset<n> b(s,pos,n); |
b是s中从位置pos开始的n个位的副本 |
1.用unsigned
值初始化bitset
对象
当用unsigned long
值作为bitset
对象的初始值时,该值将转换为二进制的位模式。而bitset
对象中的位集作为这种位模式的副本。如果bitset
类型长度大于unsigned long
值的二进制位数,则其余的高阶位将置为0;如果bitset
类型长度小于unsigned long
值的二进制位数,则只使用unsigned
值中的低阶位,超过bitset
类型长度的高阶位将被丢弃。
在32位
2.用string
对象初始化bitset
对象
当用string
对象初始化bitset
对象时,string
对象直接表示为位模式。从string
对象读入位集的顺序是从右向左:
1 |
|
bitvec4的位模式中第2和3的位置为1,其余位置都为0。如果string
对象的字符个数小于bitset
类型的长度,则高阶位将置为0。
string
对象和bitset
对象之间是反向转化的:string
对象的最右字符(即下标最低的哪个字符)用来初始化bitset
对象的低阶位(即下标为0的位)。当用string
对象初始化bitset
对象时,记住这一差别很重要。
不一定要把整个string
对象都作为bitset
对象的初始值。相反,可以只用某个子串作为初始值:
1 |
|
bitset对象上的操作
bitset操作 | |
---|---|
b.any() |
b中是否存在置为1的二进制位? |
b.none() |
b中存在置为1的二进制位吗? |
b.count() |
b中置为1的二进制位的个数 |
b.size() |
b中二进制位的个数 |
b[pos] |
访问b中在pos处的二进制位 |
b.test(pos) |
b中在pos处的二进制位是否为1? |
b.set() |
把b中所有二进制位置为1 |
b.set(pos) |
把b中在pos处的二进制位置为1 |
b.reset() |
把b中所有二进制位都置为0 |
b.reset(pos) |
把b中在pos处的二进制位置为0 |
b.flip() |
把b中所有二进制位诸位取反 |
b.flip(pos) |
把b中在pos处的二进制位取反 |
b.to_ulong() |
用b中同样的二进制位返回一个unsigned long 值 |
os<<b |
把b中的位集输出到os流 |
1.测试整个bitset
对象
2.访问bitset
对象中的位
3.对整个bitset
对象进行设置
4.获取bitset
对象的值
5.输出二进制位
6.使用位操作符