string
对象
getline
读取一整行string::size_type
类型for
语句vector
对象 P88vector
对象中添加元素
vector
对象添加元素蕴含的编程假定vector
操作
vector
内对象的索引begin
和end
运算符vector
对象的操作会使迭代器失效begin
和end
string
和vector
是两种最重要的标准库类型。string
支持可变长字符串,后者则表示可变长的集合。
迭代器,是 string 和 vector 的配套类型,常被用于访问 string 中的字符或 vector 中的元素。
using
声明(using declaration)
std::cin
表示从标准输入中读取内容,::
为作用域操作符。
含义:编译器应从操作符左侧名字所示的作用域中寻找右侧那个名字。std::cin
的意思就是要使用命名空间std
中的名字cin
。
原因:因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个 using 声明,那么每个使用了该头文件的文件就都会有这个声明。对于某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
标准库类型string
表示可变长的字符序列。
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 组成的串 |
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 | string 的操作 |
---|---|
os< |
将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
#include
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
#include
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
函数分别是如何处理空白字符的。
>>
运算符,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 str = “Hello”;
string phrase = “Hello World”;
string slang = “Hiya”;
判断:对象 str 小于对象 phrase;对象 slang 既大于 str 也大于 phrase。
当把 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.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; // 输出当前字符,后面紧跟着一个换行符
如果想要改变 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
#include
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
#include
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
#include
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 对象中的单个字符有两种方式:
下标运算符([]
),接受的输入参数是 string::size_type
类型的值,这个参数表示要访问的字符的位置;返回值是该位置上字符的引用。例:
/*将字符串的首字符改成大写形式*/
string s("some string");
if (!s.empty()) // 确保 s[0] 的位置确实有字符
s[0] = toupper(s[0]); // 为 s 的第一个字符赋一个新值
练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。
/* 练习3.10:编写一段程序,读入一个包含标点符号的字符串,将标点符号去除后输出字符串剩余的部分。*/
// 思路一:利用范围for语句遍历字符串,逐个输出非标点字符:
#include
#include
#include
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
#include
#include
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。
标准库类型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.4 | 初始化vector 对象的方法 |
---|---|
vector |
v1 是一个空 vector,它潜在的元素是 T 类型的,执行默认初始化 |
vector |
v2 中包含有 v1 所有元素的副本 |
vector |
等价于 v2(v1),v2 中包含有 v1 所有元素的副本 |
vector |
v3 包含了 n 个重复的元素,每个元素的值都是 val |
vector |
v4 包含了 n 个重复地执行了值初始化的对象 |
vector |
v5 包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector |
等价于 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 对象。
(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
回顾: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
vector<string> svec(10); // 10 个元素,每个都是空 string 对象
花括号 vs 圆括号:
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
vector<string> v5{
"hi"}; // 列表初始化,v5 有一个元素
vector<string> v6("hi"); // 错误:不能使用字符串字面值构建 vector 对象
vector<string> v7{
10}; // v7 有 10 个默认初始化的元素
vector<string> v8{
10, "hi"}; // v8 有 10 个值为 "hi" 的元素
上面其实只有 v5 是列表初始化。要想列表初始化对象,花括号里的值必须与元素类型相同。
练习3.13:下列的 vector 对象各包含多少个元素怒?这些元素的值分别是多少?
/* 练习3.13:下列的 vector 对象各包含多少个元素怒?这些元素的值分别是多少? */
(a) vector<int> v1;
// 元素数量为 0
(b) vector<int> v2(10);
// 10 个元素,每个元素都被初始化为 0
(c) vector<int> v3(10, 42);
// 10 个元素,每个元素都被初始化为 42
(d) vector<int> v4{
10};
// 1 个元素,元素值为 10
(e) vector<int> v5{
10, 42};
// 2 个元素,元素值分别为 10 和 42
(f) vector<string> v6{
10};
// 10 个元素,每一个元素都被初始化为空字符串
(g) vector<string> v7{
10, "hi"};
// 10 个元素,每一个元素都被诶初始化为 "hi"
/* 练习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;
// 思路四采用的初始化方式形式上最简洁直观。
vector
对象中添加元素创建一个空vector
,运行时再利用 push_back
向其中添加元素,push_back
负责把一个值当成vector
对象的尾元素“压到(push)” vector
对象的“尾端(back)”。
vector<int> v2; // 空 vector 对象
for (int i = 0; i != 100; ++i)
v2.push_back(i); // 依次把整数值放到 v2 尾端
// 循环结束后 v2 有 100 个元素,值从 0 到 99
/* 如果直到运行时才能直到 vector 对象中元素的确切个数,也应使用这种方法
比如:从标准输入中读取单词,将其作为 vector 对象的元素存储 */
string word;
vector<string> text; // 空 vector 对象
while (cin >> word) {
text.push_back(word); // 把 word 添加到 text 后面
}
关键概念:vector
对象能高效增长
当所有(all)元素的值都一样时,在定义 vector 对象的时候有必要设定其大小;
一旦元素的值有所不同,更有效的办法是先定一个空的 vector 对象,再在运行时向其中添加具体值。
这跟 C 或 Java在创建对象时顺便制定其容量的做法恰恰相反。
vector
对象添加元素蕴含的编程假定注意:如果循环体内部包含有向vector
对象添加元素的语句,则不能使用范围for
循环。
WARNING: 范围for
语句体内不应改变其所遍历序列的大小。
练习3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。
/* 练习3.14:编写一段程序,用 cin 读入一组整数并把它们存入一个 vector 对象。*/
#include
#include
using namespace std;
int main()
{
vector<int> vInt; // 元素类型为 int 的 vector 对象
int i; // 记录用户的输入值
char cont = 'y'; // 与用户交互,决定是否继续输入
while (cin >> i)
{
vInt.push_back(i); // 向 vector 对象中添加元素
cout << "Do you wanna continue? (y or n)? " << endl;
cin >> cont;
if (cont != 'y' && cont != 'Y')
break;
}
for (auto mem : vInt) // 使用范围for循环语句遍历 vInt 中的每个元素
cout << mem << " ";
cout << endl;
return 0;
}
/* Output:
1
Do you wanna continue? (y or n)?
y
2
Do you wanna continue? (y or n)?
y
3
Do you wanna continue? (y or n)?
Y
4
Do you wanna continue? (y or n)?
1 2 3 4
*/
/* 练习 3.17:从 cin 读入一组词并把它们存入一个 vector 对象,
然后设法把所有词都改写为大写形式。
输出改变后的结果,每个词占一行。*/
#include
#include
#include
using namespace std;
int main()
{
vector<string> vString;
string word;
char cont = 'y';
cout << "Enter the first word: " << endl;
while (cin >> word)
{
vString.push_back(word);
cout << "Do you wanna continue (y or n)? " << endl;
cin >> cont;
if (cont != 'y' && cont != 'Y')
break;
cout << "Please enter the next word: " << endl;
}
cout << "The converted result is: " << endl;
for (auto &mem : vString) {
// 使用范围for循环语句遍历vString中的每个元素
for (auto &c : mem) // 使用范围for循环语句遍历mem中的每个字符
c = toupper(c); // 改写为大写字母形式
cout << mem << endl;
}
return 0;
}
/* Output:
Enter the first word:
Meow
Do you wanna continue (y or n)?
y
Please enter the next word:
Alex
Do you wanna continue (y or n)?
Y
Please enter the next word:
Bordora
Do you wanna continue (y or n)?
n
The converted result is:
MEOW
ALEX
BORDORA
*/
vector
操作表3.5: | 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 |
|
< , <= , > , >= |
顾名思义,以字典顺序进行比较 |
使用范围for语句处理 vector 对象中的所有元素:
vector<int> v{
1,2,3,4,5,6,7,8,9}
for (auto &i : v) // 对于 v 中的每个元素(注意:i 是一个引用)
i *= i; // 求元素的平方
for (auto i : v) // 对于 v 中的每个元素
cout << i << " "; // 输出该元素
cout << endl;
第一个循环把控制变量 i 定义成引用类型,这样就能通过 i 给 v 的元素赋值,其中 i 的类型由 auto
关键字指定。
size
返回 vector 对象中元素的个数,返回值的类型是由 vector 定义的size_type
类型。
要使用size_type
,需首先指定它是由哪种类型定义的。vector
对象的类型总是包含着元素的类型:
vector<int>::size_type // 正确
vector::size_type // 错误
vector
内对象的索引下标的类型也是相应的size_type
类型。
举个例子,假设有一组成绩的集合,其中成绩的取值是从 0 到 100。以 10 分为一个分数段,要求统计各个分数段各有多少个成绩。
显然,从 0 到 100 总共有 101 种可能的成绩取值,这些成绩分布在 11 个分数段上:每 10 个分数构成一个分数段,这样的分数段有 10 个,额外还有一个分数段表示满分 100 分。
这样第一个分数段将统计成绩在 0 到 9 之间的数量;
第二个分数段将统计成绩在 10 到 19 之间的数量,以此类推。
最后一个分数段统计满分 100 分的数量。
按照上面的描述,如果输入的成绩如下:
42 65 95 100 39 67 95 76 88 76 83 92 76 93
则输出的结果应该是:
0 0 0 1 1 0 2 3 2 4 1
结果显示:
成绩在30分以下的没有、
30分至39分有 1 个、
40分至49分有 1 个、
50分至59分没有、
60分至69分有 2 个、
70分至79分有 3 个、
80分至89分有 2 个、
90分至99分有 4 个、
还有 1 个是满分。
/* 以 10 分为一个分数段统计成绩的数量:0~9, 10~19, ..., 90~99, 100 */
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
// 读取成绩
if (grade <= 100) // 检查读入的成绩是否合法(即是否小于等于100分)
++scores[grade/10]; // 将成绩对应分数段的计数值加 1
}
执行计数值累加的那条语句很好地体现了 C++ 程序代码的简洁性。
++scores[grade/10];
// 将当前分数段的计数加 1
等价于:
auto ind = grade/10; // 得到分数段索引
score[ind] = score[ind] + 1; // 将计数值加 1
另一种方法:用迭代器(iterator)改写该程序并实现完全相同的功能。(练习 3.25)
#include
#include
using namespace std;
int main()
{
vector<unsigned> scores(11);
auto it = scores.begin();
int grade;
cout << "Please enter a group of scores (0~100): " << endl;
while (cin >> grade)
if (grade <= 100)
++*(it + grade/10); //利用迭代器定位到对应的元素,加 1
cout << "You have inputted in total " << scores.size() << " scores." << endl;
cout << "The number of people in each band (from low to high) is: " << endl;
// 利用迭代器遍历 scores 的元素并逐个输出
for (it = scores.begin(); it != scores.end(); it++)
{
cout << *it << " ";
}
cout << endl;
return 0;
}
/* 通过 vector 对象的下标形式来添加元素可能会产生错误 */
vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
ivec[ix] = ix; // 严重错误:ivec 不包含任何元素
ivec
是一个空vector
,根本不包含任何元素,当然也就不能通过下标去访问任何元素。正确的方法是使用push_back
:
vector<int> ivec;
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
ivec.push_back(ix); // 正确:添加一个新元素,该元素的值是 ix
vector
对象(以及string
对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
通过下标访问不存在的元素会产生很严重的后果,所谓的缓冲区溢出(buffer overflow)指的就是这类错误。
确保下标合法的一种有效手段就是尽可能使用范围for语句。
之前学习了使用下标运算符访问 string 对象的字符或 vector 对象的元素;
使用**迭代器(iterator)**也可以实现同样的目的。
所有标准库容器都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。
有迭代器的类型同时拥有返回迭代器的成员:
begin
,负责返回指向第一个元素(或第一个字符)的迭代器;end
,负责返回指向容器(或 string 对象)“尾元素的下一位置”(one past the end)的迭代器;end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)。begin
和end
返回的是同一个迭代器,都是尾后迭代器。/* 由编译器决定 b 和 e的类型,使用 auto 关键字定义变量 b 和 e */
auto b = v.begin(), e = v.end(); // b 和 e 的类型相同
表3.6: | 标准容器迭代器的运算符 |
---|---|
*iter |
返回迭代器iter 所指元素的引用 |
iter->mem |
解引用iter 并获取该元素的名为mem 的成员,等价于(*iter).mem |
++iter |
令iter 指示容器中的下一个元素 |
--iter |
令iter 指示容器中的上一个元素 |
iter1 == iter2 |
判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等。 |
iter1 != iter2 |
如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。
通过解引用迭代器来获取它所指示的元素:
/* 将 string 对象的第一个字母改为大写形式 */
string s("some string");
if (s.begin() != s.end()) {
// 首先检查 s 是否为空,确保 s 非空
auto it = s.begin(); // it 表示 s 的第一个字符
*it = toupper(*it); // 将当前字符改成大写形式
}
// Output: Some string
Note: 因为end
返回迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。
/* 将 string 对象中第一个单词改写为大写形式*/
// 依次处理 s 的字符直至我们处理完全部字符或者遇到空白
for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
*it = toupper(*it); // 将当前字符改成大写形式
循环部分首先用s.begin
的返回值来初始化it
,意味着it
指示的是s
中的第一个字符(如果有的话)。
条件部分检查是否已到达s
的尾部,如果尚未到达,则将it
解引用的结果传入isspace
函数检查是否遇到了空白。
关键概念:泛型编程:C++程序员习惯性地使用!=
而非<
,是因为只有 string 和 vector 等一些标准库类型有下标运算符,大多数都没有定义<
运算符,但是所有标准库容器的迭代器都定义了==
和!=
。所以要养成使用迭代器和!=
的习惯。
就像不知道string
和vector
的size_type
成员到底是什么类型一样,一般来说我们也不知道或无须知道迭代器的精确类型。
实际上,那些拥有迭代器的标准库类型使用iterator
和const_iterator
来表示迭代器的类型。
vector<int>::iterator it; // it 能读写 vector 的元素
string::iterator it2; // it2 能读写 string 对象中的字符
vector<int>::const_iterator it3; // it3 只能读元素,不能写元素
string::const_iterator it4; // it4 只能读元素,不能写元素
iterator
的对象可读可写const_iterator
能读取但不能修改它所指的元素值如果vector
对象或string
对象是一个常量,只能使用const_iterator
;
如果vector
对象或string
对象不是常量,那么既能使用iterator
也能使用const_iterator
。
begin
和end
运算符vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1 的类型是 vector::iterator
auto it2 = cv.begin(); // it2 的类型是 vector::const_iterator
auto it3 = v.cbegin(); // it3 的类型是 vector::const_iterator
为了专门得到const_iterator
类型的返回值,C++新标准引入两个新函数,分别是cbegin
和cend
。
如果对象不是常量,返回iterator
;如果对象是常量,begin
和end
返回const_iterator
。
不论 vector 对象(或 string 对象)本身是否是常量,返回值都是 const_iterator
。
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。
对于一个由字符串组成的vector
对象来说,要想检查其元素是否为空,令it
是该vector
对象的迭代器,只需检查it
所指字符串是否为空就可以了:(*it).empty()
(*it).empty() // 解引用 it,然后调用结果对象的 empty 成员
*it.empty() // 错误:试图访问 it 的名为 empty 的成员,但 it 是个迭代器,没有 empty 成员
注意:(*it).empty()
圆括号必不可少。该表达式的含义是先对it
解引用,然后解引用的结果再执行点运算符。如果不加圆括号,点运算符将由it
来执行,而非it解引用的结果
。
上面第二个表达式的含义是从名为it
的对象中寻找其empty
成员,显然it
是一个迭代器,它没有哪个成员叫empty
的,所以第二个表达式将发生错误。
为了简化,箭头运算符(->) 把解引用和成员访问两个操作结合在一起,也就是说,it->mem
和 (*it).mem
表达的意思相同。
例:假设用一个名为 text 的字符串向量存放文本文件中的数据,其中的元素或者是一句话或者是一个用于表示段落分隔的空字符串。如果要输出 text 中第一段的内容,可以利用迭代器写一个循环令其遍历 text,直到遇到空字符串的元素为止:
// 依次输出 text 的每一行直至遇到第一个空白行为止
for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
cout << *it << endl;
值得注意的是,因为循环从头到尾只是读取 text 的元素而未向其中写值,所以使用了cbegin
和cend
来控制整个迭代过程。
当需要更改 vector 对象的内容,所以使用的迭代器应该是begin
和end
,而非 cbegin 和 cend。
练习 3.22:修改之前那个输出 text 第一段的程序,首先把 text 的第一段全都改成大写形式,然后再输出它。
/* 练习 3.22:修改之前那个输出 text 第一段的程序,首先把 text 的第一段全都改成大写形式,然后再输出它。*/
/* 思路:需要将第一段(vector对象第一个空串元素之前的所有元素)改写成大写字母的形式再输出。
因为需要更改 vector 对象的内容,所以使用的迭代器应该是 begin 和 end,而非 cbegin 和 cend。*/
#include
#include
#include
using namespace std;
int main()
{
vector<string> text;
string s;
// 利用 getline 读取一句话,直接回车产生一个空串,表示段落结束
while (getline(cin, s))
text.push_back(s);
// 利用迭代器遍历全部字符串,遇空串停止循环
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 对象,然后使用迭代器将所有元素的值都变成原来的两倍。
/* 练习3.23:编写一段程序,创建一个含有 10 个整数的 vector 对象,然后使用迭代器将所有元素的值都变成原来的两倍。
输出 vector 对象的内容,检验程序是否正确。*/
#include
#include
#include
#include
using namespace std;
int main()
{
vector<int> vInt;
srand((unsigned)time(NULL)); // 生成随机数种子
for (int i = 0; i < 10; i++)
{
vInt.push_back(rand() % 1000); // 每次循环生成一个1000以内的随机数并添加到vInt中
}
cout << "The 10 randomly generated numbers are: " << endl;
// 利用常量迭代器读取原始数据
for(auto it = vInt.cbegin(); it != vInt.cend(); it++)
{
cout << *it << " "; // 输出当前数字
}
cout << endl;
cout << "The 10 doubled numbers are: " << endl;
// 利用非常量迭代器修改 vInt 内容并输出
for (auto it = vInt.begin(); it != vInt.end(); it++)
{
*it *= 2;
cout << *it << " "; // 输出当前数字
}
cout << endl;
return 0;
}
vector
对象的操作会使迭代器失效vector
对象添加元素vector
对象容量的操作,比如push_back
,都会使该vector
对象的迭代器失效谨记:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
vector 和 string 迭代器支持的运算:
iter + n
iter - n
iter1 += n
iter1 -= n
iter1 - iter2
>
、>=
、<
、<=
只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓举例指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为 difference_type 的带符号整型数。string 和 vector 都定义了 difference_type,因为这个举例可正可负,所以 difference_type 是带符号类型的。
使用迭代器运算的一个经典算法是二分搜索。
// text 必须是有序的
// beg 和 end 表示我们搜索的范围
auto beg = text.begin(), end = text.end();
auto mid = text.begin() + (end - beg)/2; // 初始状态下的中间点
// 当还有元素尚未检查并且我们还没有找到 sought 时执行循环
while (mid != end && *mid != sought) {
if (sought < *mid) // 我们要找的元素在前半部分吗?
end = mid; // 如果是,调整搜索范围使得忽略掉后半部分
else // 我们要找的元素在后半部分
beg = mid + 1; // 在 mid 之后找
mid = beg + (end - beg)/2; // 新的中间点
}
循环部分先检查搜索范围是否为空,如果 mid 和 end 的当前值相等,说明已经找遍了所有元素。此时条件不满足,循环终止。当搜索范围不为空,可知 mid 指向了某个元素,检查该元素是否就是我们所要搜索的,如果是,也终止循环。
循环过程终止时,mid
或者等于end
或者指向要找的元素。如果mid
等于end
,说明 text 中没有我们要找的元素。P101
练习3.26:在这个二分搜索程序中,为什么用的是mid = beg + (end - beg) / 2
,而非mid = (beg + end) / 2
?
解答:
C++并没有定义两个迭代器的加法运算,实际上直接把两个迭代器加起来是没有意义的。
与之相反,C++定义了迭代器的检法运算,两个跌大气相减的结果是它们之间的距离,也就是说,将运算符右侧的迭代器向前移动多少个元素后可以得到左侧的迭代器,参与运算的两个跌大气必须指向同一个容器中的元素或尾后元素。
另外,C++还定义了迭代器与整数的加减法运算,用以控制迭代器在容器中左右移动。
在本题中,因为迭代器的加法不存在,所以mid = (beg + end) / 2;
不合法。
mid = beg + (end - beg) / 2;
的含义是,先计算end - beg
的值得到容器中的元素个数,然后控制迭代器从开始处向右移动二分之一容器的长度,从而定位到容器正中间的元素。
练习 3.38:将两个指针相加不但是非法的,而且也什么意义。请问为什么两个指针相加没什么意义?110:
【出题思路】与标准库vector
类似,C++也为指针定义了一系列算术运算,包括递增、递减、指针求差、指针与整数求和等,但是并没有定义两个指针的求和运算。
要想理解这一规定,必须首先明白指针的含义。
【解答】
指针也是一个对象,与指针相关的属性有 3 个,分别是指针本身的值(value)、指针所指的对象(content)、以及指针本身在内存中的存储位置(address)。它们的含义分别是:
通过上述分析我们知道,指针的值是它所指对象的内存地址,如果我们把两个指针加在一起,就是试图把内存中两个对象的存储地址加在一起,这显然是没有任何意义的。
与之相反,指针的减法是有意义的。如果两个指针指向同一个数组中的不同元素,则它们相间的结果表征了它们所指的元素在数组中的距离。
练习3.24:请使用迭代器重做3.3.3节(第94页)的最后一个练习(P101)。
/* 练习3.24:请使用迭代器重做 3.3.3 节(第94页)的最后一个练习。P101 */
/* 练习3.20:读入一组整数并把它们存入一个 vector 对象,将每对相邻整数的和输出出来。
改写你的程序,这次要求先输出第 1 个和最后 1 个元素的和,接着输出第 2 个和倒数第 2 个元素的和,以此类推。*/
// 求相邻元素和的程序如下所示:
#include
#include
using namespace std;
int main()
{
vector<int> vInt;
int iVal;
cout << "Please enter a group of numbers: " << endl;
while (cin >> iVal)
vInt.push_back(iVal);
if (vInt.cbegin() == vInt.cend())
{
cout << "There are no elements." << endl;
return -1;
}
cout << "Adjacent two numbers are: " << 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.end() - 1);
return 0;
}
/*
Please enter a group of numbers:
5
548
45
987
87
74
15
^Z
Adjacent two numbers are:
553 1032 161 15
*/
// 求首尾元素和的程序如下所示:
#include
#include
using namespace std;
int main()
{
vector<int> vInt;
int iVal;
cout << "Please enter a group of numbers: " << endl;
while (cin >> iVal)
vInt.push_back(iVal);
if (vInt.cbegin() == vInt.cend())
{
cout << "There are no elements." << endl;
return -1;
}
auto beg = vInt.begin();
auto end = vInt.end();
cout << "First and last elements are: " << *beg << " " << *(end-1) << endl;
// 利用 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;
}
/*
Please enter a group of numbers:
5
548
45
987
87
74
15
^Z
First and last elements are: 5 15
20 622 132 987
*/
练习 3.25:3.3.3节(第93页)划分分数段的程序是使用下标运算符实现的,请利用迭代器改写该程序并实现完全相同的功能。
/* 以 10 分为一个分数段统计成绩的数量:0~9, 10~19, ..., 90~99, 100
得到各分数段的人数分布(成绩从低到高)*/
// 用下标运算符
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
// 读取成绩
if (grade <= 100) // 检查读入的成绩是否合法(即是否小于等于100分)
++scores[grade/10]; // 将成绩对应分数段的计数值加 1
}
// 用迭代器改写,通过解引用迭代器来获取它所指示的元素
vector<unsigned> scores(11);
auto it = scores.begin();
int grade;
while (cin >> grade)
if (grade <= 100)
++*(it + grade/10); // 利用迭代器定位到对应的元素,加 1
// 迭代器改写的完整代码
#include
#include
using namespace std;
int main()
{
vector<unsigned> scores(11);
auto it = scores.begin();
int grade;
cout << "Please enter a group of scores (0~100): " << endl;
while (cin >> grade)
if (grade <= 100)
++*(it + grade/10); // 利用迭代器定位到对应的元素,加 1
cout << "You have inputted in total " << scores.size() << " scores." << endl;
cout << "The number of people in each band (from low to high) is: " << endl;
// 利用迭代器遍历 scores 的元素并逐个输出
for (it = scores.begin(); it != scores.end(); it++)
{
cout << *it << " ";
}
cout << endl;
return 0;
}
与 vector 相似的地方是,数组也是存放类型相同的对象的容器;
与 vector 不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。
Tip: 所以,如果不清楚元素的确切个数,请使用 vector。
练习 3.29:相比于vector来说,数组有哪些缺点,请列举一些。
解答:数组与vector的相似之处是hi都能存放类型相同的对象,且这些对象本身没有名字,需要通过其所在位置访问。
数组与vector的最大不同是,数组的大小固定不变,不能随意向数组种增加额外的元素,虽然在某些情景下运行时性能较好,但是与vector相比损失了灵活性。
具体来说:
size
函数直接获取数组的维度。
strlen
函数得到字符串的长度;sizeof(array)
/sizeof(array[0])
的方式计算数组的维度。数组的声明如 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
一样,数组的元素应为对象,因此不存在引用的数组。
练习3.27:假设 txt_size 是一个无参数的函数,它的返回值是 int。
/* 练习3.27:假设 txt_size 是一个无参数的函数,它的返回值是 int。
请回答下列哪个定义是非法的?为什么? */
unsigned buf_size = 1024;
(a) int ia[buf_size];
// 非法,不是常量表达式,buf_size 是一个普通的无符号数,不是常量,不能作为数组的维度。
(b) int ia[4 * 7 - 14];
// 合法,4 * 7 - 14 是常量表达式
(c) int ia[txt_size()];
// 非法,txt_size() 是一个普通的函数调用,没有被定义为 constexpr,不能作为数组的维度
(d) char st[11] = "fundamental";
// 非法,当使用字符串初始化字符数组时,默认在尾部添加一个空字符 '\0',算上这个符号该字符共有 12 个字符,但是字符数组 st 的维度只有 11,无法容纳题目中的字符串。
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。
如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;
相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。
const unsigned sz = 3;
int ia1[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", "bye"}; // 等价于 a4[] = {"hi", "bye", ""}
int a5[2] = {
0, 1, 2}; // 错误:初始值过多
字符数组有一个额外的初始化形式,我们可以用字符串字面值对此类数组初始化。
当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:
char a1[] = {
'c', '+', '+'}; // 列表初始化,没有空字符,维度是3
char a2[] = {
'c', '+', '+', '\0'}; // 列表初始化,含有显式的空字符,维度是4
char a3[] = "C++"; // 自动添加字符串结束的空字符,维度是4
const char a4[6] = "Daniel"; // 错误:没有空间可存放空字符!维度必须至少是7
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:
int a[] = {
0, 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 的数组,它的名字是ptrs
,然后知道数组种存放的是指向int
的指针。
当遇到指针指向数组或引用指向数组,由内往外阅读。
对于Parray
来说,因为数组多维度是紧跟着被声明的名字的,所以这里应由内向外阅读。
首先是圆括号括起来的部分,*Parray
意味着Parray
是个指针,
接下来观察右边,可知道Parray
是个指向大小为10
的数组指针,
最后观察左边,知道数组种的元素是int
。
所以含义为:Parray
是一个指针,它指向一个int
数组,数组种包含10
个元素。
同理,(&arrRef)
表示arrRef
是一个引用,它引用的对象是一个大小为10
的数组,数组种元素的类型是int
。
int *(&arry)[10] = ptrs; // arry 是数组的引用,该数组含有 10 个指针
按照由内而外的顺序阅读,首先知道arry
是一个引用,
然后观察右边知道,arry
引用的对象是一个大小为10
的数组,
最后观察左边知道,数组的元素类型是指向int
的指针。
所以含义为;arry
是一个含有10
个int
型指针的数组的引用。
数组的元素也能使用范围for语句或下标付来访问。
在使用数组下标的时候,通常将其定义为**size_t
**类型。size_t
是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。
在cstddef
头文件中定义了size_t
类型,这个文件是C标准库stddef.h
头文件的C++语言版本。
// 数组 用下标运算符
// 以 10 分为一个分数段统计成绩的数量:0~9,10~19,...,90~99,100
unsigned scores[11] = {
}; // 11 个分数段,全部初始化为 0
unsigned grade;
while (cin >> grade) {
if (grade <= 100)
++scores[grade/10]; // 将当前分数段的计数值加 1
}
// vector 用下标运算符
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
unsigned grade;
while (cin >> grade) {
// 读取成绩
if (grade <= 100) // 检查读入的成绩是否合法(即是否小于等于100分)
++scores[grade/10]; // 将成绩对应分数段的计数值加 1
}
区别:
unsigned scores[11] = {
}; // 11 个分数段,全部初始化为 0
vector<unsigned> scores(11, 0); // 11 个分数段,全都初始化为 0
与 vector 和 string 一样,当需要遍历数组的所有元素时,最好的办法也是使用范围 for 语句。
for (auto i : scores) // 对于 scores 中的每个计数值
cout << i << " "; // 输出当前的计数值
cout << endl;
练习 3.32:将上一题刚刚创建的数组拷贝给另外一个数组。
/* 练习 3.32:将上一题刚刚创建的数组拷贝给另外一个数组。
利用vector重写程序,实现类似的功能。*/
// 实现数组拷贝的程序如下所示:
#include
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;
}
// 实现 vector 拷贝的程序如下所示:
#include
#include
using namespace std;
int main()
{
const int sz = 10; // 常量 sz 作为 vector 的容量
vector<int> vInt, vInt2;
// 通过for循环为 vector对象的元素赋值
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;
}
WARNING: 大多数常见的安全问题都源于缓冲区溢出错误。当数组或其他类似数据结构的下标越界并试图访问非法内存区域时,就会产生此类错误。
通过数组名字或者数组中首元素的地址都能得到指向首元素的指针:
string nums[] = {
"one", "two", "three"}; // 数组的元素是 string 对象
string *p = &nums[0]; // p 指向 nums 的第一个元素
string *p2 = nums; // 等价于 p2 = &nums[0],自动替换为一个指向数组首元素的指针
auto
变量的初始值时,推断得到的类型是指针而非数组:int ia[] = {
0,1,2,3,4,5,6,7,8,9}; // ia 是一个含有 10 个整数的数组
auto ia2(ia); // ia2 是一个整型指针,指向 ia 的第一个元素
ia2 = 42; // 错误:ia2 是一个指针,不能用 int 值给指针赋值
尽管ia
是由 10 个整数构成的数组,但当使用ia
作为初始值时,编译器实际执行的初始化过程类似于下面的形式:
auto ia2(&ia[0]); // 显然 ia2 的类型是 int*
decltype
关键字时,上述转换不会发生,decltype(ia)
返回的类型是由 10 个整数构成的数组:decltype(ia) ia3 = {
0,1,2,3,4,5,6,7,8,9};
ia3 = p; // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把 i 的值赋给 ia3 的一个元素
vector 和 string 的迭代器支持的运算,数组的指针全都支持。
/* 例:允许使用递增运算符将指向数组元素的指针向前移动到下一个位置上 */
int arr[] = {
0,1,2,3,4,5,6,7,8,9};
int *p = arr; // p 指向 arr 的第一个元素
++p; // p 指向 arr[1]
遍历数组中的元素:
先获取到指向数组第一个元素的指针和指向数组尾元素的下一位置的指针。
int *e = &arr[10]; // 指向 arr 尾元素的下一位置的指针
arr
有 10 个元素,尾元素所在位置的索引是9
,接下来那个不存在的元素唯一的用处就是提供其地址用于初始化e
。
就像尾后迭代器一样,尾后指针也不指向具体的元素。因此不能对尾后指针执行解引用或递增的操作。
/* 用数组的指针遍历,输出 arr 的全部元素 */
for (int *b = arr; b != e; ++b)
cout << *b << endl; // 输出 arr 的元素
begin
和end
尽管能计算得到尾后指针,但这种用法极易出错。
为了让指针的使用更简单、更安全,C++ 新标准引入了两个名为begin
和end
的函数。
begin
函数返回指向ia
首元素的指针,end
函数返回指向ia
尾元素下一位置的指针,这两个函数定义在iterator
头文件中。
这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数。
正确的使用形式是将数组作为它们的参数:
int ia[] = {
0,1,2,3,4,5,6,7,8,9}; // ia 是一个含有 10 个整数的数组
int *beg = begin(ia); // 指向 ia 首元素的指针
int *last = end(ia); // 指向 arr 尾元素的下一位置的指针
使用begin
和end
可以很容易地写出一个循环并处理数组中的元素。例如,假设arr
是一个整形数组,下面的程序负责找到arr
中的第一个负数:
// pbeg 指向 arr 的首元素,pend 指向 arr 尾元素的下一位置
int *pbeg = begin(arr), *pend = end(arr);
// 寻找第一个负值元素,如果已经检查完全部元素,则结束循环
while (pbeg != pend && *pbeg >= 0) // 通过比较 pbeg 和 pend 来确保可以安全地对 pbeg 解引用
++pbeg;
Note:一个指针如果指向了某种内置类型数组的尾元素的“下一位置”,则其具备与vector
的end
函数返回的与迭代器类似的功能。
特别要注意,尾后指针不能执行解引用和递增操作。
新指针指向的元素与原来的指针相比前进了(后退了)该整数值个位置:
constexpr size_t sz = 5;
int arr[sz] = {
1,2,3,4,5};
int *ip = arr; // 等价于 int *ip = &arr[0]
int *ip2 = ip + 4; // ip2 指向 arr 的尾元素 arr[4]
注意:当给arr
加上sz
时,编译器自动地将arr
转换成指向数组arr
中首元素的指针。
执行加法后,指针从首元素开始向前移动了sz
(这里是 5)个位置,指向新位置的元素。
也就是说,它指向了数组arr
尾元素的下一位置。
// 正确:arr 转换成指向它首元素的指针;p 指向 arr 尾元素的下一位置
int *p = arr + sz; // 使用警告:不要解引用!
int *p2 = arr + 10; // 错误:arr 只有 5 个元素,p2 的值未定义
和迭代器一样,两个指针相减的结果是它们之间的距离。
两个指针相减的结果的类型是一种名为**ptrdiff_t
**的标准库类型,和size_t
一样,ptrdiff_t
也是一种定义在cstddef
头文件中的机器相关的类型。
因为差值可能为负值,所以ptrdiff_t
是一种带符号类型。
int *b = arr, *e = arr + sz;
while (b < e) {
// 使用 *b
++b;
}
只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较。
如果两个指针分别指向不相关的对象,则不能比较它们:
int i = 0, sz = 42;
int *p = &i, *e = &sz;
// 未定义的:p 和 e 无关,因此比较毫无意义!
while (p < e)
注:上述指针运算同样适用于空指针和所指对象并非数组的指针。
如果p
是空指针,允许给p
加上或减去一个值为0
的整型常量表达式。两个空指针也允许彼此相减,结果当然是0
。
指针加上一个整数所得的结果还是一个指针。
假设结果指针指向了一个元素,则允许解引用该结果指针:
int ia[] = {
0,2,4,6,8}; // 含有 5 个整数的数组
int last = *(ia + 4); // 正确:把 last 初始化成 8,也就是 ia[4] 的值
在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。
当对数组下标运算符时,编译器会自动执行上述转换操作。
ia[0] 是一个使用了数组名字的表达式,对数组执行下标运算其实是对指向数组元素的指针执行下标运算。
int ia[] = {
0,2,4,6,8}; // 含有 5 个整数的数组
int i = ia[2]; // ia 转换成指向数组首元素的指针
// ia[2] 得到 (ia + 2)所指的元素
int *p = ia; // p 指向 ia 的首元素
i = *(p + 2); // 等价于 i = ia[2]
只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:
int *p = &ia[2]; // p 指向索引为 2 的元素
int j = p[1]; // p[1]等价于 *(p + 1),就是 ia[3] 表示的那个元素
int k = p[-2]; // p[-2] 是 ia[0] 表示的那个元素
注意:数组与标准库类型 string 和 vector 是有所不同的:
标准库类型 string 和 vector 限定使用的下标必须是无符号类型,
而内置的下标运算无此要求。上面的最后一个例子很好地说明了这一点。内置的下标运算符可以处理负值。
WARNING: 内置的下标运算符所用的索引值不是无符号数,这一点与 vector 和 string 不一样。
练习 3.36:编写一段程序,比较两个数组是否相等。再写一段程序,比较两个 vector 对象是否相等。
/* 练习 3.36:编写一段程序,比较两个数组是否相等。
再写一段程序,比较两个 vector 对象是否相等。
类似于一个彩票游戏,先由程序随机选出 5 个 0~9 的数字,次过程类似于摇奖;
再由用户手动输入 5 个猜测的数字,类似于购买彩票;
分别把两组数字存入数组 a 和 b,然后逐一比对两个数组的元素;
一旦有数字不一致,则告知用户猜测错误,
只有当两个数组的所有元素都相等时,判定数组相等,即用户猜测正确。
*/
#include
#include
#include
using namespace std;
int main()
{
const int sz = 5; // 常量 sz 作为数组的维度
int a[sz], b[sz], i = 0;
srand( (unsigned) time (NULL) ); // 生成随机数种子
// 通过 for 循环为数组元素赋值
for (int i = 0; i < sz; i++)
{
// 每次循环生成一个 10 以内的随机数并添加到 a 中
a[i] = rand() % 10;
}
cout << "Please enter 5 digits (0~9), with repetition: " << endl;
int uVal;
// 通过 for 循环为数组元素赋值
for (i = 0; i < sz; i++)
if (cin >> uVal)
b[i] = uVal;
cout << "The system generated data are: " << endl;
for (auto val : a)
cout << val << " ";
cout << endl;
cout << "Your guessed data are: " << 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 << "Your guess is wrong. The two arrays are not equal." <<endl;
return -1;
}
p++; // p 向后移动一位
q++; // q 向后移动一位
}
cout << "Congratulations, you have guessed them all right!" << endl;
return 0;
}
/* 对比两个 vector 对象是否相等的程序如下所示,其中使用迭代器遍历 vector 对象的元素。*/
#include
#include
#include
#include
using namespace std;
int main()
{
const int sz = 5;
int i = 0;
vector<int> a, b;
srand( (unsigned) time (NULL));
// 通过 for 循环为 vector 元素赋值
for (int i = 0; i < sz; i++)
a.push_back(rand() % 10);
cout << "Please enter 5 digits (0~9), with repetition: " << endl;
int uVal;
// 通过 for 循环为 vector 元素赋值
for (i = 0; i < sz; i++)
if (cin >> uVal)
b.push_back(uVal);
cout << "The system generated data are: " << endl;
for (auto val : a)
cout << val << " ";
cout << endl;
cout << "Your guessed data are: " << endl;
for (auto val : b)
cout << val << " ";
cout << endl;
// 令 it1, it2 分别指向 vector 对象 a 和 b 的首元素
auto it1 = a.cbegin(), it2 = b.cbegin();
while (it1 != a.cend() && it2 != b.cend())
{
if (*it1 != *it2)
{
cout << "You have guessed them wrong. The two vectors are not equal." << endl;
return -1;
}
it1++; // p 向后移动一位
it2++; // q 向后移动一位
}
cout << "Congratulations, you have guessed them all right!" << endl;
return 0;
}
尽管 C++ 支持C风格字符串(C-style character string),但在 C++ 程序中最好还是不要使用它们。这是因为 C 风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
C 风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法。按此习惯书写的字符串存放在字符数组中并以空字符结束(null terminated),即字符串最后一个字符后面跟着一个空字符(’\0’)。一般利用指针来操作这些字符串。
C 语言标准库提供的一组函数,这些函数可用于操作 C风格字符串,它们定义在 cstring
头文件中,cstring
是 C语言头文件 string.h
的C++版本。
表3.8: | C风格字符串的函数 |
---|---|
strlen(p) |
返回 p 的长度,空字符不计算在内 |
strcmp(p1, p2) |
比较 p1 和 p2 的相等性。如果 p1==p2 ,返回 0 ;如果 p1>p2 ,返回一个正值;如果 p1 |
strcat(p1, p2) |
将 p2 附加到 p1 之后,返回 p1 |
strcpy(p1, p2) |
将 p2 拷贝给 p1 ,返回 p1 |
传入此类函数的指针必须指向以空字符作为结束的数组:
char ca[] = {
'c', '+', '+'}; // 不以空字符结束
cout << strlen(ca) << endl; // 严重错误:ca 没有以空字符结束
ca
虽然也是一个字符数组,但它不是以空字符作为结束的,因此上述程序产生未定义的结果。
strlen
函数将有可能沿着 ca
在内存中的位置不断向前寻找,直到遇到空字符才停下来。
练习3.37:以列表初始化方式赋值的C风格字符串与以字符串字面值赋值的有所区别:
// 一、以列表初始化方式赋值的 C风格字符串
const char ca[] = {
'h', 'e', 'l', 'l', 'o', '\0'};
const char *cp = ca;
while (*cp) {
cout << *cp << endl;
++cp;
}
// 二、以字符串字面值赋值的形式
const char ca[] = "hello";
const char *cp = ca;
while (*cp) {
cout << *cp <<end;
++cp;
}
比较两个 C风格字符串的方法和之前学习过的比较标准库 string 对象的方法大相径庭。
string s1 = "A string example";
string s2 = "A different string";
if (s1 < s2) // false: s2 小于 s1
// 错误做法
const char ca1[] = "A string example";
const char ca2[] = "A different string";
if (ca1 < ca2) // 未定义的:试图比较两个无关地址
/* 解释:当使用数组的时候其实真正用的是指向数组首元素的指针,
因此,上面的 if 条件实际上比较的是两个 const char*的值。
这两个指针指向的并非同一对象,所以将得到未定的结果。 */
strcmp
函数,此时比较的久不再是指针了。strcmp
返回0
;if (strcmp(ca1, ca2) < 0) // 和两个 string 对象的比较 s1 < s2 效果一样
练习 3.39:编写一段程序,比较两个 string 对象。再编写一段程序,比较两个 C风格字符串的内容。
/* 练习 3.39:编写一段程序,比较两个 string 对象。
再编写一段程序,比较两个 C风格字符串的内容。*/
/*【出题思路】
由于标准库 string 类定义了关系运算符,所以比较两个 string 对象可以直接使用 <、>、==等;
比较两个C风格字符串则必须使用 cstring 头文件中定义的 strcmp 函数。
*/
// 比较两个 string 对象的程序如下所示:
#include
#include
using namespace std;
int main()
{
string str1, str2;
cout << "Please enter two strings: " << endl;
cin >> str1 >>str2;
if (str1 > str2)
cout << "The first string is greater than the second string." << endl;
else if (str1 < str2)
cout << "The first string is smaller than the second string." << endl;
else
cout << "The two strings are equal." << endl;
return 0;
}
// 比较两个 C风格字符串的程序如下所示,其中的分支部分选用了 switch-case 语句,其效果与上一个程序的 if-else 语句非常类似。
#include
#include
using namespace std;
int main()
{
char str1[80], str2[80];
cout << "PLease enter two strings: " << endl;
cin >> str1 >> str2;
// 利用 cstring 头文件中定义的 strcmp 函数比较大小
auto result = strcmp(str1, str2);
switch(result)
{
case 1:
cout << "The first string is greater than the second string." << endl;
break;
case -1:
cout << "The first string is smaller than the second string." << endl;
break;
case 0:
cout << "The two strings are equal." << endl;
break;
default:
cout << "Undefined result" << endl;
break;
}
return 0;
}
连接或拷贝 C风格字符串也与标准库string
对象的同类操作差别很大。
要想把刚刚定义的那个 string
对象s1
和s2
连接起来,可以直接写成下面的形式:
// 将 largeStr 初始化成 s1、一个空格 和 s2 的连接
string largeStr = s1 + " " + s2;
同样的操作如果放到 ca1
和 ca2
这两个数组身上就会产生错误。表达式 ca1 + ca2
试图将两个指针相加,显然这样的操作没什么意义,也肯定是非法的。
// 如果我们计算错了 largeStr 的大小将引发严重错误
strcpy(largeStr, cal); // 把 ca1 拷贝给 largeStr
strcat(largeStr, " "); // 在 largeStr 的末尾加上一个空格
strcat(largeStr, ca2); // 把 ca2 连接到 largeStr 后面
C风格字符串的操作函数定义在cstring
头文件中。
strcpy
函数负责把字符串的内容拷贝给另一个字符串strcat
函数则负责把字符串的内容拼接到另一个字符串之后strlen
函数用于计算字符串的长度需要注意的是,利用指腹从字面值常量初始化C风格字符串时,默认在数组最后添加一个空字符,因此strlen
的计算结果比字面值显示的字符数量多1
。
为了细致起见,计算两个字符串拼接后的长字符串长度时,应该在两个字符串各自长度求和后减去1
,即减去1
个多余空字符所占的额外空间。
Tip: 对大多数应用来说,使用标准库string
要比使用 C风格字符串更安全、更高效。
练习 3.40:编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前两个数组连接后的结果。使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组中。
/* 练习 3.40:编写一段程序,定义两个字符数组并用字符串字面值初始化它们;
接着再定义一个字符数组存放前两个数组连接后的结果。
使用 strcpy 和 strcat 把前两个数组的内容拷贝到第三个数组中。*/
#include
#include
using namespace std;
int main()
{
char str1[] = "Welcome to ";
char str2[] = "C++ family!";
// 利用 strlen 函数计算两个字符串的长度,并求得结果字符串的长度
char result[strlen(str1) + strlen(str2) - 1];
strcpy(result, str1); // 把第一个字符串拷贝到结果字符串中
strcat(result, str2); // 把第二个字符串拼接到结果字符串中
cout << "The first string is: " << str1 << endl;
cout << "The second string is: " << str2 << endl;
cout << "The concatenated string is: " << result << endl;
return 0;
/* Output:
The first string is: Welcome to
The second string is: C++ family!
The concatenated string is: Welcome to C++ family!
*/
}
一般情况下,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来替代:
string
对象或为string
对象赋值。string
对象的加法运算中,允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string
对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。上述性质反过来就不成立了:如果程序的某处需要一个 C风格字符串,无法直接用string
对象来代替它。
例如,不能用string
对象直接初始化指向字符的指针。
为了完成该功能,string
专门提供了一个名为c_str
的成员函数:
string s("Hello World"); // s 的内容是 Hello World
char *str = s; // 错误:不能用 string 对象初始化 char*
const char *str = s.c_str();// 正确
c_str
函数的返回值是一个C风格的字符串。
函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string
对象的一样。
结果指针的类型是const char*
,从而确保我们不会改变字符数组的内容。
不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector
对象初始化数组;
相反的,允许使用数组来初始化vector
对象。
要实现这一目的,只需指明要拷贝区域的首元素地址和尾后地址就可以了。
int int_arr[] = {
0,1,2,3,4,5};
// ivec 有 6 个元素,分别是 int_arr 中对应元素的副本
vector<int> ivec(begin(int_arr), end(int_arr));
用于创建ivec
的两个指针实际上指明了用来初始化的值在数组int_arr
中的位置,
其中第二个指针应指向待拷贝区域尾元素的下一位置。
此例中,使用标准库begin
和end
来分别计算int_arr
的首指针和尾后指针。
用于初始化vector
对选哪个的值也可能仅是数组的一部分:
// 拷贝三个元素:int_arr[1]、int_arr[2]、int_arr[3]
vector<int> subVec(int_arr + 1, int_arr + 4);
建议:尽量使用标准库类型而非数组:
现代 C++ 程序应当尽量使用vector
和迭代器,避免使用内置数组和指针;
应该尽量使用string
,避免使用C风格的基于数组的字符串。
练习3.41:编写一段程序,用整型数组初始化一个 vector 对象。
/* 练习3.41:编写一段程序,用整型数组初始化一个 vector 对象。*/
#include
#include
#include
#include
using namespace std;
int main()
{
const int sz = 10; // 常量 sz 作为数组的维度
int a[sz];
srand( (unsigned) time (NULL)); // 生成随机数种子
cout << "The array content is: " << endl;
// 利用范围for循环遍历 数组 的每个元素
for (auto &val : a)
{
val = rand() % 100; // 生成一个 100 以内的随机数
cout << val << " ";
}
cout << endl;
// 利用 begin 和 end 初始化 vector 对象 (用数组初始化vector)
vector<int> vInt(begin(a), end(a));
cout << "The vector content is: " << endl;
// 利用范围 for 循环遍历 vector 的每个元素
for (auto val : vInt)
{
cout << val << " ";
}
cout << endl;
return 0;
}
练习 3.42:编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。
/* 练习 3.42:编写一段程序,将含有整数元素的 vector 对象拷贝给一个整型数组。*/
/*【出题思路】
C++ 允许使用数组直接初始化 vector 对象,但是不允许使用 vector 对象初始化数组。
如果想用 vector 对象初始化数组,则必须把 vector 对象的每个元素逐一赋值给数组。
*/
#include
#include
#include
#include
using namespace std;
int main()
{
const int sz = 10; // 常量 sz 作为 vector 对象的容量
vector<int> vInt;
srand( (unsigned) time (NULL) ); // 生成随机数种子
cout << "The vector content is: " << 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(); // 迭代器 it
int a[vInt.size()];
cout << "The array content is: " << endl;
// 利用范围for循环遍历数组的每个元素
for (auto &val : a)
{
val = *it; // 返回迭代器it所指元素的引用
cout << val << " ";
it++;
}
cout << endl;
return 0;
}
/* Output:
The vector content is:
30 75 11 69 82 51 46 40 23 81
The array content is:
30 75 11 69 82 51 46 40 23 81
*/
多维数组是数组的数组,一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小:
对于二维数组来说,常把第一个维度称作行,第二个维度称作列。
int ia[3][4]; // 大小为 3 的数组,每个元素是含有 4 个整数的数组
// 大小为 10 的数组,它的每个元素都是大小为 20 的数组,
// 这些数组的元素是含有 30 个整数的数组
int arr[10][20][30] = {
0}; // 将所有元素初始化为 0
多维数组的每一行分别用花括号括了起来:
int ia[3][4] = {
// 三个元素,每个元素都是大小为 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};
如果仅仅想初始化每一行的第一个元素,如下:
int ia[3][4] = {
{
0 }, {
4 }, {
8 }};
如果再省略掉内层的花括号,结果就大不一样:
// 显式地初始化第 1 行,其他元素执行值初始化
int ix[3][4] = {
0, 3, 6, 9};
// 用 arr 的首元素为 ia 最后一行的最后一个元素赋值
int ia[2][3]; // 大小为 2 的数组,每个元素是含有 3 个整数的数组
int arr[10][20][30]; // 大小为 10 的数组,它的每个元素都是大小为 20 的数组,这些数组的元素是含有 30 个整数的数组
ia[2][3] = arr[0][0][0];
/*等号右侧的运算对象包含 3 个维度。首先通过索引 0 得到最外层的数组,它是一个大小为 20 的多维数组;
接着获取这 20 个元素数组的第一个元素,得到一个大小为 30 的一维数组;
最后再取出其中的第一个元素。 */
int (&row)[4] = ia[1]; // 把row绑定到ia的第二个4元素数组上
// 把 row 定义成一个含有 4 个整数的数组的引用,然后将其绑定到 ia 的第 2 行
程序中经常会用到两层嵌套的for
循环来处理多维数组的元素:
constexpr size_t rowCnt = 3, colCnt = 4;
int ia[rowCnt][colCnt]; // 12 个未初始化的元素
// 对于每一行
for (size_t i = 0; i != rowCnt; ++i) {
// 对于行内的每一列
for (size_t j = 0; j != colCnt; ++j) {
ia[i][j] = i * colCnt + j;
}
}
用范围for
循环重写上面的代码:
size_t cnt = 0;
for (auto &row : ia) // 对于外层数组的每一个元素
for (auto &col : row) {
// 对于内层数组的每一个元素
col = cnt; // 将下一个值赋给该元素
++cnt; // 将 cnt 加 1
}
因为要改变元素的值,所以得把控制变量row
和col
声明成引用类型。
其实还有一个深层次的原因,就是如果不是引用类型,编译器初始化时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。
for (const auto &row : ia) // 对于外层数组中的每一个元素
for (auto col : row) // 对于内层数组的每一个元素
cout << col << endl;
这个循环中并没有任何写操作,课时我们还是将外层循环的控制变量声明成了引用类型,这是为了避免数组被自动转成指针。假设不是引用类型,则循环如下述形式:
for (auto row : ia)
for (auto col : row)
程序无法通过贬义,因为row
不是引用类型,所以编译器初始化row
时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样得到的row
的类型就是int*
,显然内层的循环就不合法了,编译器将试图在一个int*
内遍历。
Note: 要使用范围for
语句处理多维数组,除了嘴内层的循环外,其他所有循环的控制变量都应该是引用类型。
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; // 大小为 3 的数组,每个元素是含有 4 个整数的数组
int (*p)[4] = ia; // p 指向含有 4 个整数的数组
p = &ia[2]; // p 指向 ia 的尾元素
首先明确(*p)
意味着p
是一个指针。
接着观察右边发现,指针p
所指的是一个维度为4
的数组;
再观察左边,数组中的元素是整数。
因此,p
是指向含有4
个整数的数组的指针。
Note: 区别:
int *ip[4]; // 整型指针的数组
int (*ip)[4]; // 指向含有 4 个整数的数组
通过使用auto
或者declytpe
就能尽可能地避免在数组前面加上一个指针类型:
// 输出 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;
上面代码的分析:
外层的for
循环首先声明一个指针p
并令其指向ia
的第一个内层数组,然后依次迭代直到ia
的全部3
行都处理完为止。
其中递增运算++p
负责将指针p
移动到ia
的下一行。
内层的for
循环负责输出内层数组所包含的值。它首先令指针q
指向p
当前所在行的第一个元素。
*p
是一个含有4
个整数的数组,数组名被自动地转换成指向该数组收元素的指针。
内层for
循环不断迭代直到我们处理完了当前内层数组的所有元素为止。为了获取内层for
循环的终止条件,再一次解引用p
得到指向内层数组首元素的指针,给它加上4
就得到了终止条件。
使用标准库函数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;
}
代码分析:
这一版本的程序中,循环终止条件由end
函数负责判断。
虽然我们也能推断出p
的类型是指向含有4
个整数的数组的指针,q
的类型是指向整数的指针,但是使用auto
关键字我们就不必再烦心这些类型到底是什么了。
练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。
/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/
#include
using namespace std;
int main()
{
int ia[3][4] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
cout << "利用范围for语句输出多维数组的内容: " << endl;
for (int (&row)[4] : ia)
{
for (int &col : row)
cout << col << " ";
cout << endl;
}
cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
for (int i = 0; i != 3; i++)
{
for (int j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
for (int (*p)[4] = ia; p != ia + 3; p++)
{
for (int *q = *p; q != *p + 4; q++)
cout << *q << " ";
cout << endl;
}
return 0;
}
using int_array = int[4];
typedef int int_array[4];
// 输出 ia 中每个元素的值,每个内层数组各占一行
for (int_array *p = ia; p != ia + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << endl;
}
// 程序将类型“4 个整数组成的数组” 命名为 int_array,用类型名 int_array 定义外层循环的控制变量让程序显得简洁明了。
练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。
/* 练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。 */
/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/
#include
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 << "利用范围for语句输出多维数组的内容: " << endl;
// for (int (&row)[4] : ia)
for (int_array &row : ia)
{
for (int &col : row)
cout << col << " ";
cout << endl;
}
cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
for (int i = 0; i != 3; i++)
{
for (int j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
// for (int (*p)[4] = ia; p != ia + 3; p++)
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 关键字。
/* 练习3.45:再一次改写程序,这次使用 auto 关键字。*/
/* 练习3.44:改写上一个练习中的程序,使用类型别名来代替循环控制变量的类型。 */
/* 练习3.43:编写 3 个不同版本的程序,令其均能输出 ia 的元素。
版本1 使用范围for语句管理迭代过程;
版本2 和 版本3 都是用普通的 for 语句,
其中版本2 要求用下标运算符,版本3 要求用指针。
此外,在所有3个版本的程序中都要直接写出数据类型,而不能使用类型别名、auto 关键字或 decltype关键字。*/
#include
using namespace std;
int main()
{
int ia[3][4] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
cout << "利用范围for语句输出多维数组的内容: " << endl;
// for (int (&row)[4] : ia)
// for (int_array &row : ia)
for (auto &row : ia)
{
for (auto &col : row)
cout << col << " ";
cout << endl;
}
cout << "利用普通for语句和下标运算符输出多维数组的内容: " << endl;
for (auto i = 0; i != 3; i++)
{
for (auto j = 0; j != 4; j++)
cout << ia[i][j] << " ";
cout << endl;
}
cout << "利用普通for语句和指针输出多维数组的内容: " << endl;
// for (int (*p)[4] = ia; p != ia + 3; p++)
for (auto p = ia; p != ia + 3; p++)
{
for (auto q = *p; q != *p + 4; q++)
cout << *q << " ";
cout << endl;
}
return 0;
}