第三章 字符串、向量和数组
第二章介绍的内置类型是由C++语言直接定义的。这些类型体现了大多数计算机硬件本身具备的能力。标准库定义了另外一组具有更高级性质的类型,他们尚未直接实现到计算机硬件中。
本章介绍两种最重要的标准库类型。string
和vector
。string
表示可变长的字符序列,vector
存放的是某种给定类型对象的可变长序列。本章还介绍内置数组类型,和其他内置类型一样,数组的实现与硬件密切相关。因此相比较标准库类型string
和vector
,数组在灵活性上稍显不足。
3.1 命名空间的using声/明
作用操作符::
的含义是:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。
例如,std::cin
的意思是 要使用命名空间std
中的名字cin
。
上面使用命名空间中的成员方法显得比较烦琐,下面介绍其中一种最安全的方法(第十八章介绍另一种方法):使用 using
声明,就无须再通过专门的前缀(形如命名空间::
)去获取所需的名字了。
using
声明具有如下的形式:using namespace::name
。
一旦声明了上述语句,就可以直接访问命名空间中的名字。示例:
#include <iostream>
// using 声明,当使用名字cin时,从命名空间std中获取它
using std::cin;
int main()
{
int i;
cin >> i; // 正确:cin和std::cin含义相同
cout << i; // 错误:没有对应的using声明,必须使用完整的名字
std::cout << i; // 正确:显式地从std中使用cout
system("pause");
return 0 ;
}
每个名字都需要独立的using声明
程序中使用的每个名字都需用独立的 using
声明引入,或者需要引入后面学到的 using namespace std;
。
头文件不应包含using声明
头文件中通常不应该包含 using
声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using
声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说, 由于不经意间包含了一些名字, 反而可能产生始料未及的名字冲突。
3.2 标准库类型string
标准库类型 string
表示可变长的字符序列,使用string
类型必须首先包含 string头文件。
接下来的示例都假定已包含下述代码:
#include <string>
using std::string;
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”),s3是字面值“value”的副本 |
string s4(n, ‘c’) | 把s4初始化为由连续 n 个字符 c 组成的串 |
string s1; // 默认初始化,s1是一个空字符串
string s2 = s1; // s2是s1的副本
string s3 = "hiya"; // s3是该字符串字面值的副本(不包括空字符' ')
string s4(10, 'c') // s4的内容是 cccccccccc
直接初始化和拷贝初始化
如果 使用等号(=
) 初始化一个变量,实际上执行的是 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。反之,如果 不使用等号,则执行的是 直接初始化(direct initialization)。
string s5 = "hiya"; // 拷贝初始化
string s6("hiya") ; // 直接初始化
string s7(10, 'c'); // 直接初始化,s7的内容是cccccccccc
-
当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像前面的 s4 那样初始化要用到的值有多个,一般来说只能使用直接初始化。
-
对于多个值进行初始化的情况,用拷贝初始化也不是不可以,不过要显式地创建一个(临时)对象用于拷贝:
string s8 = string(10, 'c'); //拷贝初始化,s8的内容是cccccccccc
s8 的初始值是
string(10, 'c')
,实际上是用数字10和字符c两个参数创建出来的一个string对象,然后这个string对象又拷贝给了s8。这条语句本质上等价于下面的两条语句:
string temp(10, 'c'); // temp的内容是cccccccccc string s8 = temp; // 将temp拷贝给s8
(尽管初始化 s8 的语句合法,但和初始化 s7 的方式比较起来可读性差,也没有任何补偿优势。)
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从o计起 |
s1+s2 | 返回s1和s2连接后的结果 |
s1=s2 | 用s2的副本代替s1中原来的字符 |
s1==s2 | 如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感 |
s1!=s2 | |
<,<=,>,>= | 利用字符在字典中的顺序进行比较,且对字母的大小写敏感 |
读写string对象
在执行读取操作时,string
对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读取,直到遇见下一处空白为止。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cin >> s;
cout << s << endl;
system("pause");
return 0;
}
如果程序的输入是“ Hello World! ”(注意开头结尾处的空格),则输出将是“Hello”,输出结果中没有任何空格。
和内置类型的输入输出操作一样,string
对象的此类操作也是返回运算符左侧的运算对象作为其结果。因此,多个输入或输出可以连在一起写(遇见空格符、换行符、制表符看做是当前对象结束):
string s1, s2;
cin >> s1 >> s2; // 把第一个输入读到s1中,第二个输入读到s2中
cout << s1 << s2 << endl; // 输出两个string对象
假设程序的输入还是“ Hello World! ”,则输出将是HelloWorld!
。
读取未知数量的string对象
int main()
{
string word;
while (cin >> word) // 反复读取,直至到达文件末尾
cout << word << endl; // 逐个输出单词,每个单词后面紧跟一个换行符
}
在该程序中,读取的对象是string
,while
语句的条件负责在读取时检查流的情况,如果流有效(没遇到文件结束标志或非法输入),那么执行 while 语句内部的操作。此时,循环体将输出刚刚从标准输入读取的内容。
使用getline读取一整行
当希望能在输出的字符串中 保留输入的空白字符 时,可以使用 getline
函数。
getline
函数的参数是一个输入流和一个string
对象,getline
从给定的输入流中读入内容,当遇到换行符时就结束读取(换行符也被读进来了)并把所读的内容存入到那个string
对象中(注意不存换行符),然后返回结果。如果输入的开始就是一个换行符,则得到空 string
。
示例,改写上面的程序让它一次输出一整行,而不是每次输出一个词:
int main()
{
string line;
// 每次读入一整行,直至到达文件末尾
while(getline(cin, line))
cout << line <<endl; // 因为line不包含换行符,所以手动加上换行操作符
return 0;
}
触发
getline
函数返回的那个换行符实际上被丢弃掉了,得到的string
对象中并不包含该换行符。
string 的empty和size操作
empty
函数根据 string
对象是否为空返问一个对应的布尔值;
size
函数返回 string
对象的长度(即 string
对象中字符的个数)。
string::size_type类型
size
函数返回值其实是 string::size_type
类型,这是一种无符号类型,而且能足够存放下任何string
对象的大小。因此,所有用于存放string
类的size
函数返回值的变量,都应该是string::size_type
类型。
如果一个表达式中已经有了
size
函数就不要再使用int
了,这样可以避免混用int
和unsigned int
可能带来的问题。(在表达式中混用带符号数和无符号数将可能产生意想不到的结果)
比较string对象
string
对象相等意味着它们的长度相同而且所包含的字符也全部相同。
关系运算符<、<=、>、>=分别检验一个string
对象是否小于、小于等于、大于、大于等于另外一个string
对象。这些运算符都依照(大小写敏感的)字典顺序:
- 如果两个
string
对象的长度不同,而且较短string
对象的每个字符都与较长string
对象对应位置上的字符相同,就说较短string
对象小于较长string
对象。 - 如果两个
string
对象在某些对应的位置上不一致,则string
对象比较的结果其实是string
对象中第一对相异字符比较的结果
常见ASCII码的大小规则:0-9小于A-Z小于a-z。同个字母的大写字母比小写字母要小32。
为string对象赋值
string st1(10, 'c'), st2; // st1的内容是cccccccccc;st2是一个空字符串
st1 = st2; // 赋值:用st2的副本替换st1的内容,此时st1和st2都是空字符串
两个string对象相加
两个string
对象相加得到一个新的string
对象,其内容是把左侧的运算对象与右侧的运算对象串接。另外,复合赋值运算符(+=
)负责把右侧string
对象的内容追加到左侧string
对象的后面。
string s1 = "hello, ", s2 = "world
";
string s3 = s1 + s2; //s3的内容是 hello, world
s1 += s2; //等价于s1 = s1 + s2
字面值和string对象相加
标准库允许把字符字面值和字符串字面值转换成string
对象,所以在需要string
对象的地方可以使用这两种字面值来替代。
当把string
对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string
string s1 = "hello", s2 = "world"; //在s1和s2中都没有标点符号
string s3 = s1 + ", " + s2 + '
'; //s3的内容是 hello, world
string s4 = s1 + ", "; // 正确: 把一个string对象和一个字面值相加
string s5 = "hello" + ", "; // 错误: 两个运算对象都不是string
string s6 = s1 + ", " + "world"; // 正确:每个加法运算符都有一个运算对象是string
string s7 = "hello" + ", " +s2; //错误:不能把字面值直接相加
因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型
string
的对象。 切记,字符串字面值与string
是不同的类型。
3.2.3 处理string 对象中的字符
经常需要单独处理string
对象中的字符,比如检查一个string
对象是否包含空白,或者把string
对象中的字母改为小写,再或者查看某个特定的字符是否出现。这类处理的一个关键问题是如何获取字符本身。另一个关键问题是要知道能改变某个字符的特性。
在头文件cctype
中定义了一组标准库函数处理这部分工作:
cctype
头文件中的函数:
isalnum(c) | 当c是字母或数字时为真 |
isalpha(c) | 当c时字母时为真 |
iscntrl(c) | 当c时控制字符时为真 |
isdigit(c) | 当c是数字时为真 |
isgraph(c) | 当c不是空格但可打印时为真 |
islower(c) | 当c是小写字母时为真 |
isprint(c) | 当c是可打印字符时为真(即c是空格或c具有可视形式) |
ispunct(c) | 当c是标点符号时为真(即c不是控制字符、数字、字母可打印空白中的一种) |
isspace(c) | 当c是空白时为真(即c是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种) |
isupper(c) | 当c是大写字母时为真 |
isxdigit(c) | 当c是十六进制数字时为真 |
tolowe(c) | 如果c是大写字母,输出对应的小写字母;否则原样输出c |
toupper(c) | 如果c是小写字母,输出对应的大写字母;否则原样输出c |
建议使用C++版本的C标准库头文件。C语言中名称为 name.h 的头文件,在C++中则被命名为 cname。
因此,cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的标准。
处理每个字符?使用基于范围的for语句
如果想对string
对象中的每个字符都做点什么操作,目前最好的办法是使用C++11提供的范围 for
(range for)语句,可以遍历给定序列中的每个元素并对序列中的每个值执行某种操作。语法形式:
for (declaration : expression)
statement
其中,expression 部分是一个对象,用于表示一个序列。declaration 部分负责定义一个变量,该变量被用于访问序列中的基础元素。每次迭代,declaration 部分的变量都会被初始化为 expression 部分的下一个元素值。
示例,把string
对象中的字符以每行一个的形式输出:
string str("some string");
// 每行输出str中的一个字符
for (auto c : str) // 对于str中的每个字符
cout << c << endl; // 输出当前字符,后面紧跟一个换行符
使用范围for语句改变字符串中的字符
如果想在范围 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而小于 s.size()。C++标准并不要求标准库检测下标是否合法。使用超出范围的下标将引发不可预知的后果,以此推断,使用下标访问空
string
也会引发不可预知的结果。
下标的值称作 “下标” 或 “索引”,任何表达式只要它的值是一个整型值就能作为索引(如果某个索引是带符号类型的值,将自动转换成由string::size_type
表达的无符号类型)。
示例,将字符串的首字符改写为大写形式:
string s("some string");
if(!s.empty()) //确保s[0]的位置确有字符
s[0] = toupper(s[0]); //为s的第一个字符赋一个新值
程序的输出结果:Some string
使用下标执行迭代
示例,把s的第一个词改成大写形式:
//依次处理s中的字符直至处理完全部字符或者遇到一个空白
for(decltype(s.size()) index = 0; index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]); //将当前字符改写成大写形式
程序的输出结果:SOME string
编程时可以把下标的类型定义为相应的
size_type
,因为此类型是无符号数,可以确保下标不会小于0。此时代码只需要保证下标小于size
的值就可以了。另一种确保下标合法的有效手段就是使用范围for
语句。
使用下标执行随机访问
- 无论何时用到字符串的下标,都应该注意检查其合法性。
3.3 标准库类型vector
标准库类型 vector
表示对象的集合,因为vector
容纳着其他对象,所以也叫做 容器(container),定义在头文件 vector 中。vector
中所有对象的类型都相同,集合中的每个对象都有一个与之对应的索引, 并用于访问该对象。
C++语言既有类模板(class template),也有函数模板。
vector
是一个 类模板,模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为 实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板,需要通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。
示例:
vector<int> ivec; // ivec保存int类型的对象
vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
vector<vector<string>> file; // 该向量的元素是vector对象
vector
是模板而非类型,由vector
生成的类型必须包含vector
中元素的类型,如vector
。
因为引用不是对象,所以不存在包含引用的 vector
。
在早期的C++标准中,如果 vector
的元素还是 vector
(或者其他模板类型),定义时必须在外层 vector
对象的右尖括号和其元素类型之间添加一个空格,如 vector<vector<int> >
。但是在C++11标准中,可以直接写成 vector<vector<int>>
,不需要添加空格。
3.3.1 定义和初始化vector对象
初始化 vector
对象的方法:
vector |
v1是一个空vector,它潜在的元素是T类型的,执行默认初始化 |
vector |
v2中包含有v1所有元素的副本 |
vector |
等价于v2(v1),v2中包含有v1所有元素的副本 |
vector |
v3包含了n个重复的元素,每个元素的值都是val |
vector |
v4包含了n个重复地执行了值初始化地对象 |
vector |
v5包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector |
等价于v5{a, b, c...} |
示例:
vector<string> svec; //默认初始化,svec不含任何元素
vector<int> ivec; //初始状态为空
//在此处给ivec添加一些值
vector<int> ivec2(ivec); //把ivec的元素拷贝给ivec2
vector<int> ivec3 = ivec; //把ivec的元素拷贝给ivec3
vector<string> svec(ivec2); //错误:svec的元素是string对象,不是int
创建指定数量的元素
vector<int> ivec(10, -1); //10个int类型的元素,每个都被初始化为-1
vector<string> svec(10,"hi!") //每个都被初始化为"hi!"
值初始化
如果vector
对象的元素是内置类型,比如int
,则元素初始值自动设为0。如果元素是某种类类型,比如string
,则元素由类默认初始化:
vector<int> ivec(10); //10个元素,每个都被初始化为0
vector<string> svec(10) //10个元素,每个都是空string对象
对这种初始化的方式有两个特别限制:
-
其一,有些类要求必须明确地提供初始值,如果
vector
对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值。
对于这种类型的对象来说,只提供元素的数量而不设定初始值无法完成初始化工作。 -
其二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化
vector<int> vi = 10; //报错:No viable conversion from 'int' to 'vector<int>'
列表初始化vector对象
vector<string> articles = {"a", "an", "the"};
上述vector
对象包含三个元素:第一个是字符串“a”,第二个是字符串“an”,最后一个是字符串“the”。
C++语言提供了几种不同的初始化方式,大多数情况下这些初始化方式可以相互等价地使用,下面介绍三种例外情况:
-
使用拷贝初始化(即使用
=
)时,只能提供一个初始值 -
如果提供的是一个类内初始值,则只能使用拷贝初始化或者使用花括号的形式初始化
-
如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里
vector<string> v1 = {"a", "an", "the"}; //列表初始化 vector<string> v2 = {"a", "an", "the"}; //错误
列表初始值还是元素数量?
初始化 vector
对象时:
- 如果使用圆括号,可以说提供的值是用来构造(construct)
vector
对象; - 如果使用的是花括号,则是我们想列表初始化(list initialize)
vector
对象。
也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值地列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。
示例:
vector<int> v1(10); // v1有10个元素,每个的值都是0
vector<int> v2{10}; // v2有1个元素,该元素的值是10
vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
vector<int> v4{10, 1}; // v4有2个元素,值分别是10和1
vector<string> v5{"hi"}; //列表初始化:v5有一个元素
vector<string> v6("hi"); // 错误:不能使用字符串字面值构建vector对象
vector<string> v7{10}; // v7有10个默认初始化的元素
vector<string> v8{10, "hi"}; // v8有10个值为"hi"的元素
3.3.2 向vector对象中添加元素
对vector
对象来说,直接初始化的方式适用于三种情况:1. 初始化已知且数量较少、2. 初始值是另一个vector
对象的副本、3. 所有元素的初始值都一样。
然而更常见的情况是:创建一个vector
对象时并不清楚实际所需的元素的个数,元素的值也经常无法确定;还有些时候即使元素的初值已知,但如果这些值总量较大而各不相同,那么在创建vector
对象的时候执行初始化操作会显得过于烦琐。
有一种更好的处理办法:先创建一个空vector
,然后在运行时再利用vector
的成员函数push_back
向其中添加元素。push_back
负责把一个值当成vector
对象的尾元素 “压到(push)” vector
对象的 “尾端(back)” 。
示例:
vector<int> v2; // 空vector对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 依次把整数值放到v2尾端
// 循环结束后v2有100个元素,值从0到99
//从标准输入中读取单词,将其作为vector对象的元素存储
string word;
vector<string> text; //空vector对象
while (cin >> word) {
text.push_back(word); //把word添加到text后面
}
向vector对象添加元素蕴含的编程假定
如果循环体内部包含有向vector
对象添加元素的语句,则不能使用范围for
循环。
范围
for
语句体内不应该改变其所遍历序列的大小。
3.3.3 其他vector操作
vector
支持的操作:
v.empty() | 如果v不含有任何元素,返回真;否则返回假 |
v.size() | 返回v中元素的个数 |
v.push_back(t) | 向v的我尾端添加一个值为 t 的元素 |
v[n] | 返回v中第n个位置上元素的引用 |
v1 = v2 | 用v2中元素的拷贝替换v1中的元素 |
v1 = {a, b, c...} | 用列表中元素的拷贝替换v1中的元素 |
v1 == v2 | v1和v2相等 当且仅当 它们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 | |
<, <=, >, >= | 顾名思义,以字典顺序进行比较 |
vector
的empty
和size
两个成员与string
的同名成员功能完全一致:empty
检查vector
对象是否包含元素然后返回一个布尔值;size
则返回vector
对象中元素的个数,返回值的类型时由vector
定义的size_type
类型。
要使用
size_type
,需首先指定它是由哪种类型定义的。vector
对象的类型总是包含着元素的类型:vector<int>::size_type //正确 vector::size_type //错误
只有当元素的值可比较时,vector
对象才能被比较。一些类,如string
等,确实定义了自己的相等性运算符和关系运算符;另外一些,如Sales_item
所支持的运算,显然并不支持相等性判断和关系运算符等操作。
计算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/10]; //将对应分数段的计数值加1
}
不能用下标形式添加元素
示例,试图为vector
对象ivec
添加10个元素:
vector<int> ivec; //空 vector对象
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) {
ivec[ix] = ix; //严重错误:ivec不包含任何元素
}
//正确的方法是使用push_back:
vector<int> ivec; //空 vector对象
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) {
ivec.push_back(ix); //正确: 添加一个新元素,该元素的值是ix
}
警告:
vector
对象(以及string
对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
提示:只能对确知已存在的元素执行下标操作:
vector<int> ivec; // 空vector对象 cout << ivec[O]; // 错误:ivec不包含任何元素 vector<int> ivec2(10); // 含有10个元素的vector对象 cout << ivec2[10]; // 错误: ivec2元素的合法索引是从0到9
试图用下标的形式去访问一个不存在的元素将引发错误,而且这种错误不会被编译器所发现,而是在运行时产生一个不可预知的值。
★ 确保下标合法的一种有效手段就是尽可能使用范围
for
语句。
3.4 迭代器介绍
迭代器的作用和下标运算类似,可以访问string
对象的字符或vector
对象的元素,但是更加通用。所有标准库容器都可以使用迭代器,但是其中只有少数几种同时支持下标运算符(严格来说,string
对象不属于容器类型,但是string
支持很多与容器类型类似的操作)。
迭代器的作用类似于指针类型,也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string
对象中的字符。
3.4.1 使用迭代器
和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型都拥有 begin
和 end
两个成员。其中begin
成员返回指向第一个元素(或第一个字符)的迭代器,end
成员返回指向容器(或string
对象)“ 尾元素的下一位置(one past the end) ”的迭代器。
如有下述语句:
// 由编译器决定b和e的类型;
// b表示v的第一个元素, e表示v尾元素的下一位置
auto b = ivec.begin(), e = ivec.end(); // b和e的类型相同
-
end
成员返回的迭代器常被称为 尾后迭代器(off-the-end iterator) 或简称为 尾迭代器(end iterator)。 -
特殊情况下如果容器为空,则
begin
和end
返回的是同一个迭代器,都是尾后迭代器。
迭代器运算符
标准容器迭代器的运算符:
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指示容器中的下一元素 |
--iter | 令iter指示容器中的上一元素 |
iter1 == iter2 | 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一容器的尾后迭代器,则相等;反之,不相等 |
iter1 != iter2 |
示例,利用迭代器把string
对象的首字母改为大写:
string s("some thing");
if (s.begin() != s.end()) { //确保s非空
auto it = s.begin(); //it表示s的第一个字符
*it = toupper(*it); //将当前字符改为大写形式
}
将迭代器从一个元素移动到另外一个元素
因为
end
返回的迭代器并不实际指向某个元素,所以不能对其进行递增或者解引用的操作。
示例,利用迭代器把string
对象中的第一个单词改写为大写形式:
//依次处理s的字符直至我们处理完全部字符或者遇到空白
for (suto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it); //将当前字符改为大写形式
泛型编程
在
for
或者其他循环语句的判断条件中,最好使用!=
而不是<
。因为所有标准库容器的迭代器都定义了==
和!=
,而只有其中少数同时定义了<
运算符,用这种编程风格在标准库提供的所有容器上都有效。
迭代器类型
拥有迭代器的标准库类型使用iterator
和const_iterator
来表示迭代器的类型。
- 如果
vector
对象或string
对象是一个常量,则只能使用const_iterator
,该类型和常量指针差不多,只能读元素,不能修改元素。
vector<int>::iterator it; //it能读写vector<int>中的元素
string::iterator it2; //it2能读写string对象中的字符
vector<int>::const_iterator it3; //it3只能读元素,不能写元素
string::iterator it4; //it4只能读字符,不能写字符
begin和end运算符
begin
和 end
返回的迭代器具体类型由对象是否是常量决定,如果对象是常量,则返回 const_iterator
;如果对象不是常量,则返回 iterator
。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator
如果对象只读操作而无需写操作,最好使用常量类型。为便于专门得到const_iterator
类型的返回值,C++11新增了 cbegin
和 cend
函数,不论 vector
或 string
对象是否为常量,都返回 const_iterator
迭代器。
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator
结合解引用和成员访问操作
C++语言定义了 箭头运算符(->
) 把解引用和成员访问两个操作结合在了一起。也就是说,it->men
<==>(*it).men
某些对vector对象的操作会使迭代器失效
虽然vector
对象可以动态地增长,但是也会有一些副作用:
- 不能在范围for循环中向vector对象添加元素。
- 任何可能改变
vector
对象容量的操作,比如push_back
,都会使该vector
对象的迭代器失效。
谨记,但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
3.4.2 迭代器运算
vector
和 string
迭代器支持的运算:
iter + n | 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
iter - n | 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一位置 |
iter1 += n | 迭代器加法的复合赋值语句,将iter1加n的结果赋给iter1 |
iter1 -= n | 迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1 |
iter1 - iter2 | 两个迭代器相减的结果是他们的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
>、>=、<、<= | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一个位置 |
示例:
//计算得到最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size() /2;
如果vi有20个元素,已知下标从0开始,则迭代器所指的元素是vi[10]
,也就是从首元素开始向前相隔10个位置的那个元素。
//假设it和mid是同一个vector对象的两个迭代器,比较它们所指的位置孰前孰后
if ( it < mid)
// 处理Vl前半部分的元素
- 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个送代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为
difference_type
的类型。因为这个距离可正可负,所以difference_type
是一种带符号整数类型。
使用迭代器运算
示例,使用迭代器完成了二分搜索:
// text必须是有序的
// beg和end表示我们搜索的范围
// beg指向搜索范围内的第一个元素、end指向居元素的下一位置、mid指向中间的那个元素
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且还没有找到sought时执行循环
whi1e (mid != end && *mid != sought)
{
if (sought < *mid) // 我们要找的元素在前半部分吗?
end = mid; // 如果是,调整搜索范围使得忽略掉后半部分
e1se // 我们要找的元素在后半部分
beg = mid + 1; // 在mid之后寻找
mid = beg + (end - beg)/2; // 新的中间点
}
3.5 数组
数组是一种类似于标准库类型 vector
的数据结构(在性能和灵活性的权衡上与vector
有所不同),也是存放类型相同的对象的容器,但数组的大小确定不变,不能随意向数组中添加元素。
如果不清楚元素的确切个数,应使用
vector
。
3.5.1 定义和初始化内置数组
数组是一种复合类型,声明形式为 a[d]
,其中 a 是数组名称,d 是数组维度。维度说明了数组中元素的个数,因此必须大于0 。数组中元素的个数也属于数组类型的一部分, 编译的时候维度应该是己知的。也就是说,即维度必须是一个常量表达式。
unsigned cnt = 42; // 不是常量表达式
constexpr unsigned sz = 42; // 常量表达式
int arr[10]; // 含有10个整数的数组
int *parr[sz]; // 含有42个整型指针的数组
string bad[cnt]; // 错误:cnt不是常量表达式
string strs[get_size()]; // 当get_size是constexpr时正确,否则错误
默认情况下,数组的元素被默认初始化。
和内置类型的变量一样,如果在函数体内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。
- 定义数组的时候必须指定数组的类型,不允许用
auto
关键字由初始值列表推断类型。
显式初始化数组元素
如果定义数组时提供了元素的初始化列表,则允许省略数组维度。
-
如果在声明时没有指明维度,编译器会根据初始值的数量计算维度。
-
如果显式指明了维度,那么初始值的总数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化。
const unsigned sz = 3; int ia1[sz] = {0,1,2}; // 含有3个元素的数组,元素值分别是0,1,2 int a2[] = {0, 1, 2}; // 维度是1的数组 int a3[5] = {0, 1, 2}; // 等价于a3[] = {0, 1, 2, 0, 0} string a4[3] = {"hi", "bye"}; // 等价于a4[] = {"hi", "bye", ""} int a5[2] = {0,1,2}; // 错误:初始值过多
字符数组的特殊性
字符数组有一种额外的初始化形式,可以用字符串字面值初始化字符数组。
- 注意:字符串字面值结尾处的空字符也会一起被拷贝到字符数组中。
char a1[] = {'C', '+', '+'}; // 列表初始化,没有空字符
char a2[] = {'C', '+', '+', ' '}; // 列表初始化,含有显式的空字符
char a3[] = "C++"; // 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; // 错误:没有空间可存放空字符!
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
int a[] = {O , 1 , 2}; // 含有3个整数的数组
int a2[] = a; // 错误:不允许使用一个数组初始化另一个数组
a2 = a; // 错误:不能把一个数组直接赋值给另一个数组
理解复杂的数组声明
要想理解数组声明的含义,最好的办法就是:从数组的名字开始由内向外的顺序阅读。
int *ptrs[10]; // ptrs是含有10个整型指针的数组
int &refs[10] = /* ? */; // 错误:不存在引用的数组
int (*Parray)[10] = &arr; // Parray指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组
当然,对修饰符的数量并没有特殊限制:
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针
3.5.2 访问数组元素
使用数组下标的时候,通常将其定义为 size_t
类型,这是一种机器相关的无符号类型,能够表示内存中任意对象的大小。size_t
定义在头文件 cstddef
中。
-
数组除了大小固定这一特点外,其他用法与
vector
基本类似。 -
与
vector
和string
一样,当需要遍历数组时,最好的办法也是使用范围for
语句,可以减轻人为控制遍历过程的负担。示例,输出所有的scores:
for (auto i : scores) //对于scores中的每个计数值 cout << i << " "; //输出当前的计数值 cout << endl;
-
与
vector
和string
一样,数组的下标是否在合理范围之内由程序员负责检查。要想防止数组下标越界,除了小心谨慎注意细节以及对代码进行彻底的测试之外,没有其他好办法了。
大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
3.5.3 指针和数组
C++语言中,指针和数组有非常紧密的联系。如下述要介绍的,使用数组的时候编译器一般会把它转换成指针。
- 对数组的元素使用取地址符就能得到指向该元素的指针
string nums[] = {"one", "two", "three"}; // 数组的元素是string对象
string *p = &nums[0]; // p指向nums的第一个元素
string *p2 = nums; // 等价于p2 = &nums[0]
在大多数表达式中,使用数组类型的对象其实是在使用一个指向该数组首元素的指针。
-
当使用数组作为一个
auto
变量的初始值时,推断得到的类型是指针而非数组。但decltype
关键字不会发生这种转换,直接返回数组类型。int ia[] = {0,1,2,3,4,5,6,7,8,9}; // ia是一个含有10个整数的数纽 auto ia2(ia); // ia2是一个整型指针,指向ia的第一个元素 ia2 = 42; // 错误:ia2是一个指针,不能用int值给指针赋值 auto ia2(&ia[0]); // 显然ia2的类型是int* // ia3是一个含有10个整数的数组 decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9}; ia3 = p; // 错误:不能用整型指针给数组赋值 ia3[4] = i; // 正确:把i的值赋给ia3的一个元素
指针也是迭代器
vector
和string
的迭代器支持的运算,数组的指针全都支持。
-
就像使用迭代器遍历
vector
对象中的元素一样,使用指针也能遍历数组中的元素。(前提条件是先获取指向数组第一个元素的指针和指向数组尾元素的下一位置的指针)//获取尾后指针 int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; int *e = &arr[10]; //指向arr尾元素的下一位置的指针
标准库函数begin和end
尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,C++11标准在头文件 iterator 中定义了两个名为 begin
和 end
的函数,功能与容器中的两个同名成员函数类似,其参数是一个数组。
begin
函数返回指向 ia首元素的指针,end
函数返回指向 ia尾元素下一位置的指针。
示例,找到arr中的第一个负数:
// pbeg指向arr的首元素,pend指向arr尾元素的下一位置
int *pbeg = begin(arr), *pend = end(arr);
// 寻找第一个负值元素,如果已经检查完全部元素则结束循环
while (pbeg != pend && *pbeg >= 0)
++pbeg;
特别注意:尾后指针不能执行解引用和递增操作。
指针运算
- 给(从) 一个指针加上(减去)某整数值,结果仍是指针。
constexpr size_t sz = 5;
int arr[sz] = {1, 2, 3, 4, 5};
int *ip = arr; // 等价于int *ip = &arr[O]
int *p2 = ip + 4; // ip2指向arr的尾元素arr[4]
// 正确:arr转换成指向它首元素的指针;p指向arr尾元素的下一位置
int *p = arr + sz; //使用警告:不要解引用!
int *p2 = arr + 10; //错误:arr只有5个元素,p2的值未定义
-
两个指针相减的结果类型是
ptrdiff_t
,这是一种定义在头文件 cstddef 中的机器相关的类型。因为差值可能为负值, 所以ptrdiff_t
是一种带符号类型。auto n = end(arr) - begin(arr); // n的值是5 ,也就是arr中元素的数量
-
只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较。
如果两个指针分别指向不相关的对象,则不能比较它们。(自己测试:可以比较,只是无意义)int i = 0, sz = 42; int *p = &i, *e = &sz; //未定义的:p和e无关,因此比较毫无意义! while (p < e)
解引用和指针运算的交互
表达式 * (ia+4)
计算 ia
前进4个元素后的新地址,解引用该结果指针的效果等价于表达式 ia[4]
。
int ia[] = {0, 2, 4, 6, 8}; // 含有5个整数的数组
int last = *(ia + 4); // 正确:把last初始化成8,也就是ia[4]的值
last = *ia + 4; // 正确:last =4等价于 ia[0] + 4
下标和指针
- 对数组执行下标运算其实是对指向数组元素的指针执行下标运算。
int ia[] = {0, 2, 4, 6, 8}; // 含有五个整数的数组
int i = ia[2]; // ia转换成指向数组首元素的指针
// ia[2] 得到(ia + 2)所指的元素
int *p = ia; // p指向ia的首元素
i = *(p+2); //等价于i = ia[2]
- 标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求。
内置的下标运算符可以处理负值,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。
int *p = &ia[2]; // p指向索引为2的元素
int j = p[1]; // p[1]等价于*(p + 1),就是ia[3]表示的那个元素
int k = p[-2]; //p[-2]是ia[0]表示的那个元素
3.5.4 C风格字符串
尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞, 是诸多安全问题的根本原因。
字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的 C风格字符串(C-style character string)。C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。C风格字符串将字符串存放在字符数组中,并以 空字符结束(null terminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符