欢迎阅读我的 【C++Primer】专栏
专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 3.2标准库类型string
-
- 定义和初始化string对象
- 直接初始化和拷贝初始化
- string对象上的操作
- 读写string对象
- 读取未知数量的string对象
- string的empty和size操作
- string::size_type类型
- 比较string对象
- 为string对象赋值
- 两个string对象相加
- 字面值和string对象相加
- 处理string对象中的字符
- 处理每个字符?使用基于范围的for语句
- 使用范围for语句改变字符串中的字符
- 使用下标执行随机访问
3.2标准库类型string
标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。接下来的示例
都假定已包含了下述代码:
include < string >
using std::string;
本节描述最常用的string操作。C++标准一方面对库类型所提供的操作做了详细规定,另一方面也对库的实现做出一些性能上的需求。因此,标准库类型对于一般应用场合来说有足够的效率。
定义和初始化string对象
如何初始化类的对象是由类本身决定的。一个类可以定义很多种初始化对象的方式,只不过这些方式之间必须有所区别:或者是初始值的数量不同,或者是初始值的类型不同。下面是几个例子:
cpp
string s1; //默认初始化,s1是一个空字符串
string s2 = s1; //s2是s1的副本
string s3("value"); //s3是该字符串字面值的副本
string s4(10,'c');//s4的内容是cccccccccc
可以通过默认的方式初始化一个string对象,这样就会得到一个守的string,也就是说,该string对象中没有任何字符。如果提供了一个字符串字面值,则该字面值中除了最后那个空字符外其他所有的字符都被拷贝到新创建的string对象中去。如果提供的是一个数字和一个字符,则string对象的内容是给定字符连续重复若干次后得到的序列。
表3.1:初始化string对象的方式
string s1 默认初始化,s1是一个空串
string s2(s1) s2是s1的副本
string s2 = s1 等价于s2(s1),s2是s1的副本
string s3("value") s3是宇面值"value"的副木,除了字面值最后的那个空字符外
string s3="value" 等价于s3("value"),s3是字面值"value"的副本
string s4(n,'c') 把s4初始化为由连续n个字符c组成的串
直接初始化和拷贝初始化
C++语言有几种不同的初始化方式,通过string我们可以清楚地看到在这些初始化方式之间到底有什么区别和联系。如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)。
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s4那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:
cpp
string s5="hiyan"; // 拷贝初始化
string s6("hiya"); //直接初始化
string s7(10,"c"); //直接初始化,s7的内容是cccccccccc
对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝:
cpp
string s8 = string(10,'c'); //拷贝初始化,s8的内容是cccccccccc
s8的初始值是string(10,'c'),它实际上是用数字10和字符c两个参数创建出来的一个string对象,然后这个string对象又拷贝给了s8。这条语句本质上等价于下面的两条语句:
cpp
string temp(10,'c'); //temp的肉容是cccccccccc
string s8=temp; //将temp指贝给s8
其实我们可以看到,尽管初始化s8的语句合法,但和初始化s7的方式比较起来可读性较差,也没有任何补偿优势。
string对象上的操作
一个类除了要规定初始化其对象的方式外,还要定义对象上所能执行的操作。其中,类既能定义通过函数名调用的操作,就像Sales_item类的isbn函数那样,也能定义<<、+等各种运算符在该类对象上的新含义.表3.2中列举了string的大多数操作。
表3.2:string的操作
os<<s 将s写到输出流os当中,返回os
is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,8) 从is中读取一行赋给s,返回is
s.empty() s为空返回true,否则返回false
s.size() 返回s中字符的个数
s[n] 返回s中第n个字符的引用,位置n从0计起
s1+s2 返回s1和s2连接后的结果
s1=s2 用s2的副本代替s1中原来的字符
s1 == s2 如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
<,<=,>,>= 利用字符在字典中的顺序进行比较,且对字母的大小写敏感
读写string对象
使用标准库中的iostream来读写int、double等内置类型的值。同样,也可以使用IO操作符读写stritng对象:
cpp
//注意:要想编译下面的代码还需要适当的#include语句和using声明
int main()
{
string s;//空字符串
cin >> s;//将string对象读入s,遇到空白停止
cout << s<<endl;//输出s
return 0;
}
这段程序首先定义一个名为s的空string,然后将标准输入的内容读取到s中。在执行读取操作时,string对象会自动忽略开头的空白〔即空格符、换行符、制表符等并从第一个真正的字符开始读起,直到遇见下一处空白为止。
如上所述,如果程序的输入是" HelloWor1ld!"(注意开头和结尾处的空格),则输出将是"Hello",输出结果中没有任何空格。
和内置类型的输入输出操作一样,string对象的此类操作也是返回运算符左侧的运算对象作为其结果。因此,多个输入或者多个输出可以连写在一起:
cpp
string s1,s2;
cin >> s1 >> s2; //把第一个输入读到s1中,第二个输入读到s2中
cout << sl << s2 << endl;//输出两个string对象
假设给上面这段程序输入与之前一样的内容"hello world!",输出将是 "HelloWorld"。
读取未知数量的string对象
下面编写程序用于读取string对象:
cpp
int main()
{
string word;
while(cin >> word) //反复读取,直至到达文件末尾
cout << word << endl; //逐个输出单词,每个单词后面絮跟一个换行
return 0;
}
在该程序中,读取的对象是string而非int,但是while语句的条件部分和之前版本的程序是一样的。该条件负责在读取时检测流的情况,如果流有效,也就是说没遇到文件结束标记或非法,那么执行while语句内部的操作。此时,循环佛将输出刚刚从标准输入读取的内容。重复若干次之后,一旦遇到文件结束标记或非法输入循环也就结束了。
使用getline读取一整行
有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。getline只要一遇到换
行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此。如果输入真的一开始就是换行符,那么所得的结果是个空string。
和输入运算符一样,getline也会返回它的流参数。因此既然输入运算符能作为判断的条件,我们也能用getline的结果作为条件。例如,可以通过改写之前的程序让它一次输出一整行,而不再是每行输出一个词了:
cpp
int main()
{
string line;
//每次读入一整行,直至到达文件未尾
while(getline(cin,1ine))
cout << line << endl;
return 0;
}
因为 line中不包含换行符,所以我们手动地加上换行操作符。和往常一样,使用 endl 结束当前行并刷新显示缓冲区。
触发getline函数返回的那个换行符实际上被丢弃了,得到的string对象中并不包含该换行符。
string的empty和size操作
顾名思义,empty函数根据string对象是否为宇返回一个对应的布尔值。和Sales_item类的isbn成员一样,empty也是string的一个成员函数。调用该函数的方法很简单,只要使用点操作符指明是哪个对象执行了empty函数就可以了。
通过改写之前的程序,可以做到只输出非空的行:
cpp
//每次读入一整行,遇到空行直接跳过
while(getline(cin,1ine))
if(line.empty())
cout << line << endl;
在上面的程序中,if语句的条件部分使用了逻辑非运算符(!),它返回与其运算对象相反的结果。此例中,如果size不为空则返回真。
size函数返回string对象的长度(即string对象中字符的个数),可以使用size函数只输出长度超过80个字符的行:
cpp
string line;
//每次读入一整行,输出其中超过80个字符的行
while(getline(ctn,line))
if(line.size()>80)
cout<<line<<endl;
string::size_type类型
对于size函数来说,返回一个int是合情合理的。但其实size函数返回的是一个string::size_type类型的值,下面就对这种新的类型作解释。
string类及其他大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性,类型size_type即是其中的一种。在具体使用的时候,通过作用域操作符来表明名字size_type是在类string中定义的。
尽管我们不太清楚string::size_type类型的细节,但有一点是肯定的:它是一个无符号类型的值而且能足够存放下任何string对象的大小。所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型的。
过去,string::size_type这种类型有点儿神秘,不太容易理解和使用。在C++11新标准中,允许编译器通过auto或者decltype来推断变量的类型:
cpp
auto len=line.size(); //len的类型是string::size_type
由于size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设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对象比较的一个示例:
cpp
string str="Hello";
string phrase="HelloWorld";
string slang="Hiya";
根据规则1可判断,对象str小于对象phrase:根据规则2可判断,对象slang既大于str也大于phrase。
为string对象赋值
一般来说,在设计标准库类型时都力求在易用性上向内置类型看齐,因此大多数库类型都支持赋值操作。对于string类而言,允许把一个对象的值赋给另外一个对象:
cpp
string st1(10,'c'),st2;//st1的内容是cccccccccc;st2是一个空字符串
st1 = st2; //赋值:用st2的副本替换st1的内容
//此时st1和st2都是空字符串
两个string对象相加
两个string对象相加得到一个新的string对象,其内容是把左侧的运算对象与右侧的运算对象串接而成。也就是说,对string对象使用加法运算符(+)的结果是一个新的string对象,它所包含的字符由两部分组成:前半部分是加号左侧string对象所含的字符、后半部分是加号右侧string对象所含的字符。另外,复合赋值运算符(+=)负责把右侧string对象的内容追加到左侧string对象的后面:
cpp
string s1 = "hello", s2="world"; // 在s1 和 s2 中都没有标点符号
string s3 = s1 + ", " + s2 + '\n';
string s3 = s1 + s2; //s3的内容是hello,world
s1 += s2;//等价于s1=s1+s2
字面值和string对象相加
即使一种类型并非所需,我们也可以使用它,不过前提是该种类型可以自动转换成所需的类型。因为标准库允许把字符字面值和字符串字面值转换成string对象,所以在需要string对象的地方就可以使用这两种字面值来替代。利用这一点将之前的程序改写为如下形式:
cpp
string s1 = "hello", s2 = "world"; //在s1和s2中都没有标点符号
string s3 = s1+ "," + s2 + '\n';
当把string对象和孙符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string:
cpp
string s4 = s1 + ","; //正确:把一个string对象和一个字面值相加<20
string s5 = "hello"+","; //错误:两个运算对象都不是string
//正确;每个加法远算符都有一个运算对象是string
string s6 = s1 + "," + "world";
string s7 = "hello"+","+s2; //错误:不能把字面值直接相加
s4和s5初始化时只用到了一个加法运算符,因此很容易判断是否合法。s6的初始化形式之前没有出现过,但其实它的工作机理和连续输入连续输出是一样的,可以用如下的形式分组:
cpp
string s6=(s1+",") + "world";
其中子表达式s1+","的结果是一个string对象,它同时作为第二个加法运算符的左侧运算对象,因此上述语句和下面的两个语句是等价的:
cpp
string tmp = s1+",";//正确:加法运算符有一个运算对象是string
s6 = tmp+"world"; //正确;加法运算符有一个运算对象是string
另一方面,s7的初始化是非法的,根据其语义加上括号后就成了下面的形式:
cpp
strings7=("hello"+",")+S2;//错误:不能把字面值直接相加
很容易看到,括号内的子表达式试图把两个字符串字面值加在一起,而编详器根本没法做到这一点,所以这条语句是错误的。
因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型
处理string对象中的字符
我们经常需要单独处理string对象中的字符,比如检查一个string对象是否包含空白,或者把string对象中的字母改成小写,再或者查看某个特定的字符是否出现等。
这类处理的一个关键问题是如何获取字符本身。有时需要处理string对象中的每一个字符,另外一些时候则只需处理某个特定的字符,还有些时候遇到某个条件处理就要停下来。以往的经验告诉我们,处理这些情况常常要涉及语言和库的很多方面。
另一个关键问题是要知道能改变某个字符的特性。在cctype头文件中定义了一组标准库函数处理这部分工作,表3.3列出了主要的函数名及其含义。
表3.3:cctype头文件中的函数
isalnum© | 当c是字母或数字时为真 |
isalpha© | 当c是孙母时为真 |
iscntz1© | 当c是控制守符时为真 |
tsdigit© | 当c是数字时为真 |
isgraph© | 当c不是空格但可打印时为真 |
islower© | 当c是小写字母时为真 |
isprint© | 当c是可打印字符时为真〔即c是空格或c具有可视形式) |
ispunct© | 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空自中的-种) |
isspace© | 当c是空自时为真(即e是空格、横向制表符、纵向制表符、回车符、换行符、迹纸符中的一种) |
isupper© | 当c是大写字母时为真 |
isxdigit© | 当c是十六进制数字时为真 |
tolower© | 如果c是大写字母,输出对应的小写字母;否则原样输出c |
toupper© | 如果c是小写字母,输出对应的大写字母;否则原样输出c |
建议:使用C++版本的C标准库头文件
C++标准库中除了定义C++语言特有的功能外,也兼容了C语言的标准库。C语言的头文件形如name.h,C++则将这些文件命名为cname。也就是去除了.h后缎,而在文件名name之前添加了字母c,这里的c表示这是一个属于C语言标准库的头文件。
因此。cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。
一般来说,C++程序应该使用名为ename的头文件而不使用name.h的形式,标准库中的名字总能在命名空间std中找到。如果使用.h形式的头文件,程序员就不得不时刻牢记哪些是从C语言那儿继承过来的,哪些又是C++语言所独有的。
处理每个字符?使用基于范围的for语句
如果想对string对象中的每个字符做点儿什么操作,目前最好的办法是使用C++11新标准提供的一种语句:范围for(range for)语句。这种语句遍历给定序列中的每个元素并对序列中的每个值执行树种操作,其语法形式是:
for(declaration : expyession)
stalenzen
其中,expression部分是一个对象,用于表示一个序列。declaration部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,deciaration部分的变量会被初始化为expression部分的下一个元素值。
一个string对象表示一个字符的序列,因此string对象可以作为范围for语句其中的expression部分。举一个简单的例子,我们可以使用范围for语句把string对象中的字符每行一个输出出来:
cpp
string str("somestring")
//每行输出str中的一个字符
for(auto c:str) //对于str中的每个字符
cout << c << endl; //输出当前字符,后面紧跟一个换行符
for循环把变量c和str联系了起来,其中我们定义循环控制变量的方式与定义任意一个普通变量是一样的。此例中,通过使用auto关键字让编译器来决定变量c的类型,这里c的类型是char。每次迭代,str的下一个字符被拷贝给c,因此该循环可以读作"对于字符串str中的每个字符c,"执行某某操作。此例中的"某某操作"即输出一个字符,然后换行。
举个稍微复杂一点的例子,使用范围for语句和ispunct函数来统计string对象中标点符号的个数:
cpp
string s("HelloWorld!!");
//punct_cnt的类型和s.size的返回类型一样
decltype(s.size())punct_cnt=0;
//统计s中标点符号的数量
for(auto c:s) // 对于s中的每一个字符
if(itspunct(e)) // 如果该字符是标焯符号
++punct_cnt; // 将标点符号的计数值加1
cout<<punct_cnt<<"punctuation characters in" << s << endl;
程序的输出结果将是:
3 punctuation characters in Hello World!!
这里我们使用dec1type关键字声明计数变量punctcnt,它的类型是s.size函数返回值的类型,也就是string::size_type。使用范围for语句处理string对象中的每个字符并检查其是否是标点符号。如果是,使用递增运算符给计数变量加1。最后,待范围for语句结束后输出统计结果。
使用范围for语句改变字符串中的字符
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。
新的例子不再是统计标点符号的个数了,假设我们想要把字符串改写为大写字母的形式。为了做到这一点可以使用标准库函数toupper,该函数接收一个字符,然后输出其对应的大写形式。这样,为了把整个string对象转换成大写,只要对其中的每个字符调用toupper函数并将结果再赋给原字符就可以了:
cpp
strings("Hello World!!!");
//转换成大写形式
for(auto &c:8)//对于s中的每个字符(注意:c是引用)
c=toupper(c);//c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
上述代码的输出结果将是:
Hello World!!!
每次迭代时,变量c引用string对象s的下一个字符,赋值给c也就是在改变s中对应字符的值。因此当执行下面的语句时,
cpp
c= toupper(c);//c是一个引用,因此赋值语句将改变s中字符的值
实际上改变了c绑定的字符的值。整个循环结束后,str中的所有字符都变成了大写形式。
只处理一部分字符?
如果要处理string对象中的每一个字符,使用范围for语句是个好主意。然而,有时我们需要访问的只是其中一个字符,或者访问多个字符但遇到某个条件就要停下来。例如,同样是将字符改为大写形式,不过新的要求不再是对整个字符串都这样做,而仅仅他把string对象中的第一个字母或第一个单词大写化。
要想访问string对象中的单个字符有两种方式:一种是使用下标,另外一种是使用迭代器。
下标运算符([])接收的输入参数是string::size_type类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。
string对象的下标从0计起。如果string对象s至少包含两个字符,则s[0]是第1个字符、s[1]是第2个字符、s[s.size()-1]是最后一个字符。
string对象的下标必须大于等于0而小于s.size()。
使用超出此范围的下标将引发不可预知的结果,以此推断,使用下标访问>空string也会引发不可预知的结果。
下标的值称作"下标"或"索引",任何表达式只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值将自动转换成由string::size_type,表达的无符号类型。
下面的程序使用下标运算符输出string对象中的第一个字符:
cpp
if(!is.empty())//确保确实有字符需要输出
cout << s[0] <<endl; //输出s的第一个字符
在访问指定字符之前,首先检查s是否为空。其实不管什么时候只要对string对象使用了下标,都要确认在那个位置上确实有值。如果s为空,则s[0]的结果将是未定义的。
只要字符串不是常量,就能为下标运算符返回的字符赋新值。例如,下面的程序将字符串的首字符改成了大写形式:
cpp
string s("some string")
if(!s.empty())//确保s[0]的位置确实有字符
s[0] = toupper(s[0]); // 为s的第一个字符赋一个新的值
程序的输出结果将是:
Some string
使用下标执行迭代
另一个例子是把s的第一个词改成大写形式:
cpp
//依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
for(decltype(s.size()) index=0
index!=s.size()&&!isspace(s[index]);++index)
s[index]=toupper(s[index]);//将当前字符政成大写形式
程序的输出结果将是:
SOME string
在上述程序中,for循环使用变量index作为s的下标,index的类型是由decl1type关键字决定的。首先把index初始化为0,这样第一次迭代就会从s的首字符开始,之后每次迭代将index加1以得到s的下一个字符。循环体负责将当前的字母改写为大写形式。
for语句的条件部分涉及一点新知识,该条件使用了逻辑与运算符(&&)。如果参与运算的两个运算对象都为真,则逻辑与结果为真;否则结果为假。对这个运算符来说最重要的一点是,C++语言规定只有当左侧运算对象为真时才会检查右侧运算对象的情况。如此例所示,这条规定确保了只有当下标取值在合理范围之内时才会真的用此下标去访问字符串。也就是说,只有在index达到s.size()之前才会执行s[index]。随着index的增加,它永远也不可能超过s.size()的值,所以可以确保index比s.size()小。
提示:注意检查下标的合法性
使用下标必须确保其在合理的范围内,也就是说下标必须,大于等于0,而小于字符串的size()的值。一种简便易行的方法是,总是设下标的类型为string::size_type,因为此类型是无符号数,因为此类型是无符号数,可以确保下标不会小于0。此时,代码只需保证下标小于size()的值就史以了。
C++标准并不要求标准库检测下标是否合法。一旦使用了超出范围的下标,就会产生不可预知的结果。
使用下标执行随机访问
在之前的示例中,我们让字符串的下标每次加1从而按顺序把所有字符改写成了大写形式。其实也能通过计算得到某个下标值,然后直接获取对应位置的字符,并不是每次都得从前往后依次访问。
例如,想要编写一个程序把0到15之间的十进制数转换成对应的十六进制形式,只需初始化一个字符串令其存放16个十六进制"数字":
cpp
const string hexdigits = "0123456789RBCDEF";//可能的十六进制数字
cout << "Enter a series of numbers between 0 and 15"
<<" separated by spaces. Hit ENTER when finished:"
<<endl;
string result; //用于保存十六进制的字符串
string::size_type n; //用于保存从输入流读取的数
while(cin >> n)
if(n<hexdigits.size()) //忽略无效输入
result+= hexdigits[n];//得到对应的十六进制数字
cout<<"Your hex number is:"<<result<<endl;
假设输入的内容如下:
12 0 5 15 8 15
程序的输出结果将是:
Your hex number is: C05F8F
上述程序的执行过程是这样的:首先初始化变量hexdigits后其存放从0到F的十六进制数字,注意我们把hexdigits声明成了常量,这是因为在后面的程序中不打算再改变它的值。在循环内部使用输入值n作为hexdigits的下标, hexdigits[n]的值就是hexdigits内位置n处的字符。例如,如果n是15,则结果是F:如果n是12,则结果是C,以此类推。把得到的十六进制数字添加到result内,最后一并输出。
无论何时用到字符串的下标,都应该注意检查其合法性。在上面的程序中,下标n是string::size_type类型,也就是无符号类型,所以n可以确保大于或等于0。在实际使用时,还需检查n是否小于hexdigits的长度。