第3章 字符串、向量和数组
3.5节 数组
练习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[d],a是数组的名字,d是数组的维度(容量)。对数组维度的要求有两个,一是维度表示数组中元素的个数,因此必须大于0;二是维度属于数组类型的一部分,因此在编译时应该是已知的,必须是一个常量表达式。
【解答】
(a)是非法的,buf_size是一个普通的无符号数,不是常量,不能作为数组的维度。
(b)是合法的,4*7-14=14是一个常量表达式。
(c)是非法的,txt_size()是一个普通的函数调用,没有被定义为constexpr,不能作为数组的维度。
(d)是非法的,当使用字符串初始化字符数组时,默认在尾部添加一个空字符'\0',算上这个符号该字符串共有12个字符,但是字符数组st的维度只有11,无法容纳题目中的字符串。
需要指出的是,在某些编译器环境中,上面的个别语句被判定为合法,这是所谓的编译器扩展。不过一般来说,建议读者避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上失效。
练习3.28:下列数组中元素的值是什么?
string sa[10];
int ia[10];
int main() {
string sa2[10];
int ia2[10];
}
【出题思路】
本题旨在考查数组默认初始化的几种不同情况,如全局变量和局部变量的区别、内置类型和复合类型的区别。
【解答】
与练习2.10类似,对于string类型的数组来说,因为string类本身接受无参数的初始化方式,所以不论数组定义在函数内还是函数外都被默认初始化为空串。对于内置类型int来说,数组ia定义在所有函数体之外,根据C++的规定,ia的所有元素默认初始化为0;而数组ia2定义在main函数的内部,将不被初始化,如果程序试图拷贝或输出未初始化的变量,将遇到未定义的奇异值。
下面的程序可以验证上述分析:
#include
#include
using namespace std;
//定义在全局作用域中的数组
string sa[10];
int ia[10];
int main()
{
//定义在局部作用域中的数组
string sa2[10];
int ia2[10];
cout << " array output:" << endl;
for(auto c: sa)
cout << "* " << c << " ";
cout << endl;
for(auto c: sa2)
cout << "& " << c << " ";
cout << endl;
for(auto c: ia)
cout << "# " << c << " ";
cout << endl;
for(auto c: ia2)
cout << "@ " << c << " ";
cout << endl;
return 0;
}
运行结果:
练习3.29:相比于vector来说,数组有哪些缺点,请列举一些。
【出题思路】
数组与vector有一些类似之处,但是也有若干区别。
【解答】
数组与vector的相似之处是都能存放类型相同的对象,且这些对象本身没有名字,需要通过其所在位置访问。
数组与vector的最大不同是,数组的大小固定不变,不能随意向数组中增加额外的元素,虽然在某些情境下运行时性能较好,但是与vector相比损失了灵活性。
具体来说,数组的维度在定义时已经确定,如果我们想更改数组的长度,只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组中去。我们也无法像vector那样使用size函数直接获取数组的维度。如果是字符数组,可以调用strlen函数得到字符串的长度;如果是其他数组,只能使用sizeof(array)/sizeof(array[0])的方式计算数组的维度。
练习3.30:指出下面代码中的索引错误。
constexpr size_t array_size = 10;
int ia[array_size];
for(size_t ix = 1; ix <= arraay_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 < arraay_size; ++ix)
ia[ix] = ix;
练习3.31:编写一段程序,定义一个含有10个int的数组,令每个元素的值就是其下标值。
【出题思路】
通过循环为数组的元素赋值,通过范围for循环输出数组的全部元素。
【解答】
满足题意的程序如下所示:
#include
using namespace std;
int main()
{
const int sz = 10;
int vInt[sz];
//通过for循环为数组元素赋值
for(int i = 0; i < 10; ++i)
{
vInt[i] = i;
}
//通过范转for循环输出数组的全部元素
for(auto a: vInt)
{
cout << a << " ";
}
cout << endl;
return 0;
}
运行结果:
练习3.32:将上一题刚刚创建的数组拷贝给另外一个数组。利用vector重写程序,实现类似的功能。
【出题思路】
如果想把数组的内容拷贝给另一个数组,不能直接对数组使用赋值运算符,而应该逐一拷贝数组的元素。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 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;
}
运行结果:
练习3.33:对于104页的程序来说,如果不初始化scores将发生什么?
【出题思路】
本题旨在考查内置类型数组的初始化。
【解答】
该程序对scores执行了列表初始化,为所有元素赋初值为0,这样在后续统计时将会从0开始计算各个分数段的人数,是正确的做法。
如果不初始化scores,则该数组会含有未定义的数值,这是因为scores是定义在函数内部的整型数组,不会执行默认初始化。
练习3.34:假定p1和p2指向同一个数组中的元素,则下面程序的功能是什么?什么情况下该程序是非法的?
p1 += p2 - p1;
【出题思路】
指针的算术运算与vector类似,也可以执行递增、递减、比较、与整数相加、两个指针相减等操作。
【解答】
如果p1和p2指向同一个数组中的元素,则该条语句令p1指向p2原来所指向的元素。从语法上来说,即使p1和p2指向的元素不属于同一个数组,但只要p1和p2的类型相同,该语句也是合法的。如果p1和p2的类型不同,则编译时报错。
练习3.35:编写一段程序,利用指针将数组中的元素置为0。
【出题思路】
C++11新标准为数组引入了名为begin和end的两个函数,这两个函数与容器中的同名成员功能类似,利用begin和end可以方便地定位到数组的边界。令指针在数组的元素间移动,解引用指针即可得到当前所指的元素值。
【解答】
满足题意的程序如下所示:
#include
using namespace std;
int main()
{
const int sz = 10; //常量sz作为数组的维度
int a[sz];
//通过for循环为数组元素赋值
for(int 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对象是否相等。
【出题思路】
无论对比两个数组是否相等还是两个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;
srand((unsigned)time(NULL)); //生成随机数种子
//通过for循环为数组元素赋值
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
#include
#include
#include
using namespace std;
int main()
{
const int sz = 5; //常量sz作为数组的维度
vector a, b;
int i;
srand((unsigned)time(NULL)); //生成随机数种子
//通过for循环为数组元素赋值
for(i = 0; i < sz; ++i)
{
//每次循环生成一个10以内的随机数并添加到a中
a.push_back(rand() % 10);
}
cout << "系统数据已经生成,请输入您猜测的5个数字(0〜9),可以重复:" << endl;
int uVal;
//通过for循环为数组元素赋值
for(i = 0; i < sz; ++i)
{
if(cin >> uVal)
b.push_back(uVal);
}
cout << "系统生成的数据是:" << endl;
for(auto val : a)
{
cout << val << " ";
}
cout << endl;
cout << "您猜测的灵敏据是:" << endl;
for(auto val: b)
{
cout << val << " ";
}
cout << endl;
//令it1和it2分别指向vector对象a和b的首元素
auto it1 = a.cbegin();
auto it2 = b.cbegin();
while(it1 != a.cend() && it2 != b.cend())
{
if(it1 != it2)
{
cout << "您猜测错误,两个数组不相等" << endl;
return -1;
}
++it1; //it1向后移动一位
++it2; //it2向后移动一位
}
cout << "恭喜您全都猜对了!" << endl;
return 0;
}
运行结果:
练习3.37:下面的程序是何含义,程序的输出结果是什么?
const char ca[] = {'h', 'e', 'l', 'l', 'o'};
const char *cp = ca;
while(*cp){
cout << *cp << endl;
++cp;
}
【出题思路】
考查C风格字符串和字符数组的关系,尤其是串尾是否含有空字符的问题。C风格字符串与标准库string对象既有联系又有区别。
【解答】
程序第一行声明了一个包含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;
}
或者修改为如下形式也能达到预期效果:
const char ca[] = "hello";
const char *cp = ca;
while(*cp){
cout << *cp << endl;
++cp;
}
练习3.38:在本节中我们提到,将两个指针相加不但是非法的,而且也没什么意义。请问为什么两个指针相加没什么意义?
【出题思路】
与标准库vector类似,C++也为指针定义了一系列算术运算,包括递增、递减、指针求差、指针与整数求和等,但是并没有定义两个指针的求和运算。要想理解这一规定,必须首先明白指针的含义。
【解答】
指针也是一个对象,与指针相关的属性有3个,分别是指针本身的值(value)、指针所指的对象(content)以及指针本身在内存中的存储位置(address)。
它们的含义分别是:指针本身的值是一个内存地址值,表示指针所指对象在内存中的存储地址;指针所指的对象可以通过解引用指针访问;因为指针也是一个对象,所以指针也存储在内存的某个位置,它有自己的地址,这也是为什么有“指针的指针”的原因。
通过上述分析我们知道,指针的值是它所指对象的内存地址,如果我们把两个指针加在一起,就是试图把内存中两个对象的存储地址加在一起,这显然是没有任何意义的。与之相反,指针的减法是有意义的。如果两个指针指向同一个数组中的不同元素,则它们相减的结果表征了它们所指的元素在数组中的距离。
练习3.39:编写一段程序,比较两个string对象。再编写一段程序,比较两个C风格字符串的内容。
【出题思路】
由于标准库string类定义了关系运算符,所以比较两个string对象可以直接使用<、>、==等;比较两个C风格字符串则必须使用cstring头文件中定义的strcmp函数。
【解答】
比较两个string对象的程序如下所示:
#include
#include
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;
}
运行结果:
比较两个C风格字符串的程序如下所示,其中的分支部分选用了switch-case语句,其效果与上一个程序的if-else语句非常类似。
#include
#include
using namespace std;
int main()
{
char str1[80], str2[80];
cout << "请输入两个字符串:" << endl;
cin >> str1 >> str2;
//利用cstring头文件中定义的strcmp函数比较大小
auto result = strcmp(str1, str2);
cout << "result================" << result << endl;
if(result > 0)
cout << "第一个字符串大于第二个字符串" << endl;
else if(result < 0)
cout << "第一个字符串小于第二个字符串" << endl;
else if(0 == result)
cout << "两个字符串相等" << endl;
else
cout << "未定义的结果" << endl;
return 0;
}
运行结果:
练习3.40:编写一段程序,定义两个字符数组并用字符串字面值初始化它们;接着再定义一个字符数组存放前两个数组连接后的结果。使用strcpy和strcat把前两个数组的内容拷贝到第三个数组中。
【出题思路】
C风格字符串的操作函数定义在cstring头文件中。其中,strcpy函数负责把字符串的内容拷贝给另一个字符串,strcat函数则负责把字符串的内容拼接到另一个字符串之后。此外,strlen函数用于计算字符串的长度。
需要注意的是,利用字符串字面值常量初始化C风格字符串时,默认在数组最后添加一个空字符,因此,strlen的计算结果比字面值显示的字符数量多1。为了细致起见,计算两个字符串拼接后的长字符串长度时,应该在两个字符串各自长度求和后减去1,即减去1个多余空字符所占的额外空间。
【解答】
满足题意的程序如下所示:
#include
#include
using namespace std;
int main()
{
char str1[] = "Welcome to ";
char str2[] = "C++ family!";
//利用strlen函数计算两个字符串的长度,并求得结果字符串的长度
char result[23];
//char result[strlen(str1) + strlen(str2) - 1];//表达式的计算结果不是常量
strcpy(result, str1); //把第一个字符串拷贝到结果字符串中
strcat(result, str2); //把第二个字符串拼接到结果字符串中
cout << "第一个字符串是: " << str1 << endl;
cout << "第二个字符串是: " << str2 << endl;
cout << "拼接后的字符串是: " << result << endl;
return 0;
}
运行结果:
练习3.41:编写一段程序,用整型数组初始化一个vector对象。
【出题思路】
C++不允许用一个数组初始化另一个数组,也不允许使用vector对象直接初始化数组,但是允许使用数组来初始化vector对象。要实现这一目的,只需要指明要拷贝区域的首元素地址和尾后地址。
【解答】
满足题意的程序如下所示。使用随机数初始化数组,然后利用begin和end获得数组的范围。在用数组初始化vector对象时,只需要提供数组的元素区域。
#include
#include
#include
#include
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 vInt(begin(a), end(a));
cout << "vector的内容是:" << endl;
//利用范围for循环遍历vector的每个元素
for(auto val: vInt)
{
cout << val << " ";
}
cout << endl;
return 0;
}
运行结果:
练习3.42:编写一段程序,将含有整数元素的vector对象拷贝给一个整型数组。
【出题思路】
C++允许使用数组直接初始化vector对象,但是不允许使用vector对象初始化数组。如果想用vector对象初始化数组,则必须把vector对象的每个元素逐一赋值给数组。
【解答】
满足题意的程序如下所示:
#include
#include
#include
#include
using namespace std;
int main()
{
const int sz = 10; //常量sz作为vector对象的容量
vector 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[sz];
cout << "数组的内容是:" << endl;
//利用for循环遍历数组的每个元素
for(auto &val: a)
{
val = *it;
cout << val << " ";
++it;
}
cout << endl;
return 0;
}
运行结果: