内容简介
1、第一部分第九课:数组威武,动静合一
2、第一部分第十课预告:文件读写,海阔凭鱼跃
数组威武,动静合一
上一课《【C++探索之旅】第一部分第八课:传值引用,文件源头》中,我们学习了函数参数的不同传递形式:值传递和引用传递,也学习了如何用头文件和源文件来更好地组织项目。
在不少程序中,我们都需要使用多个相同类型的变量。例如:一个网站的用户名列表(一般是string类型);或者一场比赛的前10个最佳得分(一般是int类型)。
类似地,C++和大多数编程语言一样,也有将多个相同类型的数据组合在一起的方法。就像这一课的标题所述,我们将这种数据形式称为 数组。
我们将学习两种类型的数组。一种数组是预先知道其中所包含的元素数目的,例如一场比赛的前10个最佳得分;另一种数组的元素数目会变化,例如一个网站的用户名列表。
聪明如你应该料想到了:预先知道元素数目的数组使用起来相对简单,因此我们就由这种类型入手。
静态数组
在这一课的介绍中,我们说了数组的大致用途:
俗语说:好记性不如烂笔头。我觉得:好解释不如烂例子。因此,我们就来举个“栗子”:
实例
假如你要显示一项比赛的前五个最高得分,那么一般需要两个列表:
因此,我们要声明10个变量
例如:
string bestPlayerName1("Albert Einstein");
string bestPlayerName2("Isaac Newton");
string bestPlayerName3("Marie Curie");
string bestPlayerName4("Thomas Edison");
string bestPlayerName5("Alfred Nobel");
int bestScore1(118218);
int bestScore2(100432);
int bestScore3(87347);
int bestScore4(64523);
int bestScore5(31415);
要显示这5个分数信息也不容易:
cout << "1) " << bestPlayerName1 << " " << bestScore1 << endl;
cout << "2) " << bestPlayerName2 << " " << bestScore2 << endl;
cout << "3) " << bestPlayerName3 << " " << bestScore3 << endl;
cout << "4) " << bestPlayerName4 << " " << bestScore4 << endl;
cout << "5) " << bestPlayerName5 << " " << bestScore5 << endl;
可以看到,光是显示5个最高分数就已如此劳师动众。假如要显示的是100个最高分数,那就要声明200个变量(100个参赛者名字和100个最高分数),显示这些信息又要再写100行cout… 宝宝心里苦,但宝宝不说。
因此我们迫切需要数组的帮忙:借着数组,我们可以一次性声明100个最高分数和100个参赛者的名字。在内存中,我们可以一次性申请储存100个int型变量和100个string类型变量的空间。很棒,不是吗?
当然了,同一个数组里的元素之间要有相关性,这样才能正确规划我们的程序。把你家的狗狗的年龄和上网人数放到同一个数组,应该不太合适吧,虽然它们都是int类型的变量。
在上例中,我们说了,需要各申请100个int变量和100个string变量。因此,我们需要两个数组来分别存放。100就是这两个数组的大小。这种大小在整个程序中不会改变的数组,我们称之为静态数组。暂时我们只需要用静态数组就够了。
声明静态数组
在C++中,一个变量基本由名字和类型来做标识。既然数组就是相同变量的集合,这个规则仍然成立。只不过我们还需要加上另一个标识:数组的大小。
声明一个数组就和声明一个变量类似。如下:
类型 名字[大小];
由左到右依次写:类型,名字,中括号,
看以下实例:
#include <iostream>
using namespace std;
int main()
{
int bestScores[5]; //声明包含5个int型变量的数组,名字是bestScores
double angles[3]; //声明包含3个double型变量的数组,名字是angles
return 0;
}
我们再用内存图解来更好地理解吧:
在上图中,我们看到,内存里分配了两个大的空间,各自的标签是bestScores和angles,一个有5个"抽屉",一个有3个"抽屉"。暂时,我们还没对这些抽屉里的变量赋值,因此它们的值是任意的,正如上图中用问号所示。
我们也可以将一个const变量作为数组的大小,只要这个const变量的类型是int或unsigned int。例如:
int const arraySize(20); //数组的大小是20
double angles[arraySize];
如果要作为静态数组的大小,必须使用const变量,如果用一般的变量,是会出错的。
建议大家尽量不要用数字来作为数组的大小,用const变量是比较好的习惯。
好了,既然我们在内存中申请好了空间,那么就只剩使用啦。
访问数组的元素
数组中的每一个元素既然就是一个变量,那么其使用方式就和一般的变量没有区别。不过要访问数组中的这些元素,要使用比较特殊的方法:指明数组的名字和元素的号码。之前的例子中,我们声明了 int bestScores[5];
因此bestScores这个数组有5个元素,从第一个到第五个依次编号。
为了访问这些元素,我们使用这样的格式:数组名[元素号码]
这里的元素号码,术语称为 下标。
注意:数组中的第一个元素的下标是0,而不是从1开始的。
因此第一个元素的下标就是0,第二个元素的号码是1,依次递增。
假如我要访问数组bestScores中的第3个元素,我就要这么用:
bestScores[2] = 5;
因此,我们如果要像之前的示例程序一样为bestScores的每一个元素赋值,需要这样做:
int const bestScoreNumber(5); // 数组的大小
int bestScores[bestScoreNumber]; // 声明数组
bestScores[0] = 118218; // 填充数组的第1个元素
bestScores[1] = 100432; // 填充数组的第2个元素
bestScores[2] = 87347; // 填充数组的第3个元素
bestScores[3] = 64523; // 填充数组的第4个元素
bestScores[4] = 31415; // 填充数组的第5个元素
遍历数组
数组的一个强大之处就在于:我们可以用循环来很方便地遍历数组中的元素。
既然对于静态数组,我们预先知道数组的大小,那么我们就可以用一个for循环来遍历。
#include <iostream>
#include <string>
using namespace std;
int main ()
{
int const bestScoreNumber(5); // 数组的大小
int bestScores[bestScoreNumber]; // 声明数组
bestScores[0] = 118218; // 填充数组的第1个元素
bestScores[1] = 100432; // 填充数组的第2个元素
bestScores[2] = 87347; // 填充数组的第3个元素
bestScores[3] = 64523; // 填充数组的第4个元素
bestScores[4] = 31415; // 填充数组的第5个元素
for (int i(0); i < bestScoreNumber; ++i)
{
cout << bestScores[i] << endl;
}
return 0;
}
变量i的值依次成为0, 1, 2, 3, 4,因此被传递给cout的值依次是bestScores[0],bestScores[1],直到bestScores[4]。
注意:数组元素的下标一定不能超过数组元素数目减1,不然会报数组下标越界错误。例如上面数组的大小是5,因此数组元素的下标最大是4。
你慢慢会发现,在C++编程中,数组和for循环的组合会成为你经常使用的好工具。
数组和函数
我希望你没忘了函数是什么吧,不然就太伤偶的心了...
不论如何,我们都要来复习一下函数。
不过接下来,你将会看到:静态数组和函数并不是那么要好的朋友。
第一个限制就是:我们不能创建一个返回静态数组的函数,做不到。
第二个限制是:静态数组作为函数参数的时候,总是以引用的方式来传递的。而且并不需要加&符号:都是自动的。这就是说,当我们把一个数组传递给函数作参数时,函数可以修改此数组。
以下就是一个参数是数组的函数的样式:
void function(int array[])
{
//…
}
如上所示,数组的中括号里并不加数组的大小。
但是前面说过了,我们遍历一个数组需要知道它的大小。上面的函数样式中,我们并不能知道数组的大小,因此只能再加一个参数:函数大小。如下所示:
void function(double array[], int arraySize)
{
//…
}
我知道,这有点令人沮丧。但是不能怪我啊,毕竟并不是我发明C++的。
为了稍作练习,希望大家写一个函数,用来计算数组中所有元素的平均值。
以下是我的版本:
/*
* 计算数组元素的平均值的函数
* - array : 要计算其平均值的数组
* - arraySize : 数组的大小
*/
double average(double array[], int arraySize)
{
double average(0);
for(int i(0); i<arraySize; ++i)
{
average += array[i]; //将数组的所有元素相加
}
average /= arraySize;
return average;
}
关于静态数组,我们已经聊得差不多了,该说说动态数组了。
动态数组
之前说过,我们会介绍两种数组:一种是静态数组,数组的大小是固定不变的;另一种的大小却可以改变,这种类型的数组就是动态数组。不过既然都是数组,有些概念还是通用的。
声明动态数组
静态数组和动态数组有通用的地方,但也有很多不同。
第一个不同之处就是在程序的最开始,我们需要加一行
#include <vector>
这样我们才能使用动态数组。(当然,一般应该用new的方法来创建动态数组,我们要到第二部分:面向对象 时才会讲,暂时只用vector来代表动态数组)
vector这个英语单词本身的意思是"向量,矢量"的意思。
第二个不同点在于数组的声明方式。
声明动态数组的格式如下:
vector<类型> 名字(大小);
例如,声明一个包含5个int型变量的动态数组,我们可以这样做:
#include <iostream>
#include <vector> // 不要忘记
using namespace std;
int main()
{
vector<int> array(5);
return 0;
}
需要区分三点:
-
动态数组的情况中,类型不再是放在最前面了,这与变量和静态数组的声明方式不同。一开始先写vector这个关键字。
-
我们使用了一对奇怪的尖括号<>,其中写入类型
-
我们把数组的大小写在圆括号内,而之前静态数组时是将数组大小写在中括号内。
这也正说明了,动态数组和静态数组还是有差别的。不过,你将看到,遍历数组的方式还是类似的。
在此之前,有两个小窍门需要了解。
-
我们可以快捷地直接填充动态数组的所有元素,只需要在圆括号里加上第二个参数就可以了,如下:
vector<int> array(5, 3); //创建一个动态数组,包含5个int型变量,值都为3
vector<string> nameList(12, "无名氏"); //创建一个动态数组,包含12个string型变量,值都为"无名氏"
-
我们可以声明一个没有元素的动态数组,只需要不加圆括号就行了:
vector<double> array; // 创建一个类型为double的动态数组,0个元素
那你要问了:创建0个元素的动态数组的意义是?
还记得动态数组的性质了吗?数组的大小可变化。因此我们可以之后再往里面添加元素啊。等下你就知道了。
访问动态数组的元素
尽管动态数组的声明方式和静态数组很不同,但要访问其元素,方法又类似了。我们重新使用中括号,第一个元素的下标也是从0开始。
因此,我们可以重写我们之前的例子,用动态数组vector:
int const bestScoreNumber(5); // 数组的大小
vector<int> bestScores(bestScoreNumber); // 声明动态数组
bestScores[0] = 118218; // 填充数组的第1个元素
bestScores[1] = 100432; // 填充数组的第2个元素
bestScores[2] = 87347; // 填充数组的第3个元素
bestScores[3] = 64523; // 填充数组的第4个元素
bestScores[4] = 31415; // 填充数组的第5个元素
改变动态数组的大小
来到动态数组的知识点中的关键之处了:改变数组大小。
首先,学习如何在数组末尾处加元素吧。
我们需要用到push_back()这个函数。用法如下:
vector<int> array(3,2); //创建一个动态数组,名字是array,大小是3,每个元素都是int型变量,且值都为2
array.push_back(8); //往数组末尾添加一个新的元素(第4个),其值为8
用内存图来解释一下上面发生了什么吧:
可以看到,一个新的元素被添加到了数组尾处。
当然了,我们可以用push_back函数来添加多个元素,如下:
vector<int> array(3,2); //创建一个动态数组,名字是array,大小是3,每个元素都是int型变量,且值都为2
array.push_back(8); //往数组末尾添加一个新的元素(第4个),其值为8
array.push_back(7); //往数组末尾添加一个新的元素(第5个),其值为7
array.push_back(14); //往数组末尾添加一个新的元素(第6个),其值为14
//现在,数组中包含的元素是: 2 2 2 8 7 14
难道vector动态数组只能插入元素而不能删除元素吗?
当然不可能啦。C++的作者早就想到了。
我们可以用pop_back函数将动态数组的最后一个元素删去。用法和push_back函数类似,只不过pop_back函数的圆括号中没有参数罢了。
vector<int> array(3,2);
array.pop_back(); //删去一个元素,只剩下2个了
array.pop_back(); //删去一个元素,只剩下1个了
当然也不要删过头了,毕竟一个动态数组不能包含少于0个元素。
既然动态数组的大小是可变的,那么我们怎么即时知道其大小呢?幸好,vector中有一个函数可以使用:size(),其可以返回数组的大小。
vector<int> array(5,4); //包含5个值为4的int型变量的数组
int const size(array.size()); //size这个const变量就是array数组的大小,因此值为5
动态数组和函数
相比于静态数组,把动态数组传递给函数作为参数要容易得多。
因着size()函数,我们就不必添加第二个参数来指明数组的大小了。
如下所示:
//一个参数是int型动态数组的函数
void function(vector<int> array)
{
//…
}
很简便不是吗?但我们可以做得更好。
以前的课程中,我们说过引用传递可以防止拷贝,就可以在一定程度上优化代码。事实上,如果数组包含很多元素,那么如果要拷贝就会很花时间。因此我们可以使用这个诀窍,如下:
//一个参数是int型动态数组的函数
void function(vector<int> const& array)
{
//…
}
因为用了const,此动态数组在函数中就不能被改变;因为用了&这个引用符号,此动态数组就不会被拷贝了。
而要调用一个参数是动态数组的函数,也很简单:
vector<int> array(3,2);
function(array); //将动态数组传递给上面定义的函数
之前我们学习了使用.h头文件来储存函数的原型,因此我们需要这样用:
#ifndef ARRAY_H_INCLUDED
#define ARRAY_H_INCLUDED
#include <vector> // 引入vector头文件
void function(std::vector<int>& array); // 须要在vector前面加上std::
#endif // ARRAY_H_INCLUDED
当然,我们也可以创建返回值是动态数组的函数。我相信你已经知道怎么做了:
vector<double> function(int a)
{
//...
}
多维数组
我们可以创建int型数组,也就是说数组中的各个元素是int变量。我们也可以创建double型数组,string型数组。
我们甚至可以创建数组的数组。
我料想你也许皱起了眉头,想着:数组的数组,这是什么东东,干什么用呢?
我们首先从内存图解来慢慢理解吧:
上图中黄色的大格子,就是一个数组变量。这个数组由5个分开的更小的格子组成,而每个更小的格子里又各有4个小格子。
我们把数组的数组称为多维数组。之前我们接触的数组都是一维的。
声明多维数组
要声明这样的多维数组,我们就需要用到多个中括号了,将不同维度的对应大小写在中括号里,一个接一个。如下:
类型 数组名[大小1][大小2];
因此,为了声明如上面内存图中所示的二维数组,我们可以这么做:
int array[5][4];
或者,更优化一些,用到const变量:
int const sizeX(5);
int const sizeY(4);
int array[sizeX][sizeY];
访问多维数组的元素
我相信接下来不需要过多解释,你也猜到如何访问多维数组的元素了吧。
例如array[0][0]就是上面的黄色数组中左下角的格子;array[0][1]对应的就是它上面的那个格子;array[1][0]对应的就是它右边的那个格子。依次类推。
那么如何访问右上角那个格子呢?
也不难,就是array[4][3]
进一步探究
当然,我们可以创建三维,四维,甚至更多维的数组。例如:
double superArray[5][4][6][2][7];
上面这个数组我可不想把它画出来,因为太难了。不过,实际写程序的时候,我们甚少会用到多于三维的数组。
上面所示的多维数组,是静态的多维数组。我们也可以创建大小可变的多维数组,这就要用到vector了。比如要创建一个二维的动态数组,可以这样写:
vector<vector<int> > array;
array.push_back(vector<int>(5)); //向二维数组中添加一行,这一行包含5个元素
array.push_back(vector<int>(3,4)); //向二维数组中添加一行,这一行包含3个元素,每个元素的值为4
多维动态数组的每一行都可以有不同的大小。我们依然可以用下标来访问不同的元素。
array[0].push_back(8); //在第一行中添加一个值为8的元素
和多维静态数组一样,多维动态数组的元素也可以如下访问,不过需要确认此元素存在:
array[2][3] = 9; //改变第3行第4列的元素的值为9
可以看到,多维动态数组并不那么好用,而且并不是有效使用内存的方式。
一般对于多维数组,我们还是使用静态数组比较多。
字符串:字符数组
在结束这一课之前,还需要偷偷告诉你个事:我们之前一直用的字符串其实就是数组!
我们在string类型变量的声明时,并不易窥见此。但其实字符串就很类似字符的数组,而且和vector(动态数组)很类似。
当然了,之后学到第二部分(面向对象),会知道string其实是一个类。暂不深究。
访问字符串中的字符
了解字符串其实是字符数组可以使我们学会如何访问其中的每一个字符,甚至改写它们。我们还是用下标来访问。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string userName("Julien");
cout << "你是 " << userName<< "." <<endl;
userName[0] = 'L'; //改变第1个字符
userName[2] = 'c'; //改变第3个字符
cout << "啊,不对,你是 " << userName<< "!" << endl;
return 0;
}
运行,输出:
你是 Julien.
啊,不对,你是 Lucien.
很厉害,不是吗?但是我们可以做得更好。
字符串相关函数
我们可以用size()函数来得知string变量中字符的数目,用push_back()函数向string变量最后添加字符。就和操作vector一样。
string text("All People Seem To Need Data Processing"); //39个字符
cout << "这个句子包含 " << text.size() << "个字符." << endl;
我们也可以用+=符号来一次性添加多个字符到字符串中。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string firstName("Albert");
string lastName("Einstein");
string fullName; //空的字符串
fullName += firstName; //将名字加入空字符串
fullName += " "; //接着加入一个空格
fullName += lastName; //最后加入姓
cout << "你叫 " << fullName << "." << endl;
return 0;
}
运行,输出:
你叫 Albert Einstein.
总结
-
数组是内存中相同类型数据元素的集合,这些元素在内存地址中一个接着一个
-
一个静态数组是这样初始化的:int bestScore[5]; (包含5个元素)
-
数组中第一个元素的下标是0(例如 bestScore[0])
-
如果数组的大小(元素个数)有可能会变动,那么就须要创建动态数组,用vector:vector<int> array(5);
-
我们可以创建多维数组,例如:int array[5][6]; 就创建了一个5行6列的二维数组.
-
字符串其实可以被看作字符的数组.其中每一个元素就是一个字符.
第一部分第十课预告
今天的课就到这里,一起加油吧!
下一课我们学习:文件读写,海阔凭鱼跃