C++ Primer 第五版:第三章「字符串、向量和数组」习题答案
第三章:字符串、向量和数组
练习 3.1
使用恰当的 using 声明重做 1.4.1 节(第 11 页)和 2.6.2 节(第 67 页)的练习。
因为较为简单,每个小节就只写一个练习了。
using 声明易造成名字冲突:
- 使用 using 声明,一般建议为每个名字做独立的 using 声明,例如
using std::cin
,不太建议使用整个命名空间,例如using namespace std
,易造成名字冲突。 - 头文件不应包含 using 声明:易造成名字冲突。
练习 3.2
编写一段程序从标准输入中一次读入一行,然后修改该程序使其一次读入一个词。
练习 3.3
请说明 string 类的输入运算符和 getline 函数分别是如何处理空白字符的。
- 类似 cin >> str 的读取,string 对象会忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。
- 类似 getline (cin, str) 的读取,string 对象会从给定的输入流中读取内容,直到遇到换行符为止(换行符被读进来,但不会存放进对象)。
练习 3.4
编写一段程序读取两个字符串,比较其是否相等并输出结果。如果不相等,输出较大的那个字符串。改写上述程序,比较输入的两个字符串是否等长,如果不等长,输出长度较大的那个字符串。
练习 3.5
编写一段程序从标准输入中读入多个字符串并将它们连接在一起,输出连接成的大字符串。然后修改上述程序,用空格把输入的多个字符串分隔开来。
string 对象操作的一些总结:
- 定义、初始化和基本操作看下面代码(书本见 P76 和 P77)。
- 关系运算符 <、<=、>、>= 比较规则:依次比较每个位置上的字符大小,若都一样时,则长度更长的字符串就更大。
string
和字符字面值和字符串字面值混在一条语句中,必须确保加法运算符「+」两侧至少有一个string
对象(因为标准库可以将字面值转换为string
对象,但不允许两个字面值直接相加)size()
函数返回的是string::size_type
类型的值,是无符号数。表达式中如果有size()
,切记不要再使用int
。如果需要定义变量存储size()
函数,使用auto
或decltype
,例如auto len = str.size()
- 注意字符串字面值不是
string
对象,和string
是不同的类型(因为需要兼容 C)。
1 | // 定义和初始化(等号是拷贝初始化,圆括号是直接初始化) |
练习 3.6
编写一段程序,使用范围 for 语句将字符串内的所有字符用 X 代替。
for 范围语句中引用的理解:
1 | for (auto &c : s) |
练习 3.7
就上一题完成的程序而言,如果将循环控制变量的类型设为 char 将发生什么?先估计一下结果,然后实际编程进行验证。
字符串没有变化(因为 c 只是拷贝,即 char,而非引用,即 char &,所以无法改变字符串的)。
1 | string str("hello, world!"); |
练习 3.8
分别用 while 循环和传统 for 循环重写第一题的程序,你觉得哪种形式更好呢?为什么?
范围 for 语句更好,因为不需要直接操作索引,更简洁。
练习 3.9
下面的程序有何作用?它合法吗?如果不合法,为什么?
1 | string s; |
不合法。使用下标访问空字符串是非法的行为(通过下标访问不存在元素的行为会导致缓冲区错误 buffer overflow,本质上就是下标越界,试图访问非法内存区域)。
只能对确知已存在的元素执行下标操作:建议设下标类型为 decltype(str.size())
,这样可以确保下标不会小于 0。写程序逻辑时,自己确保下标小于 str.size()
即可(或者使用范围 for 语句遍历更方便,能有效地确保下标合法,不会出现下标越界)。
练习 3.10
编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。
C++ 标准库兼容 C 标准库
C 标准库头文件形如 ctype.h,而在 C++ 中,则是命名为 cctype,即前面加一个 c 并且去除后缀.h。使用方式,标准库的名字可以在命名空间 std 找到。
练习 3.11
下面的范围 for 语句合法吗?如果合法,c 的类型是什么?
1 | const string s = "Keep out!"; |
要根据 for 循环体中的代码来看是否合法,s 是常量,c 是 string 对象中字符的引用(const char &)。因此如果 for 循环体中的代码重新给 c 赋值就会非法,如果不改变 c 的值,那么合法。
1 | cout << c; // 合法 |
练习 3.12
下列 vector 对象的定义有不正确的吗?如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。
1 | vector<vector<int>> ivec; // 在C++11当中合法,ivec的元素是vector对象 |
练习 3.13
下列的 vector 对象各包含多少个元素?这些元素的值分别是多少?
1 | vector<int> v1; // 0个,无值 |
初始化的括号区别:
- 圆括号:构造 vector 对象,即圆括号声明容量和初值。
- 花括号:列表初始化,用花括号的值作为元素初始值。(当提供的值不能用来列表初始化时,就会用来构造对象。如
vector<string> v1{10, "hi"};
,因为类型不同,所以构造成 10 个 "hi")
练习 3.14
编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。
vctror 对象能高效增长,在定义时设定其大小意义不大。开始时创建空 vector,运行时再动态添加元素。
练习 3.15
改写上题的程序,不过这次读入的是字符串。
练习 3.16
编写一段程序,把练习 3.13 中 vector 对象的容量和具体内容输出出来。检验你之前的回答是否正确,如果不对,回过头重新学习 3.3.1 节(第 87 页)知道弄明白错在何处为止。
练习 3.17
从 cin 读入一组词并把它们存入一个 vector 对象,然后设法把所有词都改写为大写形式。输出改变后的结果,每个词占一行。
练习 3.18
下面的程序合法吗?如果不合法,你准备如何修改?
1 | vector<int> ivec; |
不合法(ivec 是空 vector 对象,对其执行下标操作是非法行为)。修正方式:
1 | ivec.push_back(42); |
练习 3.19
如果想定义一个含有 10 个元素的 vector 对象,所有元素的值都是 42,请列举出三种不同的实现方法,哪种方式更好呢?为什么?
1 | vector<int> ivec1(10, 42); |
第一种方式最好。写法简洁。
练习 3.20
读入一组整数并把它们存入一个 vector 对象,将每对相邻整数的和输出出来。改写你的程序,这次要求先输出第 1 个和最后 1 个元素的和,接着输出第 2 个和倒数第 2 个元素的和,以此类推。
练习 3.21
请使用迭代器重做 3.3.3 节(第 94 页)的第一个练习。
迭代器的总结(本质上就是指针):
- 类似于指针,解引用运算符 *,可以获取迭代器指向的内容。
- 使用迭代器的循环体是用
!=
作为判断条件(像 c 或者 java 使用下标,多是用<
)。因为 C++ 中标准库容器的迭代器都定义了==
和!=
,而只有 string 和 vector 某些库才有下标运算符。 - 容器为空时,begin 和 end 返回的是同一个迭代器(尾后迭代器)。
- 尾后迭代器指示的是不存在的 “尾后” 元素,实际并不指示某个元素,因此不能进行递增或者解引用操作。
- 只需读操作时,可用 cbegin 和 cend 函数。
- 箭头运算符是结合解引用和成员访问两个操作,即
it->men
等同于(*it).men
。 - 使用了迭代器的循环体,不要向其所属的容器添加元素。例如,在循环体中向 vector 对象 push_back,这会导致迭代器失效。(类似的,也不能在范围 for 循环中向 vector 对象添加元素)
练习 3.22
修改之前那个输出 text 第一段的程序,首先把 text 的第一段全都改成大写形式,然后输出它。
练习 3.23
编写一段程序,创建一个含有 10 个整数的 vector 对象,然后使用迭代器将所有元素的值都变成原来的两倍。输出 vector 对象的内容,检验程序是否正确。
练习 3.24
请使用迭代器重做 3.3.3 节(第 94 页)的最后一个练习。
练习 3.25
3.3.3 节(第 93 页)划分分数段的程序是使用下标运算符实现的,请利用迭代器改写该程序并实现完全相同的功能。
练习 3.26
在 100 页的二分搜索程序中,为什么用的是 mid = beg + (end - beg) / 2,而非 mid = (beg + end) / 2 ; ?
因为两个迭代器之间支持的运算符只有 -
和 >、>=、<、<=
,而没有 +
。end - beg
相减的结果是之间的距离,将之除以 2 然后与 beg 相加,表示将 beg 移动到一半的位置。
迭代器运算的理解:迭代器可以加减一个数(包括加减复合赋值运算符),但两个迭代器之间的运算符只有 -
和 >、>=、<、<=
关系运算符。
练习 3.27
假设 txt_size 是一个无参数的函数,它的返回值是 int。请回答下列哪个定义是非法的?为什么?
1 | unsigned buf_size = 1024; |
- (a) 非法。纬度必须是一个常量表达式。
- (b) 合法。
- (c) 非法。txt_size 返回的是 int,不是 constexpr int。
- (d) 非法。数组的大小应该是 12,字符串字面值末尾还有个空字符 '\0'。
练习 3.28
下列数组中元素的值是什么?
1 | string sa[10]; |
sa 的元素值全部为空字符串,ia 的元素值全部为 0。sa2 的元素值全部为空字符串,ia2 的元素没有被初始化,初值全部未定义。
练习 3.29
相比于 vector 来说,数组有哪些缺点,请列举一些。
- 数组的大小是固定的,不能随意增加元素
- 不允许拷贝和赋值
- 没有像 vector 那样的 API
练习 3.30
指出下面代码中的索引错误。
1 | constexpr size_t array_size = 10; |
当 ix 等于 10 时,表达式 ia [ix] 属于未定义行为(UB),下标越界。
通常将数组下标类型定义 size_t,size_t 定义在 cstddef 头文件,是一种机器相关的无符号类型,可以表示内存中任意对象的大小(因为被设计得足够大)。
练习 3.31
编写一段程序,定义一个含有 10 个 int 的数组,令每个元素的值就是其下标值。
练习 3.32
将上一题刚刚创建的数组拷贝给另外一个数组。利用 vector 重写程序,实现类似的功能。
练习 3.33
对于 104 页的程序来说,如果不初始化 scores 将会发生什么?
数组中所有元素的值将会未定义(有可能不是 0,会有脏数据存在)。
练习 3.34
假定 p1 和 p2 都指向同一个数组中的元素,则下面程序的功能是什么?什么情况下该程序是非法的?
1 | p1 += p2 - p1; |
将 p1 移动到 p2 的位置,即指向同一个地址。只要 p1、p2 值合法,该程序任何情况下都合法。
练习 3.35
编写一段程序,利用指针将数组中的元素置为 0。
数组与指针的总结:
- 数组名可以当作是首指针(编译器会自动将其转换成一个指向数组首元素的指针(函数传参、赋值给指针、表达式),但本质上数组名自身不是指针)
- 类似迭代器,也可以获取数组的首尾指针(假设数组 int array [10]={xxx}。类似尾迭代器,注意尾指针不指向具体元素,不能对其进行解引用或递增操作)
int *begin = array; int *end = &array[10];
int *begin = begin(array); int *end = end(array);
begin 和 end 函数定义在 iterator 头文件(数组不是类类型,没有类似 vector 迭代器那种成员函数)
- 两个指针相减的结果类型是 ptrdiff_t,是一种带符号类型,和 size_t 一样也定义在 cstddef 头文件。
- 只要指针指向数组元素(或者尾元素的下一位置,即尾指针),都可以执行下标运算(当然,需要保证运算后的指针也是指向同一数组或者尾指针)。像数组这种 C++ 语言内置的下标运算允许处理负值,但像标准库类型 string 和 vector 则限定下标是无符号类型。
1 | int *p = &array[2]; // p指向索引为2的元素 |
练习 3.36
编写一段程序,比较两个数组是否相等。再写一段程序,比较两个 vector 对象是否相等。
练习 3.37
下面的程序是何含义,程序的输出结果是什么?
1 | const char ca[] = { 'h', 'e', 'l', 'l', 'o' }; |
会将 ca 字符数组中的元素打印出来,但是因为没有空字符 '\0' 的存在,循环不会终止,会打印出来很多无意义脏数据。
练习 3.38
在本节中我们提到,将两个指针相加不但是非法的,而且也没什么意义。请问为什么两个指针相加没什么意义?
指针本质就是地址,将两个指针相减可以表示两个指针 (在同一数组中) 的距离,将指针加上一个整数也可以表示移动这个指针到某一位置。但是两个指针相加后的地址并没有逻辑上的意义。
练习 3.39
编写一段程序,比较两个 string 对象。再编写一段程序,比较两个 C 风格字符串的内容。
C 标准库 string 函数(头文件 cstring)
- 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
注意这些函数的指针必须指向以空字符作为结束的数组
1 | char ca[] = {'a', 'b'} // 没有以空字符结束。 |
练习 3.40
编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前面两个数组连接后的结果。使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组当中。
练习 3.41
编写一段程序,用整型数组初始化一个 vector 对象。
练习 3.42
编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。
使用数组初始化 vector 对象(不能用数组初始化数组;不能用 vector 对象初始化数组)
1 | int array[] = {1, 2, 3, 4, 5}; |
混用 string 对象和 C 风格字符串
- 允许使用以空字符结束的字符数组来初始化或赋值给 string 对象
- string 对象的加法运算允许以空字符结束的字符数组作为其中一个运算对象(加法运算符「+」两侧至少有一个 string 对象)
- string 复合赋值运算中允许以空字符结束的字符数组作为右侧运算对象
- string 对象不能直接初始化指向字符的指针,需要使用成员函数
c_str
返回指针,如const char *str = s.c_str();
练习 3.43
编写 3 个不同版本的程序,令其均能输出 ia 的元素。版本 1 使用范围 for 语句管理迭代过程;版本 2 和版本 3 都使用普通 for 语句,其中版本 2 要求使用下标运算符,版本 3 要求使用指针。此外,在所有 3 个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字和 decltype 关键字。
练习 3.44
改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。
练习 3.45
再一次改写程序,这次使用 auto 关键字。
多维数组和指针的总结
1 | // 多维数组本质上是数组的数组,初始化时内层嵌套的花括号也可去掉 |