《C++ Primer》第3章 字符串、向量和数组(上)
第2章介绍的内置类型是由C++语言直接定义的,体现了计算机硬件本身具备的能力。标准库定义了另外的更高级的类型,它们尚未实现到计算机硬件中。
本章将介绍两个重要的标准库类型:string
和vector
。
string:表示可变长的字符序列
vector:表示某种给定类型对象的可变长序列
本章还将介绍数组,它也是内置类型,其实现与硬件密切相关。
在这之前,先学习一下访问库中名字的简单方法:using
声明
3.1 命名空间的using声明
std::cin
中的::
叫作作用与操作符,其含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。即:在命名空间std
中寻找名字cin
。
使用using声明将使访问命名空间中名字变得更加简单。形式如下:
using namespace::name;
一旦声明上述语句,就可以直接访问命名空间中的名字:
#include <iostream>
using std::cin; //using声明,当我们使用名字cin时,从命名空间std中获取它
int main(){
int i;
cin >> i; //正确:cin和std::cin含义相同
cout << i; //错误:没有对应的using声明,必须用完整的名字
std::cout << i; //正确:显式地从std中使用cout
return 0;
}
每个名字都需要独立的using声明
按照规定,每个using声明引入命名空间中的一个成员。
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main(){
//这样就可以直接用cin、cout、endl了
}
头文件不应包含using声明
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中。
3.2 标准库类型string
标注库类型string表示可变长的字符序列,使用string类型必须包含string头文件。作为标准库一部分,string定义在命名空间std中。
3.2.1 定义和初始化string对象
初始化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")
string s4(n,'c'); //把s4初始化为连续n个字符c组成的串
直接初始化和拷贝初始化
使用等号(=)进行初始化,执行的是拷贝初始化。如果不用等号,则执行的是直接初始化。
初始值只有一个时,两种初始化方式都可以。
如果像上边的s4那样,初始化要用到多个值,则一般用直接初始化方式。
string s5 = "hiya"; //拷贝初始化
string s6("hiya"); //直接初始化
string s7(10,'c'); //直接初始化,s7的内容是cccccccccc
多个值进行初始化的情况,非要用拷贝初始化也可以,但需要显式地创建一个临时对象用于拷贝。
string s8 = string(10,'c'); //拷贝初始化,s8为cccccccccc
上边语句本质上等价于下面两条语句:
string temp(10,'c'); //temp内容为cccccccccc string s8 = temp; //将temp拷贝给s8
可以看到,这样做毫无优势,不推荐。
3.2.2 string对象上的操作
类既能定义通过成员函数名调用的操作,也能定义<<
、+
等各种运算符在该类对象上的新含义。
string的常用操作如下:
os<<s //将s写到输出流os中,返回os
is>>s //从is中读取字符串赋给s,字符串以空白分隔,返回is
getline(is,s) //从is读取一行赋给s,返回is
s.empty() //s为空时返回true,否则返回false
s.size() //返回s中字符个数
s[n] //返回s中第n个字符的引用,n从0计起
s1+s2 //返回两字符串连接的结果
s1=s2 //用s2的副本代替s1原来的内容
s1==s2 //判断s1和s2是否相等,若两串所含字符完全一样,则为相等
s1!=s2
<, <=, >, >= //按字符在字典中的顺序进行比较,大小写敏感
读写string对象
在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等),并从第一个真正的字符开始读起,直到遇见下一处空白为止。
string s;
cin >> s;
//如果输入的是“ Hello World ”,s中的内容将为“Hello”
string s1, s2;
cin >> s1 >> s2;
cout << s1 << s2;
//若输入" Hello World ",则输出将为"HelloWorld"
读写未知数量的string
用while循环语句
int main(){
string word;
while (cin >> word) //反复读取,直至文件末尾
cout << word <<endl; //逐个输出单词,每个单词后边跟一个换行
retun 0;
}
使用getline读取一整行
若要保留输入时的空白符,应该用**getline
**函数替代原来的>>
运算符。
getline函数:其参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存到那个string对象中(注意不存换行符)。getline函数返回值为它的输入流参数。
int main(){
string line;
//每次读入一整行,直至到达文件结尾
while (getline(cin,line))
cout << line << endl;
return 0;
}
注意
触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符
string的empty和size操作
empty函数根据string对象是否为空返回一个对应的布尔值。
size函数返回string对象的长度,即string对象中字符的个数
string::size_type类型
size函数的返回值是string::size_type类型的。
string类及其他大多数标准库类型都定义了几种配套的类型。
string::size_type
类型:它是一个无符号类型,且能足够存放下任何string对象的大小。
所有用于存放string类的size函数返回值的变量,都应该是string::size_type类型。
如果一个表达式中已经有了size()函数,就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。
比较string对象
按照字符在字典中的顺序进行比较,且大小写敏感。
- 如果两个string对象长度不同,而且较短string对象的每个字符都与较长string对象对应位置的字符相同,就说较短string对象小于较长string对象。
- 如果两个string对象在某些位置上不同,则比较结果为第一对相异字符的比较结果。
string str = "Hello";
string phrase = "Hello World";
string slang = "Hiya";
str小于phrase,slang既大于str也大于phrase。
为string对象赋值
允许把一个string对象的值赋给另一个string对象。
string str1(10,'c'), str2; //str1为cccccccccc,str2为空串
str1 = str2; //现在str1和str2都为空串
两个string对象相加
相加是把两个string对象串接起来,结果是一个新的string对象。
string s1 = "hello, ", s2 = "world\n";
string s3 = s1 + s2; //s3内容为hello, world\n
s1 += s2; //等价于s1 = s1 + s2;
字面值和string对象相加
标准库允许把字符字面值和字符串字面值转换成string对象。
值得注意的是:
当把string对象和字符字面值及字符串字面值混在一条语句中时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string
string s1 = "hello", s2 = "world";
string s3 = s1 + "," + "world"; //正确:每个加号都有一个运算对象是string
string s4 = "hello" + "," + s2; //错误:不能把字面值直接相加
为什么s3的初始化是正确的呢?第二个加号两边都是字符串字面值而没有string对象啊?
是这样的。s3的初始化语句可以用如下形式分组:
string s3 = (s1 + ",") + "world";
这样,第一个括号
(s1 + ",")
的结果是一个string对象,它同时作为第二个加法运算的左侧运算对象,所以第二个加号(+)的左侧运算对象是string类型,右侧是字符串字面值,所以这条语句是正确的。其实,上述语句和下面两个语句是等价的:
string temp = s1 + ","; string s3 = temp + "world";
切记,字符串字面值与string是不同的类型。
3.2.3 处理string对象中的字符
单独处理string对象中的字符,在cctype
头文件中定义了一组标准库函数来处理这些工作。
isalnum(c) //当c是字母或数字时为真
isalpha(c) //当c是字母时为真
iscntrl(c) //当c是控制字符时为真
isdigit(c) //当c是数字时为真
isgraph(c) //当c不是空格但可打印时为真
islower(c) //当c是小写字母时为真
isprint(c) //当c是可打印字符时为真(即c为空格或具有可视形式)
ispunct(c) //当c是标点符号时为真
isspace(c) //当c是空白时为真
isupper(c) //当c是大写字母时为真
isxdigit(c)//当c是十六进制数字时为真
tolower(c) //若c为大写字母,输出对应小写字母;否则原样输出c
toupper(c) //若c为小写字母,输出对应大写字母;否则原样输出c
建议使用C++版本的C标准库头文件
C++兼容了C语言的标准库。C语言的头文件形如
name.h
,那么对应到C++中就是cname
。在名为
cname
的头文件中定义的名字从属于命名空间std
,而定义在.h
的头文件中的则不然。故,C++程序应该是有名为
cname
的头文件而不是name.h
。
处理每个字符?使用基于范围的for语句
范围for:遍历给定序列中的每个元素并对序列中的每个值执行某种操作。
语法形式如下:
for (declaration: expression)
statement
其中,expression
部分是一个对象,用于表示一个序列。declaration
部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration
部分的变量会被初始化为expression
部分的下一个元素值。
string str("some string");
//每行输出str中的一个字符
for (auto c: str) //对于str中的每个字符
cout << c << endl; //输出当前字符,后面紧跟一个换行符
使用范围for语句改变字符串中的字符
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型。
string s("Hello World");
//转换成大写形式
for (auto &c: s) //对于s中的每个字符(注意:c是引用)
c = toupper(c); //c是一个引用,因此赋值语句将改变s中字符的值
cout << s << endl;
//程序将输出HELLO WORLD
只处理一部分字符?
要想访问string对象中的单个字符有两种方式:一种是使用下标,另外一种是迭代器。
下标运算符([ ]):它接受的输入参数是string::size_type
类型的值,这个参数表示要访问的字符的位置:返回值是该位置上字符的引用。
string对象的下标从0计起。
string对象的下标必须大于等于0而小于s.size()。
下标的值被称为下标或索引,任何表达式只要它的值是一个整型值就能作为索引。不过,如果某个索引是带符号类型的值,将自动转换成string::size_type
类型。
只要字符串不是常量,就能为下标运算符返回的字符赋新值。
使用下标执行迭代
//依次处理s中的字符直至处理完全部字符或遇到一个空白
for (decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]); //将当前字符改成大写形式
逻辑与运算符(&&):C++语言规定只有当左侧运算对象为真时才会去检查右侧运算对象的情况。
在此例中,这条规定就确保了只有当下标值在合理范围之内时,才会真的用此下标去访问字符串。
注意检查下标的合法性
使用下标时必须确保其在合理范围内。下边必须大于等于0且小于字符串的size()的值。一种简易方法是,总是设下标的类型为
string::size_type
,它是无符号类型,可以确保下标不会小于0。后边只需要确保下标小于size()的值就可以了。C++标准并不要求标准库检查下标合法性。一旦使用了超出范围的下标,后果不可预知。
3.3 标准库类型vector
标准库类型vector表示对象的集合,其中所有对象的类型都相同。
要使用vector
,必须包含头文件vector,作为标准库一部分,vector
定义在命名空间std
中。
C++既有类模板,也有函数模板,vector是一个类模板。
模板本身不是类或函数,可以将模板看做为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化。当使用模板时,需要指出编译器应把类或函数实例化为何种类型。
对于类模板,我们通过提供一些额外信息来指定模板到底实例化为什么什么样的类。提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。
vector
需要提供的额外信息是vector
内所存对象的类型。
vector<int> ivec; //ivec存放的是int类型的对象
vector<Sales_item> Sales_vec; //保存Sales_item类型的对象
vector<vector<string>> file; //file保存所存类型为string的vector对象
vector可以容纳绝大多数类型的对象作为其元素,但引用不是对象,所以不存在包含引用的vector
早期C++标准中如果vector的元素仍是vector(或其他模板类型),必须在外层vector的右尖括号和其元素类型之间添加一个空格。C++11则不必如此。
vector<vector<int> > file; //早期标准 vector<vector<int>> file; //C++11
但是某些编译器可能仍需按老式规定。
3.3.1 定义和初始化vector对象
初始化vector对象的方法:
vector<T> v1; //v1为空,其潜在元素类型为T,执行默认初始化
vector<T> v2(v1); //v2中包含有v1所有元素的副本
vector<T> v2 = v1; //等价于v2(v1)
vector<T> v3(n,val); //v3包含n个重复元素val
vector<T> v4(n); //等价于v4(n)
vector<T> v5{
a,b,c...}; //v5包含了初始值个数的元素,每个元素被赋予相应的初始值a,b,c...
vector<T> v5 = {
a,b,c...}; //等价于v5{a,b,c...}
- 程序在运行时可以很高效的往vector中添加元素,所以最常用的方式是先定义一个空vector,然后当获取到元素的值再逐一添加
- 用一个vector对象初始化另一个vector对象时,两个vector对象的类型必须相同。
列表初始化vector对象
C++语言的几种初始化方式(花括号初始化、圆括号初始化、拷贝初始化)大多数情况下可以相互等价使用,除以下三种例外:
-
拷贝初始化(即用=时),只能提供一个初始值。
-
类内初始值只能用拷贝初始化或花括号。
-
如果提供的是初始元素值的列表,则只能把初始值放在花括号进行列表初始化,而不能用圆括号。
vector<string> v1{ "a","an","the"}; //列表初始化 vector<string> v2("a","an","the"); //错误
创建指定数量的元素
vector<int> ivec(10,-1); //10个元素,每个都被初始化为-1
值初始化
可以只提供vector对象容纳的元素数量而略去初始值。此时将对所有元素执行默认初始化。
vector<int> ivec(10); //10个元素,每个都初始化为0
vector<string> svec(10);//10个元素,每个都是空串
但这种初始化方式有两个限制:
-
有些类要求必须明确提供初始值,对这种类型的对象而言,只提供元素数量而不提供初始值就无法完成初始化工作了。
-
如果只提供元素数量而不提供初始值,则只能使用直接初始化,不能使用拷贝初始化。
vector<int> vi = 10; //错误:必须使用直接初始化指定向量所含元素个数
列表初始值还是元素数量
有时候,初始化的真实含义依赖于用的圆括号还是花括号。
vector<int> v1(10); //v1有10个元素,都为0
vector<int> v2{
10}; //v2有一个元素,值为10
vector<int> v3(10,1); //v3有10个元素,都为1
vector<int> v4{
10,1}; //v4有两个元素,分别为10,1
- 用圆括号,提供的值是用来构造vector对象的。
- 用花括号,表示想列表初始化该vector对象。
如果用的圆括号,但括号内的值无法构造vector对象,将发生错误。
vector<string> v5("hi"); //错误:不能用字符串字面值构造vector对象
如果用的花括号,但括号内的值无法用来列表初始化vector对象,那就考虑用括号内的值来构造vector对象。
//不能用int类型的10来列表初始化string对象,所以用它来构造vector对象
vector<string> v6{
10}; //v6有10个string对象,均为空串
vector<string> v7{
10,"hi"}; //v7有10个string对象,均为hi
要想用列表初始化vector对象,花括号里的值必须与元素类型相同。
3.3.2 向vector对象中添加元素
更多的时候,先创建一个空vector,然后在运行时利用vector的成员函数push_back
向其中添加元素。
push_back负责把一个值当成vector对象的尾元素压到vector对象的尾端。
vector<int> v2; //空vector对象
for (int i = 0; i!= 100; ++i)
v2.push_back(i); //依次把整数值放到尾端
//循环结束后,v2有100个元素,从0到99
关键概念:vector对象能高效增长
一旦元素的值有所不同,我们推荐,先定义一个空vector,在运行时再向其中添加值。
向vector对象添加元素蕴含的编程假定
能高效便捷地向vector中添加元素,这种简便性也伴随着更高的编程要求,其中一条就是必须确保所写循环正确无误,特别是循环有可能改变vector对象容量的时候。
vector有一些隐含要求,其中一条就是:
如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
范围for语句体内不应改变其所遍历序列的大小。
3.3.3 其他vector操作
v.empty() //如果v中不含任何元素,则返回真;否则返回假
v.size() //返回v中元素个数
v.push_back(t) //向尾端添加一个元素,值为t
v[n] //返回v中第n个位置上元素的引用,n从0计起
v1 = v2 //用v2中元素的拷贝替换v1中的元素
v1 = {
a,b,c...} //用列表中元素的拷贝替换v1中的元素
v1 == v2 //v1和v2相等当且仅当它们元素数量相同且对于位置元素相同
v1 != v2
<, <=, >, >= //按字典顺序比较
v.size()
的返回值类型是由vector定义的size_type类型。要使用size_type,需要首先指定它是由哪种类型定义的。vector对象的类型总包含着元素的类型。
vector<int>::size_type //正确 vector::size_type //错误
计算vector内对象的索引
下标的类型是相应的size_type类型。
可以通过计算得到vector内对象的索引。
//以10分为一个分数段统计成绩的数量:0-9,10-19,...,90-99,100
vector<unsigned> scores(11,0); //11个分数段,全初始化为0
unsigned grade;
while (cin >> grade){
//读取成绩
if (grade <= 100) //只处理有效成绩
++scores[grade/100]; //将对应分数段的计数加1
}
不能使用下标形式添加元素
vector对象(以及string对象)的下标运算符可以用于访问已存在的元素,而不能用于添加元素。
关于下标必须明确一点:
只能对确知已存在的元素执行下标操作。
用下标访问不存在的元素将导致缓冲区溢出。
确保下标合法的一种有效手段:尽可能使用范围for语句。