c++ primer 笔记:字符串、向量和数组及练习题解

命名空间的 using 声明

目前使用的库函数基本属于命名空间 std,如 std::cin,意思是使用命名空间 std 中的名字 cin。
上面的方法繁琐,可以使用 **using声明(using declaration)**这种简单途径,有了 using 声明,就不用声明前缀了也能使用所需的名字,using声明形式如下:

using namespace::name;

声明后就可以直接访问命名空间中的名字:

#include <iostream>
// using declaration; when we use the name cin, we get the one from the namespace std
using std::cin;
int main()
{
 int i;
 cin >> i; // ok: cin is a synonym for std::cin
 cout << i; // error: no using declaration; we must use the full name
 std::cout << i; // ok: explicitly use cout from namepsace std
 return 0;
}

每个名字都需要独立的 using 声明,cout,cin,endl等。

头文件中不应包含 using 声明

因为所有包含头文件的文件都会有头文件中的声明,可能引起名字冲突。

练习 3.1:使用恰当的 using 声明重做 1.4.1 节 和 2.6.2 节 的练习。

答:
1.4.1的1.9:

#include <iostream>
// 使用过using声明使得cout和endl在程序中可见
using namespace std;
int main()
{
    int sum = 0, val = 50;
    while (val <= 100){
        sum += val;
        val += 1;
    }
    cout << "Sum of 50 to 100 inclusive is "
              << sum << endl;
	return 0;
}    

1.10:

#include <iostream>
// 使用过using声明使得cout和endl在程序中可见
using namespace std;
int main()
{
    int val = 10;
    while (val >= 0){
        cout << val << " ";
        val--;
    }
    cout << endl;
	return 0;
}

1.11:

#include <iostream>
// 使用过using声明使得cout和endl在程序中可见
using namespace std;
int main()
{
    int start = 0, end = 0;
    cout << "Please input two num: ";
    cin >> start >> end;
    if (start <= end) {
        while (start <= end){
            cout << start << " ";
            ++start;
        }
        cout << std::endl;
    }
    else{
        cout << "start should be smaller than end !!!";
    }
	return 0;
}  

2.6.2:略

标准库类型 string

标准库类型 string 表示可变长度的字符序列,string 定义在 std 中,使用 string 需包含 string 头文件:

#include<string>
using std::string;

定义和初始化 string 对象

下表列出了初始化 string 对象最常用的方式:

初始化语句含义
string s1默认初始化,s1 是空串
string s2(s1)s2 是 s1 的副本
string s2 = s1等价于 s2(s1)
string s3(“value”)s3 是字面值“value”的副本
string s3 = “value”等价于 s3(“value”)
string s4(n, ‘c’)把 s4 初始化为由连续 n 个字符 c 组成的串

其中改变副本的值不会对原对象的值产生影响:

#include <iostream>
#include<string>
using namespace std;
int main()
{
    string s1;
    string s2(s1);
    cout << "s1: " << s1 << ", s2: " << s2 << endl;
    s2 = "abc";
    cout << "s1: " << s1 << ", s2: " << s2 << endl;
    return 0;
}

结果:在这里插入图片描述

直接初始化和拷贝初始化

使用等号初始化是拷贝初始化(copy initialization),不使用等号执行的是直接初始化(direct initialization)。初始化值有一个时,两个初始化方法没区别;有多个时,一般用直接初始化,也可用拷贝初始化:

string s5 = "hiya";  // 拷贝初始化
string s6("hiya") ;  // 直接初始化
string s7(10, 'c');  // 直接初始化,s7的内容是cccccccccc
string s8 = string(10, 'c'); //拷贝初始化,通过创建临时对象

string 对象上的操作

下图列举了 string 的大多数操作:
在这里插入图片描述
读写 string 对象

可以通过 IO 操作符读写 string 对象:

int main()
{
	string s;          //空字符串
	cin >> s;		   //将 string 对象读入 s,遇到空白停止
	cout << s << endl;
	return 0;
}

输入 Hello World:
在这里插入图片描述
输出 Hello。

可以用连续的输入符号(>>)读取多个变量:

int main()
{
	string s1, s2;
	cin >> s1 >> s2;
	cout << s1 << s2 << endl;
	return 0;
}

输入 Hello World:
在这里插入图片描述
输出 Hello World。

读取未知数量的 string 对象

int main()
{
	string word;
	while (cin >> word) // 反复读取,直到没有输入。
		cout << word << endl; 	 // 边输入边输出。
	return 0;
}

输入 Hello C++ world !:
在这里插入图片描述
最后 ctrl+z ,回车结束输入。

使用 getline 读取一整行

int main()
{
	string line;
	while (getline(cin, line))  //getline 读取一行,但不读入回车。
		cout << line << endl;
	return 0;
}

输入不同长度的 a:
在这里插入图片描述

string 的 empty 和 size 操作

empty 函数根据 string 对象是否为空返回一个对应的布尔值。改写程序,输出非空行:

while (getline(cin, line))
	if (!line.empty()) 
 		cout << line << endl;

size返回string 对象的长度。改写程序输出长度大于 80 个字符的行:

while (getline(cin, line))
 if (line.size() > 80) cout << line << endl;

string::size_type 类型

size 函数返回值是 size_type 类型的值,是一个无符号类型而且能存放下任何 string 对象的大小。
可以通过 auto 或者 decltype 推断变量的类型:

auto len = line.size();      //len 的类型是 string::size_type

如果有表达式已经有了 size() 函数就不要用 int 了,这样能避免混用 int 和 unsigned 可能带来的问题。

比如 n 为负数时, s.size() < n 的结果几乎是 true,因为 n 会先转化为无符号数再进行比较。

比较 string 对象

  • 两个 string 长度相同而且包含的字符也相同:相等。
  • 两个 string 长度不同,短的字符与长的对应字符相等:短的小于长的。
  • 两个 string 长度不同且短的字符与长的对应字符相等,则以第一个相异字符的比较结果为准。
string str = "Hello";
string phrase = "Hello World";
string slang = "Hiya";

str 小于 phrase,slang 大于 str 与 phrase。

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 s4 = s1 + ", ";          // ok: 把一个string对象和一个字面值相加
string s5 = "hello" + ", ";     // error: 两个运算对象都不是string
string s6 = s1 + ", " + "world";    // ok: 每个加法运算符都有一个运算对象是string

每个加法运算符相加的对象至少有一个需要是 string 对象,两个字面值不能相加。

字符串字面值与 string 是不同的类型。

练习 3.2:编写一段程序从标准输入中一次读入一行,然后修改该程序使其一次读入一个词。

答:分别使用 getline 与 cin 即可:

#include <iostream>
#include <string>

using namespace std;

int main()		// 使用getline一次读入一整行
{
	string s;
	cout << "请输入字符串,我能读取一行:" << endl;
	while (getline(cin,s))
		cout << s << endl;
	return 0;
}

#include <iostream>
#include <string>

using namespace std;

int main()		// 使用cin一次读入一个词
{
	string s;
	cout << "请输入单词,我只能读取一个单词:" << endl;
	while (cin >> s)
		cout << s << endl;	
	return 0;
}

练习 3.3:请说明 string 类的输入运算符和 getline 函数分别是如何处理空白字符的。

答:string 类的输入运算符从遇见字符开始读起,遇到空白结束;getline 读取一行,包括空白,遇见换行结束。

练习 3.4:编写一段程序读取两个字符串,比较其是否相等并输出结果。如果不相等,输出比较大的那个字符串。改写上述程序,比较输入的两个字符串是否等长,如果不等长,输出长度较大的那个字符串。

答:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s1, s2;
	cout << "请输入两个字符串:" << endl;
	cin >> s1 >> s2;
	if (s1 == s2)
		cout << "两个字符串相等" << endl;
	else if(s1 > s2)
		cout << s1 << "大于" << s2 << endl;
	else
		cout << s2 << "大于" << s1 << endl;
	return 0;
}

练习 3.5:编写一段程序从标准输入中读入多个字符串并将他们连接起来,输出连接成的大字符串。然后修改上述程序,用空格把输入的多个字符串分割开来。

答:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	char cont = 'y';
	string s, result;
	cout << "请输入第一个字符串:" << endl;
	while(cin >> s)
	{
		result += s;
		cout << "是否继续(y or n)?" << endl;
		cin >> cont;
		if(cont == 'y' || cont == 'Y')
			cout << "请输入下一个字符串:" << endl;
		else
			break;
	}
	cout << "拼接后的字符串是:" << result << endl;
	return 0;
}
#include <iostream>
#include <string>

using namespace std;

int main()
{
	char cont = 'y';
	string s, result;
	cout << "请输入第一个字符串:" << endl;
	while (cin >> s)
	{
		if (!result.size())		// 第一个拼接的字符串之前不加空格
			result += s;
		else					// 之后拼接的每个字符串之前加一个空格
			result = result + " " + s;
		cout << "是否继续(y or n)?" << endl;
		cin >> cont;
		if (cont == 'y' || cont == 'Y')
			cout << "请输入下一个字符串:" << endl;
		else
			break;
	}
	cout << "拼接后的字符串是:" << result << endl;
	return 0;
}

处理 string 对象中的字符

获取字符后可以使用头文件 cctype 中的字符操作函数:
在这里插入图片描述
使用基于范围的 for 语句处理字符

**范围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;  // 输出当前字符,后面紧跟一个换行符

此外可以使用下标运算符([ ])获取单个字符,传入参数的范围是 [0,string.size() )。

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

答:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s;
	cout << "请输入一个字符串,可以包含空格:" << endl;
	getline(cin, s);		// 读取整行,遇回车符结束
	for (auto &c : s)		// 依次处理字符串中的每一个字符,必须是引用否则不能修改字符串的内容。
	{
		c = 'X';
	}
	cout << s << endl;
	return 0;
}

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

答:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s;
	cout << "请输入一个字符串,可以包含空格:" << endl;
	getline(cin, s);		// 读取整行,遇回车符结束
	for (char &c : s)		// 依次处理字符串中的每一个字符
	{
		c = 'X';
	}
	cout << s << endl;
	return 0;
}

改为 char 无影响,因为使用 auto,程序会推断 c 的类型是char。

练习 3.8:分别用while循环和传统for循环重写练习3.6的程序,你觉得哪种形式更好呢?为什么?

答:

#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s;
	cout << "请输入一个字符串,可以包含空格:" << endl;
	getline(cin, s);
	int i = 0;
	while (s[i] != '\0')
	{
		s[i] = 'X';
		++i;
	}
	cout << s << endl;
	return 0;
}

#include <iostream>
#include <string>

using namespace std;

int main()
{
	string s;
	cout << "请输入一个字符串,可以包含空格:" << endl;
	getline(cin, s);
	for (int i = 0; i < s.size(); i++)
	{
		s[i] = 'X';
	}
	cout << s << endl;
	return 0;
}

范围 for 更好,因为简单明了。

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

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

答:输出首字符,不合法,因为 s 是空串,下标 0 非法。

练习 3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。

答:

#include <iostream>
#include <string>
#include <cctype>

using namespace std;

int main()
{
	string s, result;
    decltype(s.size()) i = 0; 
	cout << "请输入一个字符串,最好含有某些标点符号:" << endl;
	getline(cin, s);
	for (auto &c : s)
	{
		if (!ispunct(c))
			result += c;
	}
	cout << result << endl;
	return 0;
}

练习 3.11:下面的范围for语句合法吗?如果合法,c的类型是什么?

答:合法,s是一个常量字符串,则c的推断类型是字符的常量引用,所以 c 绑定的对象的值不能改变。

标准库类型 vector

标准库类型 vector 表示对象的集合,其中所有对象的类型都相同,也常被称作容器。
包含头文件并声明以使用 vector:

#include<vector>
using std::vector;

vector 是一个 类模板(template),模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程,称为 实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

提供 vector 内所存放对象的类型来指定模板实例化成的类:

vector<int> ivec;				// ivec保存int类型的对象
vector<Sales_item> Sales_vec;	// 保存Sales_item类型的对象
vector<vector<string>> file;	// 该向量的元素是vector对象

vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,如 vector<int>。

定义和初始化 vector 对象

初始化 vector 对象的方法:

在这里插入图片描述
初始化 vector:

vector<int> ivec;           //空 vector。         
vector<int> ivec2(ivec);    //将 ivec 的元素拷贝给 ivec2
vector<int> ivec3=ivec;     //把 ivec 的元素拷贝给 ivec3
vector<string> svec(ivec2); //错误,两个 vector 对象的元素类型不同。

列表初始化 vector 对象

列表初始化可以用花括号内的 0 个或多个初始元素值赋给 vector 对象:

vector<string> articles = {"a", "an", "the"};

如果使用圆括号,可以说提供的值是用来构造(construct) vector 对象;
如果使用的是花括号,则是在列表初始化(list initialize) vector 对象:

vector<string> v1{"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误
vector<int> ivec(10, -1); // vector 中有10个-1
vector<int> ivec2{10, -1};// vector 中有两个元素:10,-1
vector<int> ivec3(10); // vector 中有10个元素 0
vector<int> ivec4{10};// vector 中有 1 个元素:10
vector<string> svec(10, "hi!"); // vector 中有 10 个 “hi”
vector<string> svec2{10, "hi!"}; // vector 中有 10 个 “hi”,不是列表初始化。
vector<string> svec3("hi")       //错误:不能使用字符串字面值构建 vector 对象
vector<string> svec4{"hi"}       //列表初始化,列表中有一个 “hi”

练习 3.12:下列vector对象的定义有不正确的吗?如果有,请指出来。对于正确的,描述其执行结果;对于不正确的,说明其错误的原因。
(a)vector<vector> ivec;
(b)vector<string> svec = ivec;
(c)vector<string> svec(10, “null”);

答:(a) 是正确的,定义了一个名为 ivec 的 vector 对象,其中的每个元素都是 vector 对象。
(b) 是错误的,svec 的元素类型是 string,而 ivec 的元素类型是 int,因此不能使用 ivec 初始化 svec。
(c)是正确的,定义了一个名为 svec 的 vector 对象,其中含有10个元素,每个元素都是字符串 null。

练习 3.13:下列的vector对象各包含多少个元素?这些元素的值分别是多少?
vector<int> v1;
vector<int> v2(10);
vector<int> v3(10, 42);
vector<int> v4{10};
vector<int> v5{10, 42};
vector<string> v6{10};
vector<string> v7{10, “hi”};

答:
(a)的元素数量为0。
(b)的元素数量为10,每一个元素都被初始化为0。
(c)的元素数量为10,每一个元素都被初始化为42。
(d)的元素数量为1,元素的值为10。
(e)的元素数量为2,两个元素的值分别为10和42。
(f)的元素数量为10,每一个元素都被初始化为空串。
(g)的元素数量为10,每一个元素都被初始化为 “hi”。

向 vector 对象中添加元素

可以使用 push_back 函数将一个值压到 vector 对象的 “尾端” :

vector<int> v2; // empty vector
for (int i = 0; i != 100; ++i)
	v2.push_back(i); // append sequential integers to v2
// at end of loop v2 has 100 elements, values 0 . . . 99

关键概念:vector 对象能高效增长

范围 for 语句体内不应改变其所遍历序列的大小。
练习 3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。

答:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt;		// 元素类型为int的vector对象
	int i;					// 记录用户的输入值
	char cont = 'y';		// 与用户交互,决定是否继续输入
	while (cin >> i)
	{
		vInt.push_back(i);	// 向vector对象中添加元素
		cout << "要继续吗(y or n)?" << endl;
		cin >> cont;
		if(cont != 'y' && cont != 'Y')
			break;
	}
	for(auto mem : vInt)	// 使用范围for循环语句遍历vInt中的每个元素
		cout << mem << " ";
	cout << endl;
	return 0;
}

练习 3.15:改写上题的程序,这次读入字符串。

答:

#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main()
{
	vector<string> vString;		// 元素类型为string的vector对象
	string s;					// 记录用户的输入值
	char cont = 'y';			// 与用户交互,决定是否继续输入
	while (cin >> s)
	{
		vString.push_back(s);	// 向vector对象中添加元素
		cout << "要继续吗(y or n)?" << endl;
		cin >> cont;
		if(cont != 'y' && cont != 'Y')
			break;
	}
	for(auto mem : vString)		// 使用范围for循环语句遍历vString中的每个元素
		cout << mem << " ";
	cout << endl;
	return 0;
}

其他 vector 操作

下图列出了重要的几种 vector 操作:
在这里插入图片描述
使用范围 for 语句处理 vector 对象中的所有元素:

vector<int> v{1,2,3,4,5,6,7,8,9};
for (auto &i : v) // for each element in v (note: i is a reference)
	i *= i; // square the element value
for (auto i : v) // for each element in v
	cout << i << " "; // print the element
cout << endl;

不能使用下标形式添加元素

vector<int> ivec; // empty vector
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
	ivec[ix] = ix; // disaster: ivec has no elements

上面的代码就是错误的,因为 vector 不包含任何元素,通过下标形式访问元素会造成数组越界。如上一节所述,应使用 push_back 向vector中添加元素。

练习 3.16:编写一段程序,把练习3.13中vector对象的容量和具体内容输出出来。

答:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	vector<int> v1;
	vector<int> v2(10);
	vector<int> v3(10, 42);
	vector<int> v4{ 10 };
	vector<int> v5{ 10, 42 };
	vector<string> v6{ 10 };
	vector<string> v7{ 10, "hi" };

	cout << "v1的元素个数是: " << v1.size() << endl;
	if (v1.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v1的元素分别是:" << endl;
		for(auto e : v1)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v2的元素个数是: " << v2.size() << endl;
	if (v2.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v2的元素分别是:" << endl;
		for(auto e : v2)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v3的元素个数是: " << v3.size() << endl;
	if (v3.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v3的元素分别是:" << endl;
		for(auto e : v3)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v4的元素个数是: " << v4.size() << endl;
	if (v4.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v4的元素分别是:" << endl;
		for(auto e : v4)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v5的元素个数是: " << v5.size() << endl;
	if (v5.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v5的元素分别是:" << endl;
		for(auto e : v5)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v6的元素个数是: " << v6.size() << endl;
	if (v6.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v6的元素分别是:" << endl;
		for(auto e : v6)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}

	cout << "v7的元素个数是: " << v7.size() << endl;
	if (v7.size() > 0)						// 当vector含有元素时逐个输出
	{
		cout << "v7的元素分别是:" << endl;
		for(auto e : v7)					// 使用范围for语句遍历每一个元素
			cout << e << " ";
		cout << endl;
	}
	return 0;
}

练习 3.17:从cin读入一组词并把它们存入一个vector对象,然后设法把所有词都改为大写形式。输出改变后的结果,每个词占一行。

答:

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	vector<string> vString;		// 元素类型为string的vector对象
	string s;					
	char cont = 'y';			// 与用户交互,决定是否继续输入
	cout << "请输入第一个词:" << endl;
	while (cin >> s)
	{
		vString.push_back(s);	// 向vector对象中添加元素
		cout << "要继续嘛(y or n)?" << endl;
		cin >> cont;
		if (cont != 'y' && cont != 'Y')
			break;
		cout << "请输入下一个词:" << endl;
	}
	cout << "请输入下一个词:" << endl;
	for (auto &mem : vString)
	{
		for (auto &c : mem)
			c = toupper(c);
		cout << mem << endl;
	}
	return 0;
}

练习 3.18:下面的程序合法吗?如果不合法,你准备如何修改?

vector<int> ivec;
ivec[0] = 42;

答:

该程序是非法的,因为 ivec 目前没有任何元素,因此 ivec[0] 的形式是错误的,程序试图访问的元素根本不存在。想要向 vector 对象中添加新元素,需要使用 push_back 函数。

修改后的代码:

vector<int> ivec;
ivec.push_back(42);

练习 3.19:如果想定义一个含有10个元素的vector对象,所有元素的值都是42,请例举三种不同的实现方法,哪种方式更好呢?

答:

// 解决思路一:先定义一个空vector对象,然后添加元素。
vector<int> vInt;
for (int i = 0; i < 10; ++i)
	vInt.push_back(42);
	
// 解决思路二:列表初始化,罗列出全部10个元素的值。
vector<int> vInt = {42, 42, 42, 42, 42, 42, 42, 42, 42, 42};

// 解决思路三:用括号给出所有元素的值,效果类似于解决思路二。
vector<int> vInt{42, 42, 42, 42, 42, 42, 42, 42, 42, 42};

// 解决思路四:定义的时候使用参数指定元素个数及重复的值。
vector<int> vInt(10, 42);

// 解决思路五:现制定元素个数,再利用范围for循环依次为元素赋值。
vector<int> vInt(10);
for(auto &i : vInt)
	i = 42;

练习 3.20:读入一组整数并把他们存入一个vector对象,将每对相邻整数的和输出出来。改写你的程序,这次要求先输出第一个和最后一个元素的和,接着输出第二个和倒数第二个元素的和,以此类推。

答:
求相邻元素和的程序如下所示:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt;
	int iVal;
	cout << "请输入一组数字:" << endl;
	while (cin >> iVal)
		vInt.push_back(iVal);
	if(vInt.size() == 0)
	{
		cout << "没有任何元素" << endl;
		return -1;
	}
	cout << "相邻两项的和依次是:" << endl;
	// 利用decltype推断i的类型
	for(decltype(vInt.size()) i = 0; i < vInt.size() - 1; i += 2)
	{
		// 求相邻两项的和
		cout << vInt[i] + vInt[i+1] << " ";
		// 每行输出5个数字
		if((i+2) % 10 == 0)
			cout << endl;
	}
	// 如果元素数是奇数,单独处理最后一个元素
	if (vInt.size() % 2 != 0)
		cout << vInt[vInt.size() - 1];
	return 0;
}

求首尾元素和的程序如下所示:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt;
	int iVal;
	cout << "请输入一组数字:" << endl;
	while (cin >> iVal)
		vInt.push_back(iVal);
	if(vInt.size() == 0)
	{
		cout << "没有任何元素" << endl;
		return -1;
	}
	cout << "首尾两项的和依次是:" << endl;
	// 利用decltype推断i的类型
	for(decltype(vInt.size()) i = 0; i < vInt.size() / 2 ; i++)
	{
		// 求相邻两项的和
		cout << vInt[i] + vInt[vInt.size() - i - 1] << " ";
		// 每行输出5个数字
		if((i+2) % 10 == 0)
			cout << endl;
	}
	// 如果元素数是奇数,单独处理最后一个元素
	if (vInt.size() % 2 != 0)
		cout << vInt[vInt.size() - 1];
	return 0;
}

迭代器介绍

像下标运算符一样,使用迭代器(iterator) 也可以访问 string 的字符和 vector 对象的元素。除了 vector 标准库定义的其他容器也能使用迭代器。迭代器类似于指针类型,迭代器也提供了对对象的间接访问。

使用迭代器

有的迭代器类型拥有返回迭代器的成员,beginend 分别可以返回指向第一个元素的迭代器和最后一个元素下一个位置的迭代器:

// 由编译器决定b和e的类型;
// b表示v的第一个元素, e表示v尾元素的下一位置
auto b = ivec.begin(), e = ivec.end();    // b和e的类型相同

end 成员返回的迭代器通常被称作尾后迭代器(off-the-end iterator) 或者简称为 尾迭代器(end iterator)。

note:如果容器为空,则 begin 和 end 返回的是同一个迭代器,都是尾后迭代器。

迭代器运算符

迭代器支持的运算:
在这里插入图片描述

将迭代器从一个元素移动到另外一个元素

迭代器使用递增(++)运算符来从一个元素移动到下一个元素。

note:因为 end 返回的迭代器并不实际指向某个元素,所以不能对它进行递增或解引用的操作。

在 for 或者其他循环语句的判断条件中,最好使用 != 而不是 <。所有标准库容器的迭代器都定义了 == 和 !=,但是只有其中少数同时定义了 < 运算符。

迭代器类型
拥有迭代器的标准库类型使用 iterator 和 const_iterator 表示迭代器的类型:

vector<int>::iterator it;         // it 能读写 vector<int> 的元素
string::iterator it2;             // 能读写
vector<int>::const_iterator it3;  //只读
string::const_iterator it4;       //只读

begin 和 end 运算符
begin 和 end 返回的类型由对象是否是常量决定,如果对象是常量,begin 和 end 返回 const_iterator;如果不是,返回 iterator:

vector<int> v;            
const vector<int> cv;
auto it1 = v.begin();  // it1 的类型是 vector<int>::iterator
auto it2 = cb.begin(); // it2 的类型是 vector<int>::const_iterator

cbegin 和 cend 的返回值都是 const_iterator,不论 vector 对象本身是否是常量。

凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素;也不要在范围for语句中向 vector 对象添加元素。

练习 3.21:请使用迭代器重做练习 3.16。

答:

#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main()
{
	vector<int> v1;
	vector<int> v2(10);
	vector<int> v3(10, 42);
	vector<int> v4{10};
	vector<int> v5{10, 42};
	vector<string> v6{10};
	vector<string> v7{10, "hi"};

	cout << "v1的元素分别是:" << v1.size() << endl;
	if (v1.cbegin() != v1.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v1的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v1.cbegin(); it != v1.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v2的元素分别是:" << v2.size() << endl;
	if (v2.cbegin() != v2.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v2的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v2.cbegin(); it != v2.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v3的元素分别是:" << v3.size() << endl;
	if (v3.cbegin() != v3.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v3的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v3.cbegin(); it != v3.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v4的元素分别是:" << v4.size() << endl;
	if (v4.cbegin() != v4.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v4的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v4.cbegin(); it != v4.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v5的元素分别是:" << v5.size() << endl;
	if (v5.cbegin() != v5.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v5的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v5.cbegin(); it != v5.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v6的元素分别是:" << v6.size() << endl;
	if (v6.cbegin() != v6.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v6的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v6.cbegin(); it != v6.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}

	cout << "v7的元素分别是:" << v7.size() << endl;
	if (v7.cbegin() != v7.cend())		// 当vector含有元素时逐个输出
	{
		cout << "v7的元素分别是:" << endl;
		// 使用范围for语句遍历每一个元素
		for (auto it = v7.cbegin(); it != v7.cend(); it++)
			cout << *it << " ";
		cout << endl;
	}
	return 0;
}

练习 3.22:修改之前那个输出text第一段的程序,首先把text的第一段全部改成大写形式,然后输出它。

答:

#include <iostream>
#include <vector>
#include <string>

using namespace std;

int main()
{
	vector<string> text;
	string s;
	// 利用getline读取一句话,直接回车产生一个空串,表示段落结束
	while(getline(cin, s))
		text.push_back(s);				// 逐个添加到text中
	// 利用迭代器遍历全部字符串,遇到空串停止循环
	for(auto it = text.begin(); it != text.end() && !it -> empty(); it++)
	{
		// 利用迭代器遍历当前字符串
		for(auto it2 = it -> begin(); it2 != it -> end(); it2++)
			*it2 = toupper(*it2);		// 利用toupper改写成大写形式
		cout << *it << endl;			// 输出当前字符串
	}
	return 0;
}

练习 3.23:编写一段程序,创建一个含有10个整数的vector对象,然后使用迭代器将所有元素的值都变成原来的两倍。输出vector对象的内容,检验程序是否正确。

答:

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
	vector<int> vInt;
	srand((unsigned)time(NULL));		// 生成随机数种子
	for(int i = 0; i < 10; i++)
	{
		// 每次循环生成一个1000以内的随机数并添加到vInt中
		vInt.push_back(rand() % 1000);
	}
	cout << "随机生成的10个数字是:" << endl;
	//利用常量迭代器读取原始数据
	for(auto it = vInt.cbegin(); it != vInt.cend(); it++)
	{
		cout << *it << " ";			// 输出当前数字
	}
	cout << endl;
	cout << "翻倍后的10个数字是:" << endl;
	// 利用非常量迭代器修改vInt内容并输出
	for(auto it = vInt.begin(); it != vInt.end(); it++)
	{
		*it *= 2;
		cout << *it << " ";			// 输出当前数字
	}
	cout << endl;
	return 0;
}

迭代器运算

vector 和 string 迭代器支持的迭代器运算(iterator)

在这里插入图片描述
使用迭代器运算

只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个送代器的距离。所谓距离,是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,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.24:请使用迭代器重做练习 3.20。

答:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt;
	int iVal;
	cout << "请输入一组数字:" << endl;
	while (cin >> iVal)
		vInt.push_back(iVal);
	if(vInt.cbegin() == vInt.cend())
	{
		cout << "没有任何元素" << endl;
		return -1;
	}
	cout << "相邻两项的和依次是:" << endl;
	// 利用auto推断it的类型
	for(auto it = vInt.cbegin(); it != vInt.cend() - 1; it++)
	{
		// 求相邻两项的和
		cout << (*it + *(++it)) << " ";
		// 每行输出5个数字
		if((it - vInt.cbegin() + 1) % 10 == 0)
			cout << endl;
	}
	// 如果元素数是奇数,单独处理最后一个元素
	if (vInt.size() % 2 != 0)
		cout << *(vInt.cend() - 1);
	return 0;
}

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt;
	int iVal;
	cout << "请输入一组数字:" << endl;
	while (cin >> iVal)
		vInt.push_back(iVal);
	if(vInt.cbegin() == vInt.cend())
	{
		cout << "没有任何元素" << endl;
		return -1;
	}
	cout << "首尾两项的和依次是:" << endl;
	auto beg = vInt.begin();
	auto end = vInt.end();
	// 利用auto推断it的类型
	for(auto it = beg; it != beg + (end - beg) / 2; it++)
	{
		// 求首尾两项的和
		cout << (*it + *(beg + (end - it) - 1)) << " ";
		// 每行输出5个数字
		if((it - beg + 1) % 5 == 0)
			cout << endl;
	}
	// 如果元素数是奇数,单独处理最后一个元素
	if (vInt.size() % 2 != 0)
		cout << *(beg + (end - beg) / 2);
	return 0;
}

练习 3.25:3.3.3节划分分数段的程序是使用下标运算符实现的,请利用迭代器改写该程序实现完全相同的功能。

答:

#include <iostream>
#include <vector>

using namespace std;

int main()
{
	vector<int> vInt(11, 0);
	unsigned grade;
	auto begin = vInt.begin();
	while(cin >> grade){
		if(grade <= 100)
			++(*(begin + grade/10));
	}
	for(auto it = vInt.cbegin(); it != vInt.cend(); it++)
	{
		cout << *it << " ";
	}
	cout << endl;
	return 0;
}

练习 3.26:在100页的二分搜索程序中,为什么用的是 mid = beg + (end - beg) / 2, 而非 mid = (beg + end) / 2 ; ?

答:C++并没有定义两个迭代器的加法运算,实际上直接把两个迭代器加起来是没有意义的。与之相反的,C++定义了迭代器的减法运算,两个迭代器相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动多少个元素后可以得到左侧的迭代器,参与运算的两个迭代器必须指向同一容器中的元素或尾后元素。另外,C++还定义了迭代器与整数的加减法运算,用以控制迭代器在容器中左右移动。

在本题中,因为迭代器的加法不存在,所以 mid = (beg + end) / 2; 不合法。mid = beg + (end - beg) / 2; 的含义是,先计算 end - beg 的值得到容器中的元素个数,然后控制迭代器从开始出向右移动二分之一容器的长度,从而定位到容器正中间的元素。

数组

数组类似 vector,也是存放类型相同的对象的容器,但数组的大小确定不变,不能随意向数组中添加元素。

如果不清楚元素的确切个数,应使用 vector。

定义和初始化内置数组

数组是一种复合类型,声明形式为 a[d],其中 a 是数组名称,d 是数组维度。维度说明了数组中元素的个数,因此必须大于0 。数组中元素的个数也属于数组类型的一部分, 编译的时候维度应该是己知的,即维度必须是一个常量表达式

unsigned cnt = 42; 				// 不是常量表达式
constexpr unsigned sz = 42; 	// 常量表达式
int arr[10]; 					// 含有10个整数的数组
int *parr[sz]; 					// 含有42个整型指针的数组
string bad[cnt]; 				// error:cnt不是常量表达式
string strs[get_size()]; 		// 当get_size是constexpr时正确,否则错误

默认情况下,数组的元素被默认初始化。

和内置类型的变量一样,在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

显式初始化数组元素
如果定义数组时提供了元素的初始化列表,则允许省略数组维度,编译器会根据初始值的数量计算维度。但如果显式指明了维度,那么初始值的数量不能超过指定的大小。如果维度比初始值的数量大,则用提供的值初始化数组中靠前的元素,剩下的元素被默认初始化:

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', '+', '+', '\0'};  // 列表初始化,含有显式的空字符
char a3[] = "C++";      			// 自动添加表示字符串结束的空字符
const char a4[6] = "Daniel";    	// 错误:没有空间可存放空字符!

a1 的维度是 3,a2 和 a3 的维度都是 4,a4 的定义是错误的,因为没有空间存放空字符。

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:

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个整数的数组

ptrs 从右往左读:首先是一个大小为 10 的数组,其次存放的类型是 int *。
从数组的名字开始由内向外阅读有助于理解复杂数组声明的含义。
括号内 Parray 是一个指针,看右边说明是一个指向含有十个整数的数组,看左边就是整数数组。
arrRef 类似。

练习 3.27:假设txt_size是一个无参函数,它的返回值是int。请回答下列哪个定义是非法的,为什么?‘
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)是非法的,buf_size 是一个普通的无符号数,不是常量,不能作为数组的维度。
(b)是合法的,4*7-14=14 是一个常量表达式。
(c)是非法的,text_size() 是一个普通的函数调用,没有被定义为 constexpr,不能作为数组的维度。
(d)是非法的,当使用字符串初始化字符数组时,默认在尾部添加一个空字符 ‘\0’,算上这个符号该字符串共有12个字符,但是字符数组 st 的维度只有11,无法容纳题目中的字符串。

练习 3.28:下列数组中元素的值是什么?

string sa[10];
int ia[10];
int main() {
	string sa2[10];
	int ia2[10];
}

答:对于 string 类型的数组来说,因为 string 类本身接受无参数的初始化方式,所以不论数组定义在函数内还是函数外都被默认初始化为空串。
对于内置类型 int 来说,数组 ia 定义在所有函数体之外,根据C++的规定,ia 的所有元素默认初始化为0;而数组 ia2 定义在 main 函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到未定义的奇异值。

练习 3.29:相比于vector 来说,数组有哪些缺点,请例举一些。

答:
能向 vector 中添加元素,但数组的大小固定不变,不能随意向数组中增加额外的元素,虽然在某些情境下运行时性能较好,但是与 vector 相比损失了灵活性。

访问数组元素

数组下标类型通常定义为 size_t 类型。这是一种机器相关的无符号类型,可以表示内存中任意对象的大小。size_t 定义在头文件 cstddef 中。

大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。

练习 3.30:指出下面代码中的索引错误。

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

答:本题的愿意是创建一个包含10个整数的数组,并把数组的每个元素初始化为元素的下标值。
上面的程序在 for 循环终止条件处有错,数组的下标应该大于等于0而小于数组的大小,在本题中下标的范围应该是0~9。
因此程序应该修改为:

constexpr size_t array_size = 10;
int ia[array_size];
for (size_t ix = 0; ix <= array_size; ++ix)
	ia[ix] = ix;

练习 3.31:
编写一段程序,定义一个含有10个int的数组,令每个元素的值就是其下标值。

答:

#include <iostream>

using namespace std;

int main()
{
	const int sz = 10;		// 常量sz作为数组的维度
    int a[sz];
    // 通过for循环为数组元素赋值
    for (int i = 0; i < sz; ++i)
    	a[i] = i;
    // 通过范围for循环输出数组的全部元素
    for (auto val : a)
    	cout << val << " ";
    cout << endl;
    return 0;
}

练习 3.32:将上一题刚刚创建的数组拷贝给另一数组。利用vector重写程序,实现类似的功能。

答:

#include <iostream>

using namespace std;

int main()
{
    const int sz = 10;		// 常量sz作为数组的维度
    int a[sz], b[sz];
    // 通过for循环为数组元素赋值
    for(int i = 0; i < sz; i++)
    	a[i] = i;
    for(int j = 0; j < sz; j++)
    	b[j] = a[j];
    // 通过范围for循环输出数组的全部元素
    for (auto val : b)
    	cout << val << " ";
    cout << endl;
    return 0;
}

#include <iostream>
#include <vector>

using namespace std;

int main()
{
    const int sz = 10;		// 常量sz作为vector的维度
    vector<int> vInt, vInt2;
    // 通过for循环为数组元素赋值
    for(int i = 0; i < sz; i++)
    	vInt.push_back(i);
    for(int j = 0; j < sz; j++)
    	vInt2.push_back(vInt[j]);
    // 通过范围for循环输出vector的全部元素
    for (auto val : vInt2)
    	cout << val << " ";
    cout << endl;
    return 0;
}

练习 3.33:对于104页的程序来说,如果不初始化scores将会发生什么?

答:
该程序用大括号对 scores 执行了列表初始化,为所有元素赋初值为0,这样在后续统计时将会从0开始计算各个分数段的人数,是正确的做法。
如果不初始化 scores,则该数组会含有未定义的数值,这是因为 scores 是定义在函数内部的整形数组,不会执行默认初始化。

指针和数组

对数组使用下标运算符可以得到该数组指定位置的元素。用到数组名字的地方,编译器会自动转换为数组首元素指针:

string nums[] = {"one", "two", "three"};    // 数组的元素是string对象
string *p = &nums[0];   					// p指向nums的第一个元素
string *p2 = &nums[1];   					// p指向nums的第二个元素
string *p3 = nums;      					// 等价于p3 = &nums[0]

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;    						// error:不能用整型指针给数组赋值
ia3[4] = i;     					// ok:把i的值赋给ia3的一个元素

从上面代码可以看出:当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组。但 decltype 关键字不会发生这种转换,直接返回数组类型。

指针也是迭代器

与 vector 类似,指针自增或自减可以指向其后一个或前一个元素:

int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int *p = arr;   // p 指向 arr 的第一个元素
++p;            // p 指向 arr 的第二个元素
--p;            // p 指向 arr 的第一个元素

标准库函数

C++11在头文件 iterator 中定义了两个名为 begin 和 end 的函数,功能与容器中的两个同名成员函数类似,参数是一个数组。

int ia[] = {0,1,2,3,4,5,6,7,8,9};   // ia是一个含有10个整数的数组
int *beg = begin(ia);   			// 指向ia首元素的指针
int *last = end(ia);    			// 指向arr尾元素的下一位置的指针

note: 尾指针不能进行解引用和递增操作

指针运算
给(从) 一个指针加上(减去)某整数值,结果仍是指针。新指针指向的元素与原来的指针相比前进(后退)该整数值个位置。两个指针相减的结果类型是 ptrdiff_t,这是一种定义在头文件 cstddef 中的带符号类型。因为差值可能为负值, 所以 ptrdiff_t 是一种带符号类型。

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]
auto n = end(arr) - begin(arr); // n的值是5 ,也就是arr中元素的数量

对以上指针解引用即可得到对应的值。

下标和指针

数组的索引可以为负:

int ia[] = {0, 2, 4, 6, 8};
int p = &ia[2];    //p 指向索引为 2 的元素
int k = p[-2];     //k 的值为 p 的前两个元素的值 0.
int j = p[1];

练习 3.34:假定p1 和 p2 都指向同一个数组中的元素,则下面程序的功能是什么?什么情况下该程序是非法的?

答:如果 p1 和 p2 指向同一数组中的元素,则该条语句令 p1 指向 p2 原来所指向的元素。
从语法上来说,即使 p1 和 p2 指向的元素不属于同一个数组,但是只要 p1 和 p2 的类型相同,该语句也是合法的。
如果 p1 和 p2 的类型不同,则编译时报错。

练习 3.35:编写一段程序,利用指针将数组中的元素置为0。

答:

#include <iostream>

using namespace std;

int main()
{
	const int sz = 10;		// 常量sz作为数组的维度
	int a[sz], i = 0;
	// 通过for循环为数组元素赋值
	for (i = 0; i < 10; i++)
		a[i] = i;
	cout << "初始状态下数组的内容是:" << endl;
	for (auto val : a)
		cout << val << " ";
	cout << endl;
	int *p = begin(a);			// 令p指向数组首元素
	while (p != end(a))
	{
		*p = 0;					// 修改p所指元素的值
		p++;					// p向后移动一位
	}
	cout << "修改后的数组内容是:" << endl;
	// 通过范围for循环输出数组的全部元素
	for (auto val : a)
		cout << val << " ";
	cout << endl;
	return 0;
}

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

答:对比两个数组是否相等的程序如下所示,因为长度不等的数组一定不相等,并且数组的维度一开始就要确定,所以为了简化起见没程序中设定两个待比较的数组维度一致,仅比较对应的元素是否相等。
该例类似于一个彩票游戏,先由程序随机选出5个0~9的数字,此过程类似于摇奖;再由用户手动输入5个猜测的数字,类似于购买彩票;分别把两组数字存入数组 a 和 b,然后逐一比对两个数组的元素;一旦有数字不一致,则告知用户猜测错误,只有当两个数组的所有元素都相等时,判定数组相等,即用户猜测正确。

#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
	const int sz = 5;					// 常量sz作为数组的维度
	int a[sz], b[sz], i;
	srand((unsigned)time(NULL));		// 生成随机数种子
	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 val : a)
		cout << val << " ";
	cout << endl;
	cout << "猜测的数据是" << endl;
	for (auto val : b)
		cout << val << " ";
	cout << endl;
	int *p = begin(a), *q = begin(b);	// 令p和q分别指向数组a和b的首元素
	while (p != end(a) && q != end(b))
	{
		if (*p != *q)
		{
			cout << "猜测错误,两个数据不相等" << endl;
			return -1;
		}
		p++;							// p向后移动一位
		q++;							// q向后移动一位
	}
	cout << "恭喜,全部都猜对了" << endl;
	return 0;
}

对比两个 vector 对象是否相等的程序如下所示,其中使用迭代器遍历 vector 对象的元素。

#include <iostream>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
	const int sz = 5;					// 常量sz作为数组的维度
	int a[sz], b[sz], i;
	srand((unsigned)time(NULL));		// 生成随机数种子
	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 val : a)
		cout << val << " ";
	cout << endl;
	cout << "猜测的数据是" << endl;
	for (auto val : b)
		cout << val << " ";
	cout << endl;
	int *p = begin(a), *q = begin(b);	// 令p和q分别指向数组a和b的首元素
	while (p != end(a) && q != end(b))
	{
		if (*p != *q)
		{
			cout << "猜测错误,两个数据不相等" << endl;
			return -1;
		}
		p++;							// p向后移动一位
		q++;							// q向后移动一位
	}
	cout << "恭喜,全部都猜对了" << endl;
	return 0;
}

C风格字符串

字符串字面值是一种通用结构的实例,这种结构即是C++由C继承而来的 C风格字符串(C-style character string)。C风格字符串是将字符串存放在字符数组中,并以 空字符结束(null terminated)。这不是一种类型,而是一种为了表达和使用字符串而形成的约定俗成的书写方法。以空字符结束的意思是在字符串最后一个字符后面跟着一个空字符 \0
一般利用指针来操作这些字符串。

C风格字符串的函数:
请添加图片描述
C风格字符串函数不负责验证其参数的正确性,传入此类函数的指针必须指向以 空字符 作为结尾的数组。

比较字符串

使用strcmp可以达到 string 对象使用 >, <, = 的结果。

warning: 对大多数程序来说,使用标准库string要比使用C风格字符串更加安全和高效。

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

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

答:程序第一行声明了一个包含5个字符的字符数组,因为无须修改数组的内容,所以将其定义为常量。第二行定义了一个指向字符常量的指针,该指针可以指向不通过的字符常量,但是不允许通过该指针修改所指常量的值。

while 循环的条件时 *cp,只要指针 cp 所指的字符不是空字符 ‘\0’,循环就重复执行,循环的任务有两项:首先输出指针当前所指的字符,然后将指针向后移动一位。

该程序的愿意是输出 ca 中存储的5个字符,每个字符占一行,但实际的执行效果无法符合预期。因为以列表初始化方式复制的c风格字符串与以字符串字面值赋值的有所区别,后者会在字符串最后额外增加一个空字符以示字符串的结束,而前者不会这样做。

因此在该程序中,ca 的5个字符全都输出后,并没有遇到预期的空字符,也就是说,while 循环的条件仍将满足,无法跳出。程序继续在内存中 ca 的存储位置之后挨个寻找空字符,直到找到为止。在这个过程中,额外经历的内容也将被输出出来,从而产生错误。
想要实现程序的原意,应该修改为:

const char ca[] = { 'h', 'e', 'l', 'l', 'o', '\0' };
const char *cp = ca;
while (*cp) {
    cout << *cp << endl;
    ++cp;
}

练习 3.38:在本节中我们提到,将两个指针相加不但是非法的,而且也没有什么意义。请问为什么两个指针相加没有意义?

答:指针也是一个对象,与指针相关的属性有3个,分别是指针本身的值、指针所指的对象以及指针本身在内存中的存储位置。它们的含义分别是:

  • 指针本身的值是一个个内存地址值,表示指针所指对象在内存中的存储地址;
  • 指针所指的对象可以通过解引用指针访问;
  • 因为指针也是一个对象,所以指针也存储在内存的某个位置,它有自己的地址,这也是为什么有“指针的指针”的原因。

通过上述分析可知,指针的值是它所指对象的内存地址,如果把两个指针加在一起,就是试图把内存中两个对象的存储地址加在一起,这显然是没有任何意义的。与之相反,指针的减法是有意义的。如果两个指针指向同一个数组中的不同元素,则它们相减的结果表征了它们所指的元素在数组中的距离。

练习 3.39:编写一段程序,比较两个 string 对象。再编写一段程序,比较两个C风格字符串的内容。

答:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    string str1, str2;
    cout << "请输入两个字符串:" << endl;
    cin >> str1 >> str2;
    
    if (str1 > str2)
        cout << "第一个字符串大于第二个字符串" << endl;
    else if (str1 < str2)
        cout << "第一个字符串小于第二个字符串" << endl;
    else
        cout << "两个字符串相等" << endl;
	return 0;
}

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

答:

#include <iostream>
#include <cstring>

using namespace std;

int main()
{
	char str1[] = "welcome to";
	char str2[] = "C++ family";
	// 利用strlen函数计算两个字符串的长度
	// 定义一个内存变量result,并申请长度为求得结果字符串的长度
	char *result = new char[strlen(str1) + strlen(str2) - 1];

	strcpy(result, str1);		// 把第一个字符串拷贝到结果字符串中
	strcat(result, str2);		// 把第二个字符串拼接到结果字符串中

	cout << "第一个字符串是:" << str1 << endl;
	cout << "第二个字符串是:" << str2 << endl;
	cout << "拼接后的字符串是:" << result << endl;
	system("pause");
	return 0;
}

与旧代码的接口

混用 string 对象和 c 风格字符串
任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:

  • 允许使用以空字符结束的字符数组来初始化 string 对象或为 string 对象赋值。
  • 在 string 对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是)。
  • 在 string 对象的复合赋值运算中,允许使用以空字符结束的字符数组作为右侧运算对象。

不能用 string 对象直接初始化指向字符的指针。为了实现该功能,string 提供了一个名为 c_str 的成员函数,返回 const char* 类型的指针,指向一个以空字符结束的字符数组,数组的数据和 string 对象一样。

string s("Hello World");    // s的内容是Hello World
char *str = s;  			// 错误: 不能用string对象初始化char*
const char *str = s.c_str();// 正确

如果后续操作改变了 s 的值就可能让之前返回的数组失效,如果执行完c_str() 函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。

使用数组初始化 vector 对象

前面说过,不允许使用一个数组对另外一个数组赋值,但能使用数组来初始化 vector 对象,初始化时需要指明要拷贝区域的首元素地址和尾后元素地址就可以了:

int int_arr[] = {0, 1, 2, 3, 4, 5};
// ivec有6个元素,分别是int_arr中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));

建议:尽量使用标准库类型而非数组

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

答:

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
    const int sz = 10;  				// 常量sz作为数组的维度
    int a[sz];
    srand((unsigned) time (NULL));  	// 生成随机数种子
    cout << "数组的内容是" << endl;
    // 利用范围for循环遍历数组的每个元素
    for(auto &val : a)
    {
    	val = rand() % 100;				// 生成一个100以内的随机数
    	cout << val << " ";
    }
    cout << endl;
	// 利用begin和end初始化vector对象
	vector<int> vInt(begin(a), end(a));
	cout << "vector的内容是:" << endl;
	// 利用范围for循环遍历vector的每个元素
	for(auto val : vInt)
	{
		cout << val << " ";
	}
	cout << endl;
    return 0;
}

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

答:

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>

using namespace std;

int main()
{
	const int sz = 10;  				// 常量sz作为vector对象的容量
	vector<int> vInt;
	srand((unsigned)time(NULL));  	// 生成随机数种子
	cout << "vector对象的内容是" << endl;
	// 利用范围for循环遍历vector对象的每个元素
	for (int i = 0; i != sz; i++)
	{
		vInt.push_back(rand() % 100);		// 生成一个100以内的随机数
		cout << vInt[i] << " ";
	}
	cout << endl;

	auto it = vInt.cbegin();
	int *a = new int[vInt.size()];
	cout << "数组的内容是:" << endl;
	// 利用范围for循环遍历数组的每个元素
	for (int i = 0; i < vInt.size(); i++)
	{
		a[i] = *it;
		cout << a[i] << " ";
		it++;
	}
	cout << endl;
	return 0;
}

多维数组

C++中的多维数组其实就是数组的数组。当一个数组的元素仍然是数组时,通常需要用两个维度定义它:一个维度表示数组本身的大小,另一个维度表示其元素(也是数组)的大小。通常把二维数组的第一个维度称作行,第二个维度称作列。

多维数组初始化的几种方式:

int ia[3][4] = 
{   // 三个元素,每个元素都是大小为3的数组
    {0, 1, 2, 3},   // 第1行的初始值
    {4, 5, 6, 7},   // 第2行的初始值
    {8, 9, 10, 11}  // 第3行的初始值
};
// 没有标识每行的花括号,与之前的初始化语句是等价的
int ib[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
// 显式地初始化每行的首元素
int ic[3][4] = {{ 0 }, { 4 }, { 8 }};
// 显式地初始化第1行,其他元素执行值初始化
int id[3][4] = {0, 3, 6, 9};

多维数组的下标引用

可以使用下标访问多维数组的元素,数组的每个维度对应一个下标运算符。

  • 如果表达式中下标运算符的数量和数组维度一样多,则表达式的结果是给定类型的元素。
  • 如果下标运算符数量比数组维度小,则表达式的结果是给定索引处的一个内层数组。
// 用arr的首元素为ia最后一行的最后一个元素赋值
ia[2][3] = arr[0][0][0];
int (&row)[4] = ia[1];  // 把row绑定到ia的第二个4元素数组上

使用范围 for 语句处理多维数组

为了避免数组被自动转换成指针,语句的外层循环控制变量必须声明成引用类型。

for (const auto &row : ia)  // 对于外层数组的每一个元素
    for (auto col : row)    // 对于内层数组的每一个元素
        cout << col << endl;

如果 row 不是引用类型,编译器初始化 row 时会自动将数组形式的元素转换成指向该数组内首元素的指针,这样得到的 row 就是 int* 类型,而之后的内层循环则试图在一个 int* 内遍历,程序将无法通过编译。

for (auto row : ia)
    for (auto col : row)

使用范围 for 语句处理多维数组时,除了最内层的循环,其他所有外层循环的控制变量都应该定义成引用类型。

指针和多维数组

当程序使用多维数组的名字时,也会自动转成指向数组首元素的指针。

定义指向多维数组的指针时,别忘了这个多维数组实际上是数组的数组。

因为多维数组实际上是数组的数组,所以由多维数组名称转换得到的指针指向第一个内层数组。

int ia[3][4];       // 大小为3的数组,每个元素是含有4个整数的数纽
int (*p)[4] = ia;   // p指向含有4个整数的数组
p = &ia[2];         // p指向ia的尾元素

C++11新标准,使用 auto 和 decltype 能简化复杂的指针定义。

// 输出ia中每个元素的值,每个内层数组各占一行
// p指向含有4个整数的数组
for (auto p = ia; p != ia + 3; ++p) 
{
    // q指向4个整数数组的首元素,也就是说,q指向一个整数
    for (auto q = *p; q != *p + 4; ++q)
        cout << *q << ' ';
    cout << endl;
}

使用标准库函数 begin 和 end 也能实现同样的功能,而且看起来更简洁一些。

// p指向ia的第一个数组
for (auto p = begin(ia); p != end(ia); ++p) 
{
	// q指向内层数组的首元素
	for (auto q = begin(*p); q != end(*p); ++q)
		cout << *q << ' '; 	// 输出q所指的整数值
	cout << endl;
}

练习 3.43:编写3个不同版本的程序,令其均能输出ia的元素。版本1使用范围for语句管理迭代过程;版本2和版本3都使用普通for语句,其中版本2要求使用下标运算符,版本3要求使用指针。此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto关键字和decltype关键字。

答:

#include <iostream>

using namespace std;

int main()
{
	int ia[3][4] =
	{
		{ 0, 1, 2, 3 },
		{ 4, 5, 6, 7 },
		{ 8, 9, 10, 11 }
	};
	cout << "range for:" << endl;
	for (int (&row) [4]: ia)
	{
		for (int col : row)
			cout << col << " ";
		cout << endl;
	}

	cout << "xiabiao:" << endl;
	for (int i = 0; i != 3; ++i)
	{
		for (int j = 0; j != 4; j++)
			cout << ia[i][j] << " ";
		cout << endl;
	}

	cout << "pointer:" << endl;
	for (int (*p)[4] = ia; p != ia + 3; p++)
	{
		for (int *q = *p; q != *p + 4; q++)
			cout << *q << " ";
		cout << endl;
	}
	return 0;
}

练习 3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。

答:

#include <iostream>

using namespace std;
using int_array = int[4];

int main()
{
	int ia[3][4] =
	{
		{ 0, 1, 2, 3 },
		{ 4, 5, 6, 7 },
		{ 8, 9, 10, 11 }
	};
	cout << "range for:" << endl;
	for (int_array &row : ia)
	{
		for (int col : row)
			cout << col << " ";
		cout << endl;
	}

	cout << "xiabiao:" << endl;
	for (int i = 0; i != 3; ++i)
	{
		for (int j = 0; j != 4; j++)
			cout << ia[i][j] << " ";
		cout << endl;
	}

	cout << "pointer:" << endl;
	for (int_array *p = ia; p != ia + 3; p++)
	{
		for (int *q = *p; q != *p + 4; q++)
			cout << *q << " ";
		cout << endl;
	}
	return 0;
}

练习 3.45:再一次改写程序,这次使用 auto 关键字。

答:

#include <iostream>

using namespace std;

int main()
{
	int ia[3][4] =
	{
		{ 0, 1, 2, 3 },
		{ 4, 5, 6, 7 },
		{ 8, 9, 10, 11 }
	};
	cout << "range for:" << endl;
	for (auto &row : ia)
	{
		for (auto col : row)
			cout << col << " ";
		cout << endl;
	}

    cout << "xiabiao:" << endl;
    for (auto i = 0; i != 3; i++)
    {
        for (auto j = 0; j != 4; j++)
            cout << ia[i][j] << " ";
    	cout << endl;
    }

    cout << "pointer:" << endl;
    for (auto p = ia; p != ia + 3; p++)
    {
        for (auto q = *p; q != *p + 4; q++)
            cout << *q << " ";
    	cout << endl;
	}
    return 0;
}

关注博仔不迷路,一起学好C++。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

进击的博仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值