目录
本章将介绍最重要的标准库类型:string 和 vector。string 表示可变长的字符序列,vector 存放的是某种给定类型对象的可变长序列。本章还将介绍内置数组类型。
3.1 命名空间的 using 声明
目前为止,我们用到的库函数基本上都属于命名空间 std,本节将学习其中最安全的方法,也就是使用 using声明(using declaration)。
//using 声明具有如下的形式
using namespace::name
//例如
using std::cin
每个名字都需要独立的 using 声明
按照规定,每个 using 声明引入命名空间中的一个成员。
头文件不应包含 using 声明
位于头文件的代码一般来说不应该使用 using 声明。这是因为头文件的内容会拷贝到所有引用它的文件中去。如果某个头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。可能会产生冲突
3.2 标准库类型 string
标准库类型 string 表示可变长的字节序列,使用 string 类型必须首先包含 string 头文件。
#include<string>
3.2.1 定义和初始化 string 对象
//定义和初始化string对象
string s1; //默认初始化,s1是一个空字符串
string s2 = s1;//s2 是 s1 的副本
string s3 = "hiya";//s3 是该字符串字面值的副本
string s4(10, 'c');//s4的内容是 cccccccccc
初始化 string 对象的方式:
//初始化 string 对象的方式
string s1; //默认初始化,s1是一个空串
string s2(s1); //s2 是 s1 的副本
string s2 = s1; //等价于s2(s1),s2 是 s1 的副本
string s3("hiya"); //s3 是字面值"hiya"的副本,除了字面值最后的那个空字符外
string s3 = "hiya"; //等价于 s3("hiya"),s3 是字面值"hiya"的副本
string s4(n, 'c'); //s4的内容是 cccccccccc,把s4初始化为由连续n个字符c组成的串
直接初始化和拷贝初始化
如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)。
3.2.2 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连接后的结果
s1 == s2; //如果s1和s2中所含的字符完全一样,则它们相等;大小写敏感
s1 != s2; //与上一条相反,判断不相等
<,<=,>,>= //利用字符再字典中的顺序进行比较,且对字母的大小写敏感
读写 string 对象
与内置类型的输入输出操作一样。
string s1,s2;
cin>>s1>>s2;
cout<<s1<<s2;
读取未知数量的string 对象
//读取未知数量的 string 对象
string word;
while (cin >> word) //反复读取,直至到达文件末尾
{
cout << word << endl;//逐个输出单词,每个单词后面紧跟一个换行
}
使用 getline() 读取一整行
getline() 函数的参数是一个输入流和一个 string 对象,函数从给定的输入流中读入内容,直到遇到换行符位置(注意换行符也被读进来了),然后把所读的内容存入到 string 对象中去(注意不存换行符)。
//使用 getline() 读取一整行
string line;
//每次读入一整行,直至到达文件末尾
while (getline(cin, line))
cout << line << endl;
return 0;
string 的 empty 和 size 操作
使用点操作符(.)调用即可。
string::size_type 类型
size 函数返回的是一个 string::size_type 类型的值。它是一个无符号类型的值而且能足够存放下任何 string 对象的大小。
由于 size 函数返回的是一个无符号整型数,如果一条表达式中已经有了 size 函数就不要在表达式中再使用 int了,这样可以避免混用 int 和 unsigned 可能带来的问题。
比较 string 对象
string 类定义了几种用于比较字符串的运算符。这些比较运算符逐一比较 string 对象中的字符,并且对大小写敏感。
这些运算符都依照(大小写敏感的)字典顺序:
- 如果两个 string 对象的长度不同,而且较短 string 对象的每个字符都与较长 string 对象对应位置上的字符相同,就说较短 string 对象小于较长 string 对象。
- 如果两个 string 对象在某些对应的位置上不一致,则 string 对象比较的结果其实是 string 对象中第一对相异字符相比较的结果。
为 string 对象赋值
可以用等号进行赋值。
两个 string 对象相加
两个 string 对象相加将得到一个新的 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 s4 = s1 + ","; //正确:把一个 string 对象和一个字面值相加
string s5 = "hello" + ","; //错误:两个运算对象都不是 string
string s6 = s1 + "," + "world"; //正确:每个加法运算符都有一个运算对象是 string
string s7 = "hello" + "," + s2; //错误:不能把字面值之间相加
s6 合法是因为 s1+"," 的结果是一个 string 对象,它同时作为第二个加法运算符的左侧运算对象。
注意:C++语言中的字符串字面值并不是标准库类型 string 的对象。字符串字面值与 string 是不同的类型。
3.2.2 节练习
//练习3.2 编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词
//读入一整行,直至到达文件末尾
string line;
cout << "请输入您的字符串,可以包含空格:" << endl;
while (getline(cin, line))
cout << line << endl;
//读入一个词
string word;
cout << "请输入您的单词,不可包含空格:" << endl;
while (cin >> word)
{
cout << word << endl;
}
读入整行:
读入一个词:
练习3.3:请说明 string 类的输入运算符和 getline 函数分别是如何处理空包字符的。
标准 string 的输入运算符自动忽略字符串开头的空白(包括空格符、换行符、制表符等),从第一个真正的字符开始读起,直到遇到下一处空白为止。
如果希望在最终的字符串中保留输入时的空白符,应该使用 getline 函数代替原来的 >> 运算符,getline 从给定的输入流中读取数据,知道遇到换行符为止,此时换行符也被读取进来,但是并不存储在最后的字符串中。
练习3.4
//练习3.4:编写程序读入两个字符串,并比较是否相等。若不相等,输出较大的那个字符串
string s1, s2;
cout << "请输入两个字符串:" << endl;
cin >> s1 >> s2;
if (s1 == s2)
{
cout << "两个字符串相等" << endl;
return;
}
else if(s1 > s2)
{
cout << s1 << " 大于 " << s2 << endl;
}
else
{
cout << s2 << " 大于 " << s1 << endl;
}
//改写上述程序:比较是否等长,输出长度较大的字符串
string s1, s2;
cout << "请输入两个字符串:" << endl;
cin >> s1 >> s2;
auto len1 = s1.size();
auto len2 = s2.size();
if (len1 == len2)
{
cout << "两个字符串一样长" << endl;
return;
}
else if (len1 > len2)
{
cout << s1 << " 长于 " << s2 << endl;
}
else
{
cout << s2 << " 短于 " << s1 << endl;
}
练习3.5:
//练习3.5 编写程序从标准输入流汇总读入多个字符串并将它们连接在一起,输出连接成的大字符
string s1, s;
cout << "请输入一个字符串,按ctrl+z结束输入" << endl;
while (cin >> s1)
{
s += s1;
}
cout << "拼接后的字符串是:" << s<<endl;
//练习3.5 修改上述程序,用空格把输入的多个字符分隔开来
string s1, s;
cout << "请输入一个字符串:" << endl;
while (cin >> s1)
{
if (!s.size()) //第一个拼接的字符串之前不加空格
s += s1;
else
{
s = s + " " + s1;
}
}
cout << "拼接后的字符串是:" << s << endl;
3.2.3 处理 string 对象中的字符
在 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 是十六进制数字时为真 |
tolower(c) | 如果 c 是大写字母,输出对应的小写字母;否则原样输出 c |
toupper(c) | 如果 c 是小写字母,输出对应的大写字母;否则原样输出 c |
处理每个字符?使用基于范围的 for 语句
如果想对 string 对象中的每个字符做点儿什么操作,目前最好的方法是使用C++11 新标准提供的一种语句: 范围 for(range for) 语句。语法形式如下:
for (declaration: expression)
statement
其中, expression 部分是一个对象,用于表示一个序列。 declaration 部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代, declaration 部分的变量会被初始化为 expression 部分的下一个元素值。
举个简单的例子,我们可以使用 for 范围语句把 string 对象中的字符每行一个输出来。
string str("some string");
//每行输出 str 中的一个字符
for (auto c : str)
cout << c << endl;
再举个稍微复杂一点的例子,使用范围 for 语句和 ispunct 函数来统计 string 对象中标点符号的个数。
//统计 string 对象中标点符号的个数
string s("hello,world!!");
//cnt 的类型和 s.size 的返回类型一样
decltype(s.size()) cnt = 0;
for (auto c : s)
{
if (ispunct(c))
{
++cnt;
}
}
cout << "字符串中标点符号的数目为:" << cnt << endl;
使用范围 for 语句改变字符串中的字符
如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。记住,所谓引用只是给定对象的一个别名,因此当使用这个引用这个循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。
假设我们想把字符串改写为大写字母的形式。
//将字符串改写成大写字母的形式
string s("Hello World");
for (auto &c : s) //注意,c 是引用
{
c = toupper(c);//c 是一个引用,因此赋值语句将改变 s 中字符的值
}
cout << s << endl;
只处理一部分字符?
要想访问 string 对象中的单个字符有两种形式:一种是使用下标;另外一种是使用迭代器,其中迭代器将在之后介绍。
下标运算符( [ ] )接收的输入参数是 string::size_type 类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。
string 对象的下标从 0 计起。下标的值称作“下标”或“索引”。
string s("sssss dtr");
if(!s.empty())
cout<<s[0]<<endl; //输出 s 的第一个字符
使用下标迭代式
把 s 的第一个词改成大写形式:
//一次处理 s 中字符,直至处理完全部字符或遇到一个空包
string s("some string");
for (decltype(s.size())index = 0; index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]);//将当前字符改成大写形式
使用下标必须确保其在合理范围内,即下标必须大于等于0而小于字符串的size() 的值。
使用下标执行随机访问
编写一个程序把0到15之间的十进制数转换成对应的十六进制形式。
//把0到15之间的十进制数转换成对应的十六进制数
const string hexdigits = "0123456789ABCDEF";
cout << "请输入几个0到15之间的整数,用空格分隔,ctrl + z 停止输入" << endl;
string result;
int n;
while (cin >> n)
{
if (n < hexdigits.size())//忽略无效输入
result += hexdigits[n];//得到对应的十六进制数
}
cout << "对应的十六进制数为:" << result << endl;
3.2.3 节练习
//练习3.6:编写一段程序,使用范围for语句将字符串内的所有字符用X代替
string s("some sttrin");
for (auto &c : s)//记住,应该使用引用
{
c = 'X';
}
cout << s << endl;
//练习3.7:就上一题而言,如果将循环控制变量的类型设为 char 将发生什么?先估计一下结果,然后编程验证。
string s("some sttrin");
for (char &c : s)
{
c = 'X';
}
cout << s << endl;
结果一样,因为 auto 推断出的类型也是 char。
不合法,初始状态下没有给 s 赋任何初值,所以字符串 s 的内容为空,也就不存在首字符,下标 0 是非法的。
//练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余部分。
string s;
cout << "请输入一个包含标点符号的字符串:" << endl;
getline(cin, s);
for (auto &c : s)
{
if (!ispunct(c))//只能判断英文标点符号
{
cout << c;
}
}
cout << endl;
语法上是合法的, s 是一个常量字符串,则 c 的推断类型是常量引用, c 所绑定的对象值不能改变。
3.3 标准库类型 vector
标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为 vector “容纳”着其他对象,所以它也常被称做容器(container)。
C++ 语言既有类模板(class template),也有函数模板,其中 vector 是一个类模板。
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
注意: vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,例如 vector<int>。
vector 能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所有不存在包含引用的 vector。
3.3.1 定义和初始化 vector 对象
vector<T> v1 | v1 是一个空 vector,它潜在的元素是T类型的,执行默认初始化 |
vector<T> v2(v1) | v2 中包含有 v1 所有元素的副本 |
vector<T> v2 = v1 | 等价于 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 包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector<T> v5={a,b,c...} | 等价于 v5{a,b,c...} |
可以默认初始化 vector 对象,从而创建一个指定类型的空vector。
列表初始化 vector 对象
C++11 新标准提供了另外一种为 vector 对象的元素赋初值的方法,即列表初始化。
vector<string> articles = {"a", "an", "the"};
如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里。
vector<string> v2("a", "an", "the");//错误
创建指定数量的元素
vector<int> ivec(10, -1); //10个 int 类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!");//10个 string 类型的元素,每个都被初始化为 "hi!"
值初始化
通常情况下,可以只提供 vector 对象容纳的元素数量而不用略去初值。此时库会创建一个值初始化的(value-initialized)元素初值,并将它赋给容器中的所有元素。这个初值由 vector 对象中元素的类型决定。
vector<int> ivec(10); //10个 int 类型的元素,每个都被初始化为 0
vector<string> svec(10);//10个 string 类型的元素,每个都是空 string 对象
对于这种初始化的方式有两个特殊的限制:一、有些类要求明确地提供初始值(定义在函数体内部的内置数据类型将不被初始化(uninitialized)),如果 vector 对象中元素的类型不支持默认初始化,则必须提供初始的元素,不能只提供元素的数量。
二、如果只提供了元素的数量而没有设定初始值,只能使用直接初始化。
vector<int> vi = 10; //错误:必须使用直接初始化的形式指定向量大小
列表初始化还是元素数量?
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。
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
如果用的是圆括号,可以说提供的值是用来构造(construct) vector 对象的。
如果用的是花括号,可以表述成我们想要列表初始化(list initialize)该 vector 对象。
另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了。
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.1 节练习
(a)是正确的,定义了一个名为 ivec 的 vector 对象,其中每个元素都是 vector<int> 对象
(b)是错误的,svec 的元素类型是 string,而 ivec 的元素类型是 int,因此不能使用 ivec 初始化 svec。
(c)是正确的,定义了一个名为 svec 的 vector 对象,其中含有 10 个元素,每个元素都是字符串 null。
(a)元素数量为0;
(b)元素数量为10,每一个元素都被初始化为0;
(c)元素数量为10,每一个元素都被初始化为42;
(d)元素数量为1,元素的值为10;
(e)元素数量为2,两个元素的值分别为 10 和 42;
(f)元素数量为10,每一个元素都被初始化为空串;
(g)元素数量为10,每一个元素都被初始化为 "hi"。
3.3.2 向 vector 对象中添加元素
使用 vector 的成员函数 push_back 向其中添加元素。push_back 负责把一个值当成 vector 对象的尾元素“压到(push)” vector 对象的“尾端(back)”。
vector<int> v2;
for (int i = 0; i != 100; ++i)
{
v2.push_back(i);
}
//循环结束后 v2 有100个元素,值从0到99
关键概念:vector 对象能高效增长
C++ 标准要求 vector 应该能在运行时高效快速地添加元素。那么在定义 vector 对象时设定其大小也就没什么必要了。
向 vector 对象添加元素蕴含的编程假定
必须要确保所写的循环正确无误,特别是在循环有可能改变 vector 对象容量的时候。
如果循环体内包含有向 vector 对象添加元素的语句,则不能使用范围 for 循环。
3.3.2 节练习
//练习3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象
vector<int> ivec;
int num;
cout << "请输入一组整数:ctrl+z结束输入" << endl;
while (cin >> num)
{
ivec.push_back(num);
}
//输出 ivec 的每个元素
for (auto num : ivec)
{
cout << num << " ";
}
cout << endl;
//练习3.15:改写上题的程序,不过这次读入的是字符串
vector<string> ivec;
string s;
cout << "请输入一组字符串:ctrl+z结束输入" << endl;
while (cin >> s)
{
ivec.push_back(s);
}
//输出 ivec 的每个元素
for (auto s : ivec)
{
cout << s << " ";
}
cout << endl;
3.3.3 其他 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 内对象的索引
使用下标运算符能获取到指定的元素。vector 对象的下标也是从 0 开始计起的。
举个例子,假设有一组成绩的集合,其中成绩的取值是从 0 到100。以 10分为一个数段,统计各个分数段各有多少个成绩。
//统计各个分数段的各有多少个成绩
vector<unsigned> scores(11, 0); // 11个分数段,全部初始化为0
unsigned grade;
while (cin >> grade)
{
if (grade <= 100) //只处理有效的成绩
{
++scores[grade / 10]; //将相应分数段的计数值加1
}
}
for (auto g : scores)
{
cout << g << " ";
}
cout << endl;
不能用下标形式添加元素
vector 对象(以及 string 对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
3.3.3 节练习
//练习3.17:从 cin 读入一组词并把它们存入一个 vector 对象
//然后设法把所有词都改写为大写形式。输出改变后的结果,每个词占一行
string word;
vector<string> sv;
cout << "请输入一组字符串,用空格分隔:" << endl;
while (cin >> word)
{
for (auto &c : word)
{
c = toupper(c);
}
sv.push_back(word);
}
//输出改变后的结果
for (auto w : sv)
{
cout << w << endl;
}
不合法,改成 ivec.push_back(42);
//练习3.19:使用3种方法定义一个含有10个元素,每个元素的值都是42的 vector 对象
vector<int> v1(10, 42);
vector<int> v2{ 42,42,42,42,42,42,42,42,42,42 };
vector<int> v3;
for (int i = 0; i < 10; ++i)
{
v3.push_back(42);
}
第一种最好。
//练习3.20:读入一组整数并把它们存入一个 vector 对象,将每对相邻整数的和输出出来
vector<int> v;
int n;
cout << "请输入一组整数,用空格分隔:" << endl;
while (cin >> n)
{
v.push_back(n);
}
//输出相邻整数的和
for (int i = 0; i < v.size() - 1; ++i)
{
if (i != v.size() - 1)
{
cout << "第 " << i+1 << " 个数和第 " << i + 2 << " 个数的和为 " << v[i] + v[i + 1] << endl;
}
}
//修改上述的程序,先输出第1个和最后一个元素的和,然后输出第2个和倒数第2个元素的和,以此类推
vector<int> v;
int n;
cout << "请输入一组整数,用空格分隔:" << endl;
while (cin >> n)
{
v.push_back(n);
}
if (v.empty())
{
return;
}
int len = v.size() - 1;
//输出相邻整数的和
for (int i = 0; i < v.size() - 1; ++i)
{
//数量是偶数
if ((len + 1) % 2 == 0)
{
if (i == (len / 2) +1)
{
break;
}
cout << "第 " << i+1 << " 个数和第 " << len-i+1 << " 个数的和为 " << v[i] + v[len-i] << endl;
}
else
{
if (i == (len / 2))
{
break;
}
cout << "第 " << i + 1 << " 个数和第 " << len - i + 1 << " 个数的和为 " << v[i] + v[len - i] << endl;
}
}
3.4 迭代器介绍
3.4.1 使用迭代器
和指针不一样,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为 begin 和 end 的成员,其中 begin 成员负责返回指向第一个元素(或第一个字符)的迭代器。
end 成员则负责返回指向容器(或 string 对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素。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 | 或者它们是同一个容器的尾后迭代器,则相等;反之,不相等 |
将迭代器从一个元素移动到另外一个元素
迭代器使用递增(++)运算符来从一个元素移动到下一个元素。迭代器的递增是将迭代器“向前移动一个位置”。
迭代器类型
那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来代表迭代器的类型。const_iterator 能读取但是不能修改它所指的元素值。iterator 的对象可读可写。
begin 和 end 运算符
begin 和 end 返回的具体类型由对象是否是常量决定。
也可用 cbegin 和 cend 返回 const_iterator。
结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能进一步访问它的成员。例如:对于一个由字符串组成的 vector 对象来说,要想检查其元素是否为空,令 it 是该 vector 对象的迭代器,只需检查 it 所指字符串是否为空就可以了。
(*it).empty();
注意,上面的圆括号必不可少。为了简化山上述表达式,C++定义了箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说 it-<>mem 和 (*it).mem 表达的意思相同。
某些对 vector 对象的操作会使迭代器失效
限制一:不能在范围 for 循环中向 vector 对象添加元素。
限制二:任何一种可能改变 vector 对象容量的操作,比如 push_back ,都会使该 vector 对象的迭代器失效。
3.4.1 节练习
//练习3.23:创建一个含有10个整数的 vector 对象,使用迭代器将所有元素的值都变成原来的两倍。
vector<int> v;
srand((unsigned)time(NULL));
for (int i = 0; i < 10; ++i)
{
v.push_back(rand() % 1000);
}
cout << "随机生成的10个数字是:";
for (auto it = v.begin(); it != v.end(); ++it)
{
cout << *it << " ";
(*it) = 2 * (*it);
}
cout << endl;
cout << "翻倍后的10个数字是:";
for (auto n : v)
{
cout << n << " ";
}
cout << endl;
3.4.2 迭代器运算
迭代器的递增运算令迭代器每次移动一个元素,所有的标准库容器都有支持递增运算的迭代器。
iter + n | 迭代器加上一个整数仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器位元素的下一位置。 |
iter - n | 迭代器减去一个整数仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器位元素的下一位置。 |
iter1 += n | 迭代器加法的复合赋值语句,将 iter1 加 n 的结果赋给 iter1 |
iter1 -= n | 迭代器减法的复合赋值语句,将 iter1 减 n 的结果赋给 iter1 |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动差值个元素后得到左侧的迭代器。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
>, >= , <, <= | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置 |
迭代器的算术运算
可以令迭代器和一整数值相加(或相减),其返回值是向前(或向后)移动了若干个位置的迭代器。
使用迭代器运算
使用迭代器运算的一个经典算法是二分搜索。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜索;假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索。
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg) / 2;//初始状态下的中间点
while (mid != end && *mid != target)
{
if (target < *mid)
{
end = mid;
}
else
{
beg = mid + 1;
}
mid = beg + (end - beg) / 2; //新的中间点
}
3.4.2 节练习
C++ 并没有定义两个迭代器的加法运算,实际上直接把两个迭代器加起来是没有意义的。
与之相反,C++ 定义了迭代器的减法运算,两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动多少个元素后可以得到左侧的迭代器,参与运算的两个迭代器必须指向同一个容器中的元素或尾后元素。
另外,C++ 还定义了迭代器与整数的加减法运算,用以控制迭代器在容器中左右移动。
在本题中,因为迭代器的加法不存在,所以 mid = (beg + end)/2; 不合法。 mid= beg+(end - beg)/2; 的含义是,先计算 end - beg 的值得到容器中的元素个数,然后控制迭代器从开始处向右移动二分之一容器的长度,从而定位到容器正中间的元素。
3.5 数组
数组是一种类似标准库 vector 的数据结构。与 vector 相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与 vector 不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。
tip:如果不清楚元素的确切个数,请使用 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 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,因此不存在引用的数组。
显示初始化数组元素
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。
//显示初始化数组元素
const unsigned sz = 3;
int arr[sz] = { 0,1,2 }; //含有 3 个元素的数组,元素值分别是0,1,2
int a2[] = { 0,1,2 }; //维度是3的数组
int a3[5] = { 0,1,2 }; //等价于 a3[] = { 0,1,2,0,0 };
string a4[3] = { "hi","ya" }; //等价于 a4[] = { "hi","ya", "" }
int a5[2] = { 0,1,2 }; //错误:初始值过多
字符数组的特殊性
字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去。
//字符数组
char a1[] = { 'c','+','+' }; //列表初始化,没有空字符
char a2[] = { 'c','+','+','\0' }; //列表初始化,含有显式的空字符
char a3[] = "c++"; //自动添加表示字符串结束的空字符
const char a4[6] = "daniel"; //错误:没有空间可存放空字符
不允许拷贝和赋值
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a[] = {0, 1, 2};
int a2[] = a; //错误
a2 = a; //错误
一些编译器支持数组的赋值,这就是所谓的编译器扩展,但是最好避免使用非标准特性。
理解复杂的数组声明
int arr[10];
int *ptrs[10]; //ptrs是含有10个整型指针的数组
int &refs[10] =/* ?*/; //错误:不存在引用的数组
int(*Parray)[10] = &arr;//Parray指向一个含有10个整数的数组
int(&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
3.5.1 节练习
(a)(c)(d)都是非法的,(b)合法,
练习3.28:相比于 vector 来说,数组有哪些缺点,请列举一些。
数组的大小固定不变,不能随意向数组终止增加额外的元素,虽然在某些情境下运行时性能较好,但是与 vector 相比损失了灵活性。
具体来说,数组的维度在定义时就已经确定,如果我们想更改数组的长度,只能创建一个更大的数组,然后将原数组的所有元素复制到新数组中去。我们也无法像 vector 那样使用 size 函数直接获取数组的维度。如果是字符数组,可以调用 strlen 函数得到字符串的长度; 如果是其他数组,只能使用 sizeof(array)/sizeof(array[0]) 的方式计算数组的维度。
3.5.2 访问数组元素
数组的元素也能使用范围 for 语句 或下标运算符来访问。数组的索引从0开始。
在使用数组下标的时候,通常将其定义为 size_t 类型。size_t 是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。
检查下标的值
下标应该大于等于0而且小于数组的大小,防止数组下标越界。
3.5.2 节练习
//练习3.31:编写一段程序,定义一个含有10个 int 的数组,令每个元素的值就是其下标值
int arr[10];
vector<int> v;
for (int i = 0; i < 10; ++i)
{
arr[i] = i;
v.push_back(i);
}
cout << "数组中的元素为:";
for (auto n : arr)
{
cout << n << " ";
}
cout << endl;
//练习3.32:将上题创建的数组拷贝给另一个数组
int arr2[10];
vector<int> v2(v); //也可使用 for 循环赋值 v2.push_back(v[i]);
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i)
{
arr2[i] = arr[i];
}
cout << "v2中的元素为:";
for (auto c : v2)
{
cout << c << " ";
}
3.5.3 指针和数组
在C++ 语言中,指针和数组有非常紧密的联系。使用数组的时候,编译器一般会把它转换成指针。
通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针。
string nums[] = { "one","two","three" };
string *p = &nums[0];//p指向 nums 的第一个元素
然而,数组还有一个特性:在很多用到数组名字的地方,编译器都会自动将其替换为一个指向数组首元素的指针。
string *p2 = nums;//等价于 p2=&nums[0]
指针也是迭代器
可以使用指针遍历数组中的元素。这样做的前提是先得获取到指向数组第一个元素的指针和指向数组尾元素的下一位置的指针。
int *e = &arr[10];//指向 arr 尾元素的下一位置的指针 arr有10个元素
for(int *b = arr; b != e; ++b)
{
cout<<*b<<endl;//输出 arr 的元素
}
标准库函数 begin 和 end
上面我们通过计算得到了尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,C++11 新标准引入了两个名为 begin 和 end 的函数。使用方法如下:
int ia[] = { 0,1,2,3,4,5,6,7,8,9 };
int *beg = begin(ia); // 指向ia 首元素的指针
int *last = end(ia); //指向ia 尾元素下一位置的指针
一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与 vector 的 end 函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。
指针运算
指针的运算包括:解引用、递增、比较、与整数相加、两个指针相减等。
解引用和指针运算的交互
指针加上一个整数所得的结果还是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针。
下标和指针
内置的下标运算符所用的索引值不是无符号类型。
3.5.3 节练习
//练习3.35:编写一段程序,利用指针将数组中的元素置为0
int ia[] = { 0,1,2,3,4,5,6,7,8,9 };
cout << "修改前:";
for (auto n : ia)
{
cout << n << " ";
}
cout << endl;
for (int *beg = begin(ia); beg != end(ia); ++beg)
{
(*beg) = 0;
}
cout << "修改后:";
for (auto n : ia)
{
cout << n << " ";
}
//练习3.36:编写一段程序,比较两个数组是否相等
#include<iostream>
#include<ctime>
using namespace std;
int main()
{
const int sz = 5;
int a[sz], b[sz], i;
srand((unsigned)time(NULL));
//通过 for 循环为数组元素赋值
for (i = 0; i < sz; ++i)
{
//每次循环生成一个10以内的随机数并添加到 a 中
a[i] = rand() % 10;
}
cout << "系统数据已生成,请输入您猜测的5个数字(0-9),可以重复:" << endl;
int uVal;
//通过 for 循环为数组元素赋值
for (i = 0; i < sz; ++i)
{
if (cin >> uVal)
{
b[i] = uVal;
}
}
cout << "系统生成的数据是:" << endl;
for (auto v : a)
{
cout << v << " ";
}
cout << endl;
cout << "您猜测的数据是:" << endl;
for (auto v : b)
{
cout <<v << " ";
}
cout << endl;
int *p = begin(a), *q = begin(b);
while (p != end(a) && q != end(b))
{
if (*p != *q)
{
cout << "猜测有误,两个数组不相等" << endl;
return -1;
}
p++;
q++;
}
cout << "猜对了,两个数组相等" << endl;
system("pause");
return 0;
}
3.5.4 C 风格字符串
尽管C++ 支持 C风格字符串,但在 C++ 程序中最好还是不要使用它们。
字符串字面值是一种通用结构的实例,这种结构即是 C++ 由 C 继承而来的 C 风格字符串(C-style character string)。C 风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated)。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符('\0')。一般利用指针来操作这些字符串。
C 标准库 String 函数
strlen(p) | 返回 p 的长度,空字符不计算在内 |
strcmp(p1,p2) | 比较 p1 和 p2 的相等性。如果 p1 == p2,返回0;如果 p1 > p2,返回一个正值;如果 p1 < p2 ,返回一个负值 |
strcat(p1,p2) | 将 p2 附加到 p1 之后,返回 p1 |
strcpy(p1,p2) | 将 p2 拷贝给 p1,返回 p1 |
传入此类函数的指针必须指向以空字符作为结束的数组。
char ca[] = { 'c','+','+' }; //不以空字符结尾
cout << strlen(ca) << endl; //错误,打印出来是15,加上空字符后打印出来是3
比较字符串
比较标准库 string 对象的时候,用的是普通的关系运算符和相等性运算符。
string s1 = "aaaa";
string s2 = "bbbb";
if(s1 < s2) //true s1 小于 s2
如果把这些运算符用在两个C 风格字符串上,实际比较的将是指针而非字符串本身:、
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if (ca1 < ca2) //未定义的:试图比较两个无关地址
谨记之前介绍过的,当使用数组的时候其实真正用的是指向数组首元素的指针。
要想比较两个C风格字符串,需要调用 strcmp 函数。
目标字符串的大小由调用者决定
总结:还是用标准库string
3.5.5 与旧代码的接口
任何出现字符串字面值的地方都可以用空字符结束的字符数组来替代:
- 允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值。
- 在 string 对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在 string 对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
上述的性质反过来就不成立了:如果程序的某处需要一个 C风格字符串,无法直接用 string 对象来代替它。例如,不能用 string 对象直接初始化指向字符的指针。为了完成该功能, string 专门提供了一个名为 c_str 的成员函数。
string s("hello world");
char *str = s; //错误:不能用 string 对象初始化 char*
const char *str = s.c_str();//正确
c_str 函数的返回值是一个C风格的字符串。也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个 string 对象的一样。结果指针的类型是 const char*,从而确保我们不会改变字符数组的内容。
如果执行完 c_str() 函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
使用数组初始化 vector 对象
不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用 vector 对象初始化数组。相反地,可以使用数组来初始化 vector 对象。
//使用数组初始化 vector 对象
int int_arr[] = { 0,1,2,3,4,5 };
vector<int> ivec(begin(int_arr), end(int_arr));
3.6 多维数组
严格来说,C++语言中没有多维数组,通常所说的多维数组其实就是数组的数组。
当一个数组的元素仍然是数组时,通常使用两个纬度来定义它:一个纬度表示本数组本身大小,另外一个纬度表示其元素(也是数组)大小。
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
//大小为10 的数组,它的每个元素都是大小为20 的数组,
//这些数组的元素是含有30个整数的数组
int arr[10][20][30] = { 0 };
对于二维数组来说,常把第一个纬度称作行,第二个纬度称作列。
多维数组的初始化
允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。
//初始化多维数组
int ia[3][4] = { //3个元素,每个元素都是大小为4的数组
{0,1,2,3}, //第1行的初始值
{4,5,6,7}, //第2行的初始值
{8,9,10,11} //第3行的初始值
};
其中内层嵌套的花括号并非必需的。下面是另一种写法。
//另一种写法
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11 };
多维数组的下标引用
可以使用下标运算符类访问多维数组的元素,此时数组的每个维度对应一个下标运算符。
//两层嵌套 for 循环遍历二维数组
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt];
//对于每一列
for (size_t i = 0; i != rowCnt; ++i)
{
//对于每一列
for (size_t j = 0; j < colCnt; ++j)
{
//将元素的位置索引作为它的值
ia[i][j] = i * colCnt + j;
}
}
使用范围 for 语句处理多维数组
在C++11 新标准中新增了范围 for 语句,所以前一个程序可以简化成如下的形式,
size_t cnt = 0;
for (auto &row : ia) //对于外层数组的每一个元素
{
for (auto &col : row) //对于内层数组的每一个元素
{
col = cnt; //将下一个值赋给该元素
++cnt; //将 cnt 加 1
}
}
注意,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,事实上,要使用范围 for 语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型,如果不用引用类型,编译器会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针,这样内层的循环就不合法了。
指针和多维数组
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针。
int ia[3][4];
int(*p)[4] = ia; // p 指向含有4个整数的数组
p = &ia[2]; // p 指向ia的尾元素
在上述声明中,圆括号必不可少。
int *ip[4]; //整型指针的数组
int(*ip)[4]; //指向含有4 个整数的数组
随着 C++11 新标准的提出,通过使用 auto 或 decltype 就能尽可能地避免在数组前面加上一个指针类型了。
类型别名简化多维数组的指针
读、写和理解一个指向多维数组的指针是一个让人不胜其扰的工作,使用类型别名可以简化这项工作。
using int_array = int[4]; //新标准下类型别名的声明
typedef int int_array[4]; //等价的 typedef 声明
3.6 节练习
//练习3.43:编写3个不同版本的程序,令其都能输出 ia 的元素
int ia[3][4] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
//第1种,使用范围 for 语句管理迭代过程
cout << "第1种,利用范围 for 语句输出多维数组的内容:" << endl;
for (int(&row)[4] : ia)
{
for (int col : row)
{
cout << col << " ";
}
cout << endl;
}
//第2种,使用普通的 for 语句,要求使用下标运算符
cout << "第2种,使用普通的 for 语句,要求使用下标运算符:" << endl;
for (int i = 0; i != 3; ++i)
{
for (int j = 0; j != 4; ++j)
{
cout << ia[i][j] << " ";
}
cout << endl;
}
//第 3 种,使用普通 for 语句,要求用指针
cout << "第 3 种,使用普通 for 语句,要求用指针:" << endl;
for (int(*p)[4] = ia; p != ia + 3; ++p)
{
for (int *q = *p; q != *p + 4; ++q)
{
cout << *q << " ";
}
cout << endl;
}
//练习3.44:使用类型别名
int ia[3][4] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
using int_array = int[4];
//第1种,使用范围 for 语句管理迭代过程
cout << "第1种,利用范围 for 语句输出多维数组的内容:" << endl;
for (int_array &row : ia)
{
for (int col : row)
{
cout << col << " ";
}
cout << endl;
}
//第2种,使用普通的 for 语句,要求使用下标运算符
cout << "第2种,使用普通的 for 语句,要求使用下标运算符:" << endl;
for (int i = 0; i != 3; ++i)
{
for (int j = 0; j != 4; ++j)
{
cout << ia[i][j] << " ";
}
cout << endl;
}
//第 3 种,使用普通 for 语句,要求用指针
cout << "第 3 种,使用普通 for 语句,要求用指针:" << endl;
for (int_array *p = ia; p != ia + 3; ++p)
{
for (int *q = *p; q != *p + 4; ++q)
{
cout << *q << " ";
}
cout << endl;
}
//练习3.45:使用auto 关键字
int ia[3][4] = { 0,1,2,3,4,5,6,7,8,9,10,11 };
//第1种,使用范围 for 语句管理迭代过程
cout << "第1种,利用范围 for 语句输出多维数组的内容:" << endl;
for (auto &row : ia)
{
for (int col : row)
{
cout << col << " ";
}
cout << endl;
}
//第2种,使用普通的 for 语句,要求使用下标运算符
cout << "第2种,使用普通的 for 语句,要求使用下标运算符:" << endl;
for (auto i = 0; i != 3; ++i)
{
for (auto j = 0; j != 4; ++j)
{
cout << ia[i][j] << " ";
}
cout << endl;
}
//第 3 种,使用普通 for 语句,要求用指针
cout << "第 3 种,使用普通 for 语句,要求用指针:" << endl;
for (auto *p = ia; p != ia + 3; ++p)
{
for (auto *q = *p; q != *p + 4; ++q)
{
cout << *q << " ";
}
cout << endl;
}