引言
- 除了第2章介绍的内置类型之外,C++语言还定义了 -个内容丰富的抽象数据类型库。其中,string和 vector是两种最重耍的标准库类型,前者支持可变长字符串,后者则 表示可变长的集合。还有…种标准库类型是迭代器,它是string和vector的配套类型,常被用于访问string中的字符或vector中的元素。
- 内置数组是一种更基础的类型,string和vector都是对它的某种抽象。本章将分别介绍数组以及标准摩类型string和 vector.
- 第2 章介绍的内置类型是由C++语言直接定义的。这些类型,比如数字和字符,体现了大多数计算机硬件本身具备的能力。标准库定义了另外一组具有更高级性质的类型,它们尚未直接实现到计算机硬件中。
- 本章将介绍两种最重要的标准库类型:string和 vector。string表示可变长的 字符序列,vector存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类型,和其他内置类型一样,数组的实现与硬件密切相关。因此相较于标准库类型string和vector,数组在灵活性上稍显不足。 在开始介绍标准库类型之前,先来学习一种访问库中名字的简单方法
3.1命名空间的using声明
- 目前为止,我们用到的库函数基本上都属于命名空间std,而程序也显式地将这一点标示了出来。例如,std::cin表示从标准输入中读取内容。此处使用作用域操作符(::)(参见1.2节,第7页)的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。因此,std::cin的意思就是要使用命名空间std中的名字cin。上面的方法显得比较烦琐,然而幸运的是,通过更简单的途径也能使用到命名空间中的成员。本节将学习其中一种最安全的方法,也就是使用using声明(usingdeclaration).18.2.2节(第702页)会介绍另一种方法。
- 有了using声明就无须专门的前缀(形如命名空间::)也能使用所需的名字了。using声明具有如下的形式:
- using namespace::name; 一旦声明了上述语句,就可以直接访问命名空间中的名字
每个名字都需要独立的using声明
- 按照规定,每个using声明引入命名空间中的一个成员。例如,可以把要用到的标准库中的名字都以using声明的形式表示出来,重写1.2节 (第 5 页)的程序如下:
- 在上述程序中,一开始就有对cin、cout和 endl的 using声明,这意味着我们不用再 添加std::形式的前缀就能直接使用它们。C++语言的形式比较自由,因此既可以一行只放一条using声明语句,也可以一行放上多条。不过要注意,用到的每个名字都必须有自己的声明语句,而且每句话都得以分号结束。
头文件不应包含using声明
- 位于头文件的代码(参见2.6.3节,第67页)一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
一点注意事项
- 经本节所述,后面的所有例子将假设,但凡用到的标准库中的名字都已经使用using语句声明过了。例如,我们将在代码中直接使用cin,而不再使用std::cin?为了让书中的代码尽量简洁,今后将不会再把所有using声明和#include指令一一标出。附录A中的表A.1(第766页)列出了本书涉及的所有标准库中的名字及对应的头文件。
3.2标准库类型string
- 标准库类型string表示可变长的字符序列,使用string类型必须首先包含<~84~]string头文件。作为标准库的一部分,string定义在命名空间std中。接下来的示例都假定已包含了下述代码:
- #include <string> using std::string;
- 本节描述最常用的string操作,9.5节 (第320页)还将介绍另外一些。
3 .2 .1 定义和初始化string对象
- 如何初始化类的对象是由类本身决定的。一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。
表 3.1列出了初始化string对象最常用的一些方式,下面是几个例子:
- 可以通过默认的方式(参见2.2.1节,第 40页)初始化一个string对象,这样就会得到 —个空的string,也就是说,该string对象中没有任何字符。如果提供了一个字符串字面值(参见2.1.3节,第 36页),则该字面值中除了最后那个空字符外其他所有的字符都被拷贝到新创建的string对象中去。如果提供的是一个数字和一个字符,则string对象的内容是给定字符连续重复若干次后得到的序列
直接初始化和拷贝初始化
- 由2.2.1节(第39页)的学习可知,C++语言有几种不同的初始化方式,通过string我们可以清楚地看到在这些初始化方式之间到底有什么区别和联系。如果使用等号(=)初始化-个变量,实际上执行的是拷贝初始化(copyinitialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(directinitialization)。
- 等号使用拷贝初始化;无等号使用直接初始化
- 当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的S4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:
3.2.2string对象上的操作
- 一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义通过函数名调用的操作,就像Sales_item类的isbn函数那样(参见1.5.2节,第20页),也能定义<<、+等各种运算符在该类知象上的新含义。表3.2中列举了string的大多数操作。
读写string对象
- 第 1章曾经介绍过,使用标准库中的iostream来读写int, double等内置类型的 值。同样,也可以使用IO操作符读写string对象:
- cin<<s;//将string对象读入s,遇到空白停止
- cout<<s<<endl;//输出s
使用getline读取一整行
- 有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。和输入运算符一样,getline也会返回它的流参数。因此既然输入运算符能作为判断的条件(参见1.4.3节,第13页),我们也能用getline的结果作为条件。例如,可以通过改写之前的程序让它一次输出一整行,而不再是每行输出一个词了:
- 因为line中不包含换行符,所以我们手动地加上换行操作符。和往常一样,使用endl 结束当前行并刷新显示缓冲区。
- 触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符
string 的 empty 和 size 操作
- 顾名思义,empty函数根据string对象是否为空返回一个对应的布尔值(参见第2.1节,30页)。和Sales_item类(参见1.5.2节,第20页)的isbn成员一样,empty也是string的一个成员函数。调用该函数的方法很简单,只要使用点操作符指明是哪个对象执行了empty函数就可以了。
- 在上面的程序中,if语句的条件部分使用了逻辑非运算符(!),它返回与其运算对象相反的结果。此例中,如果str不为空则返回真。size函数返回string对象的长度(即string对象中字符的个数),可以使用size函数只输出长度超过80个字符的行:
string::size_type类型
- 对于size函数来说,返回一个int或者如前面2.1.1节(第31页)所述的那样返回-个unsigned似乎都是合情合理的。但其实size函数返回的是一个string::size_type类型的值,下面就对这种新的类型稍作解释。string类及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type即是其中的一种。在具体使用的时候,通过作用域操作符来表明名字size_type是在类string中定义的。尽管我们不太清楚string::size_type类型的细节,但有一点是肯定的:它是一个无符号类型的值(参见2.1.1节,第30员)而且能足够存放下任何string对象的大小。
- 所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
- 过去,string::size_type这种类型有点儿神秘,不太容易理解和使用。在C++11新标准中,允许编译器通过auto或者decltype(参见2.5.2节,第61页)来推断变量的类型:
- auto len=line.size();//len的类型是string::size_type
- 由于size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果(参见2.1.2节,第33页)。例如,假设n是一个具有负值的int,则表达式s.size()<n的判断结果几乎肯定是true。这是因为负值n会自动地转换成一个比较大的无符号值。
- 如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免丁中混用int和unsigned可能带来的问题
比较string对象
- string类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较string对象中的字符,并且对大小写敏感,也就是说,在比较时同一个字母的大写形式和小写形式是不同的。
- 相等性运算符(==和!=)分别检验两个string对象相等或不相等,string对象相等意味着它们的长度相同而且所包含的字符也全都相同。关系运算符<、<=、>、>=分别检验一个string对象是否小于、小于等于、大于、大于等于另外一个string对象。上述这些运算符都依照(大小写敏感的)字典顺序:
- 1.如果两个string对象的长度不同,而且较短string对象的每个字符都与较长string对象对应位置上的字符相同,就说较短string对象小于较长string对象。
- 2,如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一对相异字符比较的结果
两个string对象相加
- 两个string对象相加得到一个新的string对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。也就是说,对string对象使用加法运算符(+)的结果是一个新的string对象,它所包含的字符由两部分组成:前半部分是加号左侧string对象所含的字符、后半部分是加号右侧string对象所含的字符。另外,复合赋值运算符(+=)(参见1.4.1节,第10页)负责把右侧string对象的内容追加到左侧string对象的后面:
字面值和string对象相加
- 如2.1.2节(第33页)所讲的,即使一种类型并非所需,我们也可以使用它,不过前提是该种类型可以自动转换成所需的类型。因为标准库允许把字符字面值和字符串字面值(参见2.1.3节,第36页)转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。利用这一点将之前的程序改写为如下形式:
- string si = nhelloM , s2 = "world"; // 在 si 和 s2 中都没有标点符号
- string s3 = si + ", " + s2 + f \nf ;
- 当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加 法运算符( + ) 的两侧的运算对象至少有一个是string:
- 用双引号包含起来的叫做字符串字面值 / 字面值,等号两边至少一个需要 是string
- s4和 s5初始化时只用到了一个加法运算符,因此很容易判断是否合法。s6的初始化形式之前没有出现过,但其实它的工作机理和连续输入连续输出(参见1.2节,第 6 页)是一样的,可以用如下的形式分组:
- string s6 = (si + ", ") + "world";
- 其中子表达式sl + "/的结果是一个string对象,它同时作为第二个加法运算符的左侧运算对象,因此上述语句和下面的两个语句是等价的:
- 很容易看到,括号内的子表达式试图把两个字符串字面值加在一起,而编译器根本没法做到这一点,所以这条语句是错误的。
- 因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象切记,字符串字面值与string是不同的类型.
3.2.3处理string对象中的字符
- 我们经常需要单独处理string对象中的字符,比如检查一个string对象是否包含空白,或者把string对象中的字母改成小写,再或者查看某个特定的字符是否出现等。这类处理的一个关键问题是如何获取字符本身。有时需要处理string对象中的每一个字符,另外一些时候则只需处理某个特定的字符,还有些时候遇到某个条件处理就要停下来。以往的经验告诉我们,处理这些情况常常要涉及语言和库的很多方面。
- 另一个关键问题是要知道能改变某个字符的特性。在 cctype头文件中定义了一组标准库函数处理这部分工作,表 3.3列出了主要的函数名及其含义。
建议:使用C++版本的C标准库头文件
- C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname。也就是去掉了.h后缀,而在文件名mme之前添加了字母c,这里的c表示这是一个属于C语言标准库的头文件。因此,cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。一般来说,C++程序应该使用名cname的头文件而不使用name.h的形式,标准库中的名字总能在命名空间std中找到。如果使用.h形式的头文件,程序员就不得不时刻牢记哪些是从C语言那儿继承过来的,哪些又是C++语言所独有的。
使用范围for语句改变字符串中的字符
- 如果想要改变string对象中字符的值,必须把循环变量定义成引用类型(参见2.3.1节,第45页)。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。
- 新的例子不再是统计标点符号的个数了,假设我们想要把字符串改写为大写字母的形式。为了做到这一点可以使用标准库函数toupper,该函数接收一个字符,然后输出其对应的大写形式。这样,为了把整个string对象转换成大写,只要对其中的每个字符调用toupper函数并将结果再赋给原字符就可以了:
- 每次迭代时,变量c 引用string对象s 的下一个字符,赋值给c 也就是在改变s 中对应字符的值。因此当执行下面的语句时,
- c = toupper (c) ; // c 是一个引用,因此赋值语句将改变s 中字符的值
- 实际上改变了 c 绑定的字符的值。整个循环结束后,str中的所有字符都变成了大写形式。