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() 函数,使用 autodecltype,例如 auto len = str.size()
  • 注意字符串字面值不是 string 对象,和 string 是不同的类型(因为需要兼容 C)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义和初始化(等号是拷贝初始化,圆括号是直接初始化)
string s1; //默认初始化为空串
string s2(s1); // s2是s1的副本
string s2 = s1; // 同上,不过是拷贝初始化
string s3 = ("hello"); // s3是字面值"hello"的副本(不包括字面值中的空字符)
string s3 = "hello"; // 同上
string s4(n, 'c'); // s4初始化为由n个字符c组成的串

// 基本操作
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个字符的引用,下标从0计起。
s1 + s2; // 返回s1和s2连接后的结果
s1 = s2; // 用s2的副本代替s1中原来的字符
s1 == s2; // 判断两个string对象是否相等,对字母大小写敏感
s1 != s2;
<, <=, >, >= ; // 利用字符在字典中的顺序进行比较,对字母大小写敏感

练习 3.6

编写一段程序,使用范围 for 语句将字符串内的所有字符用 X 代替。

for 范围语句中引用的理解:

1
2
3
4
5
6
7
for (auto &c : s)
c = 'X';
// 等价于
for (auto itr = s.begin(); itr != s.end(); ++itr) {
auto &c = *itr; // 每个迭代中用一个新引用绑定一个元素
c = 'X';
}

练习 3.7

就上一题完成的程序而言,如果将循环控制变量的类型设为 char 将发生什么?先估计一下结果,然后实际编程进行验证。

字符串没有变化(因为 c 只是拷贝,即 char,而非引用,即 char &,所以无法改变字符串的)。

1
2
3
string str("hello, world!");
for (char c : str)
c = 'X';

练习 3.8

分别用 while 循环和传统 for 循环重写第一题的程序,你觉得哪种形式更好呢?为什么?

范围 for 语句更好,因为不需要直接操作索引,更简洁。

练习 3.9

下面的程序有何作用?它合法吗?如果不合法,为什么?

1
2
string s;
cout << s[0] << endl;

不合法。使用下标访问空字符串是非法的行为(通过下标访问不存在元素的行为会导致缓冲区错误 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
2
const string s = "Keep out!";
for(auto &c : s){ /* ... */ }

要根据 for 循环体中的代码来看是否合法,s 是常量,c 是 string 对象中字符的引用(const char &)。因此如果 for 循环体中的代码重新给 c 赋值就会非法,如果不改变 c 的值,那么合法。

1
2
cout << c;  // 合法
c = 'X'; // 不合法

练习 3.12

下列 vector 对象的定义有不正确的吗?如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。

1
2
3
vector<vector<int>> ivec;         // 在C++11当中合法,ivec的元素是vector对象
vector<string> svec = ivec; // 不合法,类型不一样,ivec的元素是vector int,svec的元素是string
vector<string> svec(10, "null"); // 合法,svec有10个字符串:"null"

练习 3.13

下列的 vector 对象各包含多少个元素?这些元素的值分别是多少?

1
2
3
4
5
6
7
vector<int> v1;         // 0个,无值
vector<int> v2(10); // 10个,都是0
vector<int> v3(10, 42); // 10个,都是42
vector<int> v4{ 10 }; // 1个,10
vector<int> v5{ 10, 42 }; // 2个,10和42
vector<string> v6{ 10 }; // 10个,空串
vector<string> v7{ 10, "hi" }; // 10个,都是"hi"

初始化的括号区别:

  • 圆括号:构造 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
2
vector<int> ivec;
ivec[0] = 42;

不合法(ivec 是空 vector 对象,对其执行下标操作是非法行为)。修正方式:

1
ivec.push_back(42);

练习 3.19

如果想定义一个含有 10 个元素的 vector 对象,所有元素的值都是 42,请列举出三种不同的实现方法,哪种方式更好呢?为什么?

1
2
3
4
5
vector<int> ivec1(10, 42);
vector<int> ivec2{ 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 };
vector<int> ivec3;
for (int i = 0; i < 10; ++i)
ivec3.push_back(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
2
3
4
5
unsigned buf_size = 1024;
(a) int ia[buf_size];
(b) int ia[4 * 7 - 14];
(c) int ia[txt_size()];
(d) char st[11] = "fundamental";
  • (a) 非法。纬度必须是一个常量表达式。
  • (b) 合法。
  • (c) 非法。txt_size 返回的是 int,不是 constexpr int。
  • (d) 非法。数组的大小应该是 12,字符串字面值末尾还有个空字符 '\0'。

练习 3.28

下列数组中元素的值是什么?

1
2
3
4
5
6
string sa[10];
int ia[10];
int main() {
string sa2[10];
int ia2[10];
}

sa 的元素值全部为空字符串,ia 的元素值全部为 0。sa2 的元素值全部为空字符串,ia2 的元素没有被初始化,初值全部未定义。

练习 3.29

相比于 vector 来说,数组有哪些缺点,请列举一些。

  • 数组的大小是固定的,不能随意增加元素
  • 不允许拷贝和赋值
  • 没有像 vector 那样的 API

练习 3.30

指出下面代码中的索引错误。

1
2
3
4
constexpr size_t array_size = 10;
int ia[array_size];
for (size_t ix = 1; ix <= array_size; ++ix)
ia[ix] = ix;

当 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
2
3
int *p = &array[2]; // p指向索引为2的元素
int j = p[1]; // p[1]等价于*(p+1),即array[3]
int k = p[-2]; // 即array[0]

练习 3.36

编写一段程序,比较两个数组是否相等。再写一段程序,比较两个 vector 对象是否相等。

练习 3.37

下面的程序是何含义,程序的输出结果是什么?

1
2
3
4
5
6
const char ca[] = { 'h', 'e', 'l', 'l', 'o' };
const char *cp = ca;
while (*cp) {
cout << *cp << endl;
++cp;
}

会将 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
2
3
char ca[] = {'a', 'b'} // 没有以空字符结束。
cout << strlen(ca) << endl; // 严重错误,没有空字符,该函数有可能在内存中不断向前寻找,直至找到空字符。
// 修正方式:用字符串字面值给ca赋初值"ab"。

练习 3.40

编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前面两个数组连接后的结果。使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组当中。

练习 3.41

编写一段程序,用整型数组初始化一个 vector 对象。

练习 3.42

编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。

使用数组初始化 vector 对象(不能用数组初始化数组;不能用 vector 对象初始化数组)

1
2
3
int array[] = {1, 2, 3, 4, 5};
vector<int> v(begin(array), end(array)); // 使用首尾指针
vector<int> subv(array + 1, array + 3); // 数组中的部分也可用来初始化

混用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 多维数组本质上是数组的数组,初始化时内层嵌套的花括号也可去掉
int ia[2][2] = {{1, 2},
{1, 3}};

// 使用范围for语句处理多维数组时,要将控制变量声明为引用类型(为避免编译器将其转换成指针)
for (auto &row : ia)
for (auto &col : row)
;

// 定义指向多维数组的指针注意写法(手动写较易写错,建议使用auto或者decltype)
int *ip[2]; // 整型指针的数组
int(*ip)[2]; // 指针,指向含有2个整数的数组
int(&ip)[2] = ia[0]; // 引用,指向含有2个整数的数组
// 类型别名简化写法,下面两行等同于int(*ip)[2];
using int_array = int[2];
int_array *ip;
本文结束 感谢阅读
Adios!
许可注意: 若想对本作品进行转载、引用亦或是进行二次创作时,请详细阅读上述相关协议内容(若不理解,请点击链接跳转阅读)。为保障本人权利,对于违反者,本人将依法予以处理!同时会向搜索引擎提交 DMCA 的投诉申请。望周知!—— Mr. Kin
勘误声明: 虽本人写作时已尽力保证其内容的正确性,但因个人知识面和经验的局限性以及计算机技术等相关技术日新月异,本作品内容或存在一些错误之处。欢迎联系我以更正错误,不胜感激!—— Mr. Kin
侵权声明: 若本站采用的第三方内容侵犯了你的版权,请联系我进行处理,谢谢!—— Mr. Kin
免责声明: 根据中国《计算机软件保护条例》第十七条规定:“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。” 本站分享的任何逆向破解软件,版权所有者归原软件著作权人。仅供个人使用或学习研究,严禁商业或非法用途,严禁用于打包恶意软件推广,否则后果由用户承担责任,特此说明。—— Mr. Kin
靓仔 / 美女,不考虑支持一下我吗?谢谢鼓励!(๑・̀ㅂ・́)و✧
Mr. Kin 微信 微信
Mr. Kin 支付宝 支付宝
Mr. Kin 领取支付宝红包 领取支付宝红包