第3章 字符串、向量和数组
string
和vector
是两种最重要的标准库类型。string
支持可变长字符串,后者则表示可变长的集合。
迭代器,是 string 和 vector 的配套类型,常被用于访问 string 中的字符或 vector 中的元素。
3.1 命名空间的 using 声明
using
声明(using declaration)
std::cin
表示从标准输入中读取内容,::
为作用域操作符。
含义:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。std::cin
的意思就是要使用命名空间std
中的名字cin
。
头文件不应包含 using 声明
原因:因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
3.2 标准库类型 string
标准库类型string
表示可变长的字符序列。
3.2.1 定义和初始化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 组成的串 |
直接初始化和拷贝初始化
- 拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去
- 直接初始化(direct initialization),不使用等号
string s5 = "hiya";
// 拷贝初始化
string s6("hiya");
// 直接初始化
string s7(10, 'c');
// 直接初始化,s7 的内容是 ccccccc √ 可读性更好
string s8 = string(10, 'c');
// 拷贝初始化,s8 的内容是 cccccccc
注:s8 的拷贝初始化本质上等价于下面的两条语句:
string temp(10, ‘c’); // temp 的内容是 cccccccc
string s8 = temp; // 将 temp 拷贝给 s8
3.2.2 string 对象上的操作
表3.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 |
用s2 的副本代替s1 中原来的字符 |
s1 == s2 |
如果s1 和s2 中所含的字符完全一样,则它们相等; |
s1 != s2 |
string 对象的相等性判断对字母的大小写敏感 |
< , <= , > , >= |
利用字符在字典中的顺序进行比较,且对字母的大小写敏感 |
在执行读取操作时,string
对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的自负开始读起,直到遇见下一处空白为止。如果程序的输入是"Hello World!
",则输出将是“Hello
”。
/* 如果程序输入的内容为“ Hello World! ” */
/* 情况1:只能输出 Hello 的情况:*/
int main()
{
string s; // 空字符串
cin >> s; // 将 string 对象读入 s,遇到空白停止
cout << s << endl; // 输出 s
return 0;
}
/* 情况2:输出 HelloWorld! 的情况: */
int main()
{
string s1, s2;
cin >> s1 >> s2; // 把第一个输入读到 s1 中,第二个输入读到 s2 中
cout << s1 << s2 << endl; // 输出两个 string 对象
}
使用getline
读取一整行
使用情景:有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用 getline
函数代替原来的 >>
运算符。
getline
与换行符水火不容
getline
函数的参数是一个输入流和一个string
对象,
函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),
然后把所读的内容存入到那个string
对象中去(注意不存换行符)。
Note:触发getline
函数返回的那个换行符实际上被丢掉了,得到的string
对象中并不包含该换行符。
getline
只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符也是如此,此种情况下那么所得的结果是个空string。
我们也能用getline
的结果作为判断条件,可以让程序一次输出一整行,而不再是每行输出一个词了:
int main()
{
string line;
// 每次读入一整行,直至到达文件末尾
while (getline(cin, line))
cout << line << endl;
return 0;
}
因为line
中不包含换行符,所以我们手动地加上换行操作符。使用 endl
结束当前行并刷新显示缓冲区。
练习3.2:编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。
输入一整行:while (getline(cin, line))
输入一个词:while (cin >> word)
/* 练习3.2:编写一段程序从标准输入中一次读入一整行,然后修改该程序使其一次读入一个词。
【思路】:常用的字符串读取方式有两种:
(1) 使用`getline`函数一次读入一整行;
行的结束标识是回车符,如果一开始输入的就是回车符,则`getline`直接结束本次读取,所得的结果是一个空字符串
while (getline(cin, line))
(2) 使用`cin`一次读入一个词,遇空白停止。
while (cin >> word)
*/
// (1) 使用 getline 一次读入一整行,遇回车结束
#include <iostream>
#include <string>
using namespace std;
int main()
{
string line;
cout << "Please enter a string including space key: " << endl;
while (getline(cin, line))
cout << line << endl;
return 0;
}
// (2) 使用 cin 一次读入一个词,遇空白结束
#include <iostream>
#include <string>
using namespace std;
int main()
{
string word;
cout << "Please enter a word, without space: " << endl;
while (cin >> word)
cout << word << endl;
return 0;
}
练习 3.3:请说明 string
类的输入运算符和 getline
函数分别是如何处理空白字符的。
- 标准库 string 的输入运算符自动忽略字符串开头的空白(包括空格符、换行符、制表符等),从第一个真正的字符开始读起,直到遇见下一处空白为止。
- 如果希望在最终的字符串中保留输入时的空白符,应该使用 getline 函数代替原来的
>>
运算符,getline
从给定的输入流中读取数据,直到遇到换行符为止,此时换行符也被读取进来,但是并不存储在最后的字符串中。
string::size_type
类型
string::size_type
是一个无符号类型的值,而且能足够存放下任何 string 对象的大小。
注意:在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。
如果一条表达式中已经有了size()
函数就不要再使用int
了,这样可以避免混用int
和unsigned
可能带来的问题。
举例 P93:s.size 函数返回值的类型是 string::size_type
/* 使用范围 for 语句和 ispunct 函数来统计 string 对象中标点符号的个数:*/
string s("Hello Wolrd!!!");
// punct_cnt 的类型和 s.size 的返回类型一样
decltype(s.size()) punct_cnt = 0;
// 统计 s 中标点符号的数量
for (auto c : s) // 对于 s 中的每个字符
if (ispunct(c)) // 如果该字符是标点符号
++punct_cnt; // 将标点符号的计数值加 1
cout << punct_cnt << " punctuation characters in " << s << endl;
比较 string 对象
-
- 如果两个 string 对象的长度不同,而且较短 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 s4 = s1 + ", "; // 正确:把一个 string 对象和一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算对象都不是 string
string s6 = s1 + ", " + "world"; // 正确:每个加法运算符都有一个运算对象是 string
// s6 的初始化形式工作机理和连续输入连续输出是一样的,可以用如下的形式分组:
// string s6 = (s1 + ", ") + "world";
string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加
切记:字符串字面值与 string 是不同的类型。C++语言中的字符串字面值并不是标准库类型 string 的对象。
3.2.3 处理 string 对象中的字符
表3.3 | 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 |
建议:使用 C++ 版本的 C 标准库头文件。比如cctype
头文件和ctype.h
头文件的内容是一样的,只不过从命名规范上来讲更符合 C++ 语言的要求。
处理每个字符?使用基于范围的for
语句
范围for
(range for)语句,这种语句遍历给定序列中的每个元素并对序列中的每个值执行某种操作:
expression 表示一个序列,declaration 定义一个变量,该变量将被用于访问序列中的基础元素。
每次迭代,declaration 部分的变量会被初始化为 expression 部分的下一个元素。
for (declaration : expression)
statement
举例:使用范围 for 语句把 string 对象中的字符每行一个输出出来:
string str("some string");
// 每行输出 str 中的一个字符
for (auto c : str) // 对于 str 中的 每个字符
cout << c << endl; // 输出当前字符,后面紧跟着一个换行符
使用范围 for 语句改变字符串中的字符
如果想要改变 string 对象中字符的值,必须把循环变量定义成引用类型。
string s("Hello Wolrd!");
// 转换成大写形式。
for (auto &c : s) // 对于 s 中的每个字符(注意:c 是引用)
c = toupper(c); // c 是一个引用,因此赋值语句将改变 s 中字符的值
cout << s << endl;
练习3.8:分别用 while 循环和传统的 for 循环重写 范围for循环 的程序。
/* 练习3.8:分别用 while 循环和传统的 for 循环重写 范围for循环 的程序。
你觉得哪种形式更好呢?为什么? */
/*--------------------------*/
// 使用 范围for循环
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cout << "Please enter a string, including space: " << endl;
getline(cin, s); // 读取整行,遇回车符结束
for (auto &c : s) // 依次处理字符串中的每一个字符
{
c = 'X';
}
cout << s << endl;
return 0;
}
/*--------------------------*/
// 使用传统 for 循环实现的程序如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cout << "Please enter a string, including space: " << endl;
getline(cin, s);
for (unsigned int i = 0; i < s.size(); i++) // 使用 unsigned int i
{
s[i] = 'X';
}
cout << s << endl;
return 0;
}
/*--------------------------*/
// 使用 while 循环实现的程序如下:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s;
cout << "Please enter a string, including space: " << endl;
getline(cin, s);
int i = 0;
while(s[i] != '\0') // 编译器在每个字符串的结尾处添加一个空字符(`'\0'`)
{
s[i] = 'X';
++i;
}
cout << s << endl;
return 0;
}
/*在本例中,我们希望处理字符串中的每一个字符,且无需在意字符的处理顺序,
因此与传统的 while 循环和 for 循环相比,使用范围 for 循环更简洁直观。*/
练习3.11:下面的范围for语句合法吗?如果合法,c的类型是什么?
/* 练习3.11:下面的范围for语句合法吗?如果合法,c的类型是什么?*/
const string s = "Keep out!";
for (auto &c : s)
{
c = 'X';
}
解答:语法上来说是合法的,s
是一个常量字符串,则c
的推断类型是常量引用,即c
所绑定的对象值不能改变。
由于c
是绑定到常量的引用,其值不能改变。否则编译器会报错。
只处理一部分字符?
访问 string 对象中的单个字符有两种方式:
-
- 使用下标
-
- 使用迭代器 (将在3.4节P95 和第9章中介绍)
下标运算符([]
),接受的输入参数是 string::size_type
类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。例:
/*将字符串的首字符改成大写形式*/
string s("some string");
if (!s.empty()) // 确保 s[0] 的位置确实有字符
s[0] = toupper(s[0]); // 为 s 的第一个字符赋一个新值
练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。
/* 练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。*/
// 思路一:利用范围for语句遍历字符串,逐个输出非标点字符:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s;
cout << "Please enter a string including punctuations: " << endl;
getline(cin, s);
for (auto c : s)
{
if (!ispunct(c))
cout << c;
}
cout << endl;
return 0;
}
// 思路二:利用普通 for 循环遍历字符串,通过下标执行随机访问,把非标点字符拼接成一个新串后输出:
#include <iostream>
#include <string>
#include <cctype>
using namespace std;
int main()
{
string s, result;
cout << "Please enter a string including punctuations: " << endl;
for (decltype(s.size()) i = 0; i < s.size(); i++)
{
if (!ispunct(s[i])) // 下标运算符
result += s[i];
}
cout << result << endl;
return 0;
}
使用下标执行迭代
for
循环使用变量index
作为s
的下标,index
的类型是由decltype
关键字决定的。
/* 依次处理 s 中的字符直至我们处理完全部字符或者遇到一个空白 */
for (decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]);
++index)
s[index] = toupper(s[index]); // 将当前字符改成大写形式
提示:注意检查下标的合法性:下标必须大于等于 0 而小于字符串的 size() 的值。一种简便易行的方法是,总是设下标的类型为 string::size_type
,因此此类型是无符号数,可以确保下标不会小于 0。此时,代码只需保证下标小于 size() 的值就可以了。
使用下标执行随机访问
/* 编写一个程序把 0 到 15 之间的十进制数转换成对应的十六进制形式,
只需初始化一个字符串令其存放 16 个十六进制“数字” */
const string hexdigits = "0123456789ABCDEF"; // 可能的十六进制数字
cout << "Enter a series of numbers between 0 and 15 separated by spaces."
<< "Hit ENTER when finsihed: "
<< endl;
string result; // 用于保存十六进制的字符串
string::size_type n; // 用于保存从输入流读取的数
while (cin >> n)
if (n < hexdigits.size()) // 忽略无效输入确保输入的数小于16
result += hexdigits[n]; // 得到对应的十六进制数字
cout << "Your hex number is: " << result << endl;
hexdigits[n]
的值就是 hexdigits 内位置n
处的字符。例如,如果 n 是 15,则结果是 F
;如果 n 是 12,则结果是 C
。
下标n
是string::size_type
类型,也就是无符号类型,所以n
可以确保大于或等于 0。
3.3 标准库类型 vector
标准库类型vector
表示对象的几何,其中所有对象的类型都相同。因为vector
“容纳着”其他对象,所以它也被称作容器(container)。
C++语言既有类模板(class template),也有函数模板,其中vector
是一个类模板。
模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一分说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或模板实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式:在模板名字后面跟一对尖括号,在括号内放上信息。
vector<int> ivec; // ivec 保存 int 类型的对象
vector<Sales_item> Sales_vec; // 保存 Sales_item 类型的对象
vector<vector<string>> file; // 该向量的元素是 vector 对象
3.3.1 定义和初始化 vector 对象
表3.4 | 初始化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<int> ivec; // 初始状态为空
vector<int> ivec2(ivec); // 把 ivec 的元素拷贝给 ivec2
vector<int> ivec3 = ivec; // 把 ivec 的元素拷贝给 ivec3
vector<string> svec(ivec2); // 错误:svec的元素是string对象,不是int
练习3.12:下列 vector 对象的定义有不正确的吗?
/* 练习3.12:下列 vector 对象的定义有不正确的吗?
如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。*/
(a) vector<vector<int>> ivec;
// 正确。定义了一个名为 ivec 的 vector 对象,其中的每个元素都是 vector<int> 对象。
(b) vector<string> svec = ivec;
// 错误。svec 的元素类型是 string,而 ivec 的元素类型是 int,因此不能使用 ivec 初始化 svec。
(c) vector<string> svec(10, "null");
// 正确。定义了一个名为 svec 的 vector 对象,其中含有 10 个元素,每个元素都是字符串 null。(null 是一个 int 变量,但这里写的是 "null",是字符串)。
列表初始化vector
对象 P88
列表初始化:用花括号括起来 0 个或多个厨师元素值被赋给 vector 对象:
vector<string> articles = {"a", "an", "the"};
回顾:C++定义了初始化的几种不同形式:P39
其中用花括号来初始化变量的这种初始化形式被称为列表初始化(list initialization)。
int units_sold = 0;
int units_sold = {
0};
int units_sold{
0};
int units_sold(0);
列表初始化 vector 对象,特殊要求是:如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
vector<string> v1{
"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误
创建指定数量的元素
还可以用vector
对象容纳的元素数量和所有元素的统一初始值来初始化 vector 对象:
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 个元素,每个都初始化为 0<