第十七章 数组(二)
17.1 数组与内存变量需要占用内存空间,内存空间有地址。不同数据类型的变量,可能占用不同的内存大小及有不同的内存结构。 以前我们所学都称为“简单数据类型”,如:int,char,float,double,bool。像 char,bool,只占用一个字节,所以我们不去管它的的“结构”,其余如int,float,double占用多个字节,但比较简单,适当的时候我们会去探讨4个字节是如何组成一个整数。 后来我们学习了数组。数组变量占用内存的大小是不定的,因为不同的数组变量除了类型可以不同,还可以拥有不同个数的元素,这两点都影响它的大小。
因此,数组是我们第一个要着力研究它的结构的数据类型。和后面我们还要学习的更多数据类型相比,数组的结构还是相当简单的。简单就简单在它的各个元素大小一致,整整齐齐地排列。
17.1.1 数组的内存结构变量需要占用内存空间,内存空间有地址。
声明一个整型变量
int a;
系统会为该变量申请相应大小的空间,一个int类型的变量时,需要占用4个字节的空间,如下图: 也就是说,一个 int 类型的变量,它的内存结构就是 “4个连续的字节”。
当我们声明一个数组:int arr[100];
我们可以想像,arr数组在内存中占用了100 * sizeof(int) 个字节。
现在请大家打开Windows的画笔程序,家画一个数组的内存结构示意图。
17.1.2 数组的内存地址
一个int类型变量,占用4个字节的内存,其中第一个字节的位置,我们称为该变量的内存地址。 同样,一个数组变量,占用一段连续的内存,其中第一个字节的位置,我们称为该数组变量的内存地址。
还记得 & 这个符号吗?通过它我们可以得到指定变量的内存地址。
int a; cout << &a << endl;
& 称为“取址符”。如果你有点记不清,可以查看以前的课程。
本章第一个需要你特别注意的内容来了: 查看数组变量的地址,不需要使用 & 。下面的话是一个原因也是一个结论,你必须记住。
C,C++语言中,对数组变量的操作,就相当于直接对该数组变量的地址的操作。
因此,想要查看一个数组变量的地址,代码为:
int arr[10]; cout << arr << endl; //注意,arr之前无需 & 。
现在,请大家打开CB,然后将上面代码写成完整的一个控制台程序,看看输出结果。
17.1.3 数组元素的内存地址
一个数组变量包含多个连续的元素,每一个元素都是一个普通变量。因此,对就像对待普通变量一样可以通过&来取得地址:
//查看数组中第一个元素的地址: int arr[10]; cout << &arr[0] << endl;
例一: 现在,请大家在CB里继续上一小节的代码,要求:用一个for循环,输出数组arr中每一个元素的地址。 如果你已完成,现在来看我的答案。
#include <iostream.h> ... int arr[10];
for(int i=0; i<10; i++) cout << &arr[i] << endl; ... cin.get();
我们把它和前面输出数组地址的例子结合起来,然后观察输出结果。
... int arr[10];
//输出数组的地址: cout << "数组arr的地址: " << arr << endl;
//输出每个元素的地址: for(int i=0; i<10; i++) cout << "元素arr[" <<i <<"]的地址:" << &arr[i] << endl;
...
输出结果:
第一个要注意的的是头两行告诉我们,整个数组变量arr的地址,和第一个元素arr[0],二者的地址完全一样。 事实上,数组和元素,是对同一段内存的两种不同的表达。把这一段内存当成一个整体变量,就是数组,把这段内存分成大小相同的许多小段,就是一个个数组元素。
请参看下图: (分开一段段看是一个个元素,整体看称为一个数组,但二者对应的是同一段内存)
第二个要注意的,大家算算相邻的两个元素之间地址差多少?比如 &arr[1] - &arr[0] = 1245028 - 1245024 = 4个字节。这4字节,就是每个数组元素的大小。当然,这里是int类型,所以是4字节,如果是一个char或bool 类型的数组,则每个元素的大小是1。
根据这两点,我来提几个问题:
1、如果知道某个int类型数组的地址是 1245024,请问下标为5的元素的地址是多少? 2、如果知道某个char类型的数组,其下标为4的元素地址为:1012349,请问下标为2的元素地址是多少?
由于可通过 sizeof() 操作来取得各类型数据的大小,所以我们可以假设有一数组:
T arr[N]; //类型为T,元素个数为N。
存在: &arr[n] = arr + sizeof(T) * n ; (0 <= n < N) 或者: &arr[n] = arr + sizeof(arr[0]) * n; (0 <= n < N)
17.1.4 数组访问越界
上一章我们说过“越界”。由于这一问题的重要性,我们需要专门再说一回。
越界?越谁的界?当然是内存。一个变量存放在内存里,你想读的是这个变量,结果却读过头了,很可能读到了另一个变量的头上。这就造成了越界。有点像你回家时,走过了头,一头撞入邻居家……后果自付。
数组这家伙,大小不定!所以,最容易让程序员走过头。
我们通过数组的下标来得到数组内指定索引的元素。这称作对数组的访问。 如果一个数组定义为有n个元素,那么,对这n个元素(0 到 n-1)的访问都合法,如果对这n个元素之外的访问,就是非法的,称为“越界”。 比如,定义一个数组:
int arr[10];
那么,我们可以访问 arr[0] ~ arr[9] 这10个元素。如果你把下标指定为 10 ,比如:
int a = arr[10]; //访问了第11个元素。
这就造成了数组访问越界。
访问越界会出现什么结果了? 首先,它并不会造成编译错误! 就是说,C,C++的编译器并不判断和指出你的代码“访问越界”了。这将很可怕,也就是说一个明明是错误的东西,就这样“顺利”地通过了编译,就这样不知不觉地,一个BUG,“埋伏”在你的程序里。 更可怕的是,数组访问越界在运行时,它的表现是不定的,有时似乎什么事也没有,程序一直运行(当然,某些错误结果已造成);有时,则是程序一下子崩溃。 不要埋怨编译器不能事先发现这个错误,事实上从理论上编译过程就不可能发现这类错误。也不要认为:“我很聪明,我不会犯这种错误的,明明前面定义了10个元素,我不可能在后面写出访问第11个元素的代码!”。
请看下面的代码:
int arr[10];
for(int i=1; i<=10; i++) { cout << arr[i]; }
它就越界了,你看出原因了吗?
再说上一章的成绩查询。我们让用户输入学生编号,然后查该学生的成绩。如果代码是这样:
int cj[100]; ...
//让用户输入学生编号,设现实中学生编号由1开始: cout << "请输入学生编号(在1~100之间):" int i; cin >> i;
//输出对应学生的成绩: cout << cj[i-1];
这段代码看上去没有什么逻辑错误啊。可是,某些用户会造成它出错。听话的用户会乖乖地输入1到100之间数字。而调皮的用户呢?可能会输入101,甚至是-1 —— 我向来就是这种用户 ——这样程序就会去尝试输出:cj[100] 或 cj[-2]。 解决方法是什么?这里有一个简单,只要多写几个字:
... cout << "请输入学生编号(在1~100之间 如果不输入这个范围之内数,计算机将爆炸!):" int i; cin >> i; ...
系主任在使用你的这个程序时,十个指头一定在不停地颤抖…… 理智的作法还是让我们程序员来负起这个责任吧,我们需要在输出时,做一个判断,发现用户输入了不在编号范围之内的数,则不输出。正确答案请看上章。
为什么数组访问越界会造成莫名其妙的错误? 前面一节我们讲到数组占用了一段连续的内存空间。然后,我们可以通过指定数组下标来访问这块内存里的不同位置。因此,当你的下标过大时,访问到的内存,就不再是这个数组“份内”的内存。你访问的,将是其它变量的内存了。 前面不是说数组就像一排的宿舍吗?假设有5间,你住在第2间;如果你晚上喝多了,回来时进错了房间,只要你进的还是这5间,那倒不会有大事,可是若是你“越界”了。竟然一头撞入第6间……这第6间会是什么?很可能它是走廊的尽头,结果你一头掉下楼,这在生活中是不幸,可对于程序倒是好事了,因为错误很直接(类似直接死机),你很容易发现。可是,如果第6间是??据我所知,第6间可能是小便处,也可能是女生宿舍。
17.2 二维数组
事实要开始变得复杂。 生活中,有很多事物,仅仅用一维数组,将无法恰当地被表示。还是说学生成绩管理吧。一个班级30个学员,你把他们编成1到30号,这很好。但现在有两个班级要管理怎么办?人家每个班级都自有自的编号,比如一班学生编是1~30;二班的学生也是1~30。你说,不行,要进行计算机管理,你们两班学员的编号要混在一起,从1号编到60号。
另外一种情况,仍然只有一个班级30人。但这回他们站到了操场,他们要做广播体操,排成5行6列。这时所有老师都不管学员的编号了,老师会这样喊:“第2排第4个同学,就说你啦!踢错脚了!”。假设我们的校长大人要坐在校长室里,通过一个装有监视器的电脑查看全校学员做广播体操,这时,我们也需要一个多维数组。
17.2.1 二维数组基本语法
语法:定义一个二维数组。
数据类型 数组名[第二维大小][第一维大小];
举例:
int arr[5][6]; //注意,以分号结束。
这就是操场上那个“5行6列的学生阵”。当然,哪个是行哪个列凭你的习惯。如果数人头时,喜欢一列一列地数,那你也可以当成它是“5列6行”——台湾人好像有这怪僻——我们还是把它看成5行6列吧。
现在:
第一排第一个学员是哪个?答:arr[0][0]; 第二排第三个学员是?答:arr[1][2];
也不并不困难,对不?惟一别扭的其实还是那个老问题:现实上很多东西都是从1开始计数,而在C里,总是要从0开始计数。
接下来,校长说,第一排的全体做得很好啊,他们的广播体操得分全部加上5分!程序如何写?答:
for(int col=0; col<6; col++) { arr[0][col] += 5; }
对了,这里我没有用 i 来作循环的增量,而是用col。因为col在英语里表示“列”,这样更直观对不?下面要用到行,则用row。
广播操做到“跳跃运动”了,校长大人在办公室蹦了两下,感觉自已青春依旧,大为开心,决定给所有学员都加1分,程序如何写?答:
for(int row = 0; row < 5; row++) { for(int col = 0; col < 6; col++) { arr[row][col] += 1; } }
看明白了吗?在二维数组,要确定一个元素,必须使用两个下标。 另外,这个例子也演示了如何遍历一个二维数组:使用双层循环。第一层循环让row 从 0到 4, 用于遍历每一行;col从0到5,遍历每一行中的每一列。 (遍历:访问某一集合中的每一个元素的过程)
大家把这两个程序都实际试一试.
17.2.2 二维数组初始化
一维数组可以定义时初始化:
int arr[] = {0,1,2,3,4};
二维数组也可以:
int arr[5][6] = { { 0, 1, 2, 3, 4, 5}, {10,11,12,13,14,15}, {20,21,22,23,24,25}, {30,31,32,33,34,35}, {40,41,42,43,44,45}, }; //注意,同样以分号结束
初始化二维数组使用了两层{},内层初始化第一维,每个内层之间用逗号分隔。
例二:
我们可以把这个数组通过双层循环输出:
for(int row = 0; row < 5; row++) { for(int col = 0; col < 6; col++) { cout << arr[row][col] << endl; } }
这段代码会把二维数组arr中的所有元素(5*6=30个),一行一个地,一古脑地输出,并不适于我们了解它的二维结构。我们在输出上做些修饰:
for(int row = 0; row < 5; row++) { cout << "第" << row + 1 << "行: "
for(int col = 0; col < 6; col++) { cout << arr[row][col] << ","; //同一行的元素用逗号分开 }
cout << endl; //换行 }
请大家分别上机试验这两段代码,对比输出结果,明白二维数组中各元素次序。下面是完整程序中,后一段代码的输出:
现在说初始化时,如何省略指定二维数组的大小。 回忆一维数组的情况: int arr[] = {0,1,2,3,4}; 代码中没有明显地指出arr的大小,但编译器将根据我们对该数组初始化数据,倒推出该数组大小应为5。
那么,二维数组是否也可以不指定大小呢?比如:
int arr[][] = { {1,2,3}, {4,5,6} }; //ERROR!
答案是:对了一半……所以还是错,这样定义一个二维数组,编译器不会放过。正确的作法是: 必须指定第二维的大小,而可以不指定第二维的大小,如:
int arr[][3] = { {1,2,3}, {4,5,6} };
编译器可以根据初始化元素的个数,及低维的尺寸,来推算出第二维大小应为:6 / 3 = 2。但是,很可惜,你不能反过来这样: int arr[2][] = { {1,2,3}, {4,5,6} }; //ERROR! 不能不指定低维尺寸。
事实上,低维的花括号是写给人看的,只要指定低维的尺寸,编译器甚至允许你这么初始化一个二维数组:
int arr[][3] = {1,2,3,4,5,6}; //看上去像在初始一维数组?其实是二维的。
看上去像在初始一维数组?其实是二维的。 为什么可以这样?我们下面来说说二维数组的内存结构。
17.2.3 二维数组的内存结构
从逻辑上讲,一维数组像一个队列,二维数组像一个方阵,或平面:
一维数组的逻辑结构和它在内存里的实际位置相当一致的。但到了二维数组,我们应该想,在内存真的是排成一个“平面”吗?这不可能。内存是一种物理设备,它的地址排列是固定的线性结构,它不可能因为我们写程序中定义了一个二维数组,就把自已的某一段地址空间重新排成一个“平面”。后面我们还要学更高维数组,比如三维数组。三维数组的逻辑结构像一个立方体。你家里有“魔方”吗?拿出来看看,你就会明白内存更不可能把自已排出一个立方体。
结论是:内存必须仍然用直线的结构,来表达一个二维数组。
比如有一个二维数组:
char arr[3][2] = //一个3行2列的二维数组。 { {'1','2'}, {'3','4'}, {'5','6'} };
它的内存结构应为:
(二维数组 char arr[3][2] 的内存结构:每个元素之间的地址仍然连续)
也就是说,二维数组中的所有元素,存放在内存里时,它们的内存地址仍然是连续的。假如另有一个一维数组:
char arr[6] = {'1','2','3','4','5','6'}; //一维数组
这个一维数组的内存结构:
(一维数组 char arr[3][2] 的内存结构)
你猜到我想说什么了吗?请对比这两个表:一个有2*3或3*2的二维数组,和一个有6个元素的同类型一维数组,它们的内存结构完全一样。所以前面我们说如此定义并初始化一个二维数组:
int arr[][3] = {1,2,3,4,5,6};
也是正确的,只是对于程序员来说有些不直观,但编译器看到的都一样:都是那段同内存中的数据。不一样的是前面的语法。对于一维数组: int arr[] = {1,2,3,4,5,6}; 红色部分告诉编译器,这是一个一维数组。对于二维数组: int arr[][3] = {1,2,3,4,5,6}; 红色部分告诉编译器,这是一个二维数组,并且低维尺寸为3个,也就是要按每3个元素分出一行。C++的语法规定,编译器首先查看低维大小,所以我们若没有指明低维大小,则编译器立即报错,停止干活。因此,定义: int arr[2][] = {1,2,3,4,5,6}; 是一种错误。
17.2.4 二维数组的内存地址
了解了二维数组的内存结构,我们再来说说几个关于二维数组地址问题,会有些绕,但并不难。嗯,先来做一个智力测试。 以下图形中包含几个三角形?
正确答案是:3个。想必没有人答不出。我们要说的是 :这三个三角形中,两个小三角和一个大三角重叠着,因此若计算面积,则面积并非三个三角形的和,而是两个小三角或一个大三角的面积。
这个问题我们在一维数组时已经碰到过:一个数组本身可称为一个变量,而它包含的各个元素也都是一个个变量,但它们占用的内存是重叠的。
二维数组本身也是一个变量,并且也直接代表该数组的地址,我们要得到一个二维数组变量的地址,同样不需要取址符:&。
int arr[2][3] = {1,2,3,4,5,6};
//输出整个二维数组的地址。 cout << arr;
同样,我们也可以得到每个元素的地址,不过需要使用取址符:
//输出第一个元素(第0行第0列)的地址: cout << &arr[0][0] << endl; //输出第2行第3列的元素地址: cout << &arr[1][2] << endl;
除此之外,我们还可以按“行”来输出元素地址,不需要使用取址符:
//输出第一行元素的起始地址: cout << arr[0] << endl; //输出第二行元素的起始地址: cout << arr[1] << endl;
上图表明:arr, arr[0], &arr[0][0] 都指向了同一内存地址。即: arr == arr[0] == &arr[0][0]。 另外: arr[1] == &arr[1][0] 及 arr[2] == &arr[2][0]。
我们可以有这些推论: 二维数组中的每一行,相当于一个一维数组。或者说,一维数组是由多个简单变量组成,而二维数组是由多个一维数组组成。 示意图:
例子: int arr[2][3]; 则: arr[i][j] 相当于一个普通int变量。而 arr[i] 相当于一个一维数组。
现在,我还是来提问两个问题:
问题一: 有一数组 char arr[3][4]; 已知 arr 中第一个元素(arr[0][0])的地址为:10000,请问 &arr[2][1] 的值为?
解答:先要知道arr[1][1]是数组arr中的第几个元素? 数组arr共3行,每行4列,而arr[2][1]是位于第3行第2列,所以它是第: 2 * 4 + 2 = 10,即第10个元素。 这样就计算出来,第1个元素地址是10000,则第10个元素地址: 10000 + (10 - 1) * sizeof(char) = 10009。
问题二: 如果上题中的数组为: int arr[3][4];其余不变,请问该如何计算? 答案:10000 + (10 - 1) * sizeof(int) = 10036。
17.3 二维数组实例是不是前面的内容让你有些发晕。知识重在应用。我们还是来多操练几个二维数组的例子吧。但是,等用得多了,用得熟了,我希望大家回头再看前面的那些内容。
17.3.1 用二维数组做字模
例三: 字模程序。
手机屏幕是如何显示英文字母或汉字的?这个小程序将要从原理上模拟这个过程。
手机屏幕采用的字体称为“点阵”字体。所以“点阵”,就是用一个个小点,通过“布阵”,组成一个字形。而这些点阵数据,就是一个二维数组中的元素。不同的手机,点阵的大小也不同。如果不支持中文,则最小只需7*7;但若是要支持汉字,则应不小于9*9,否则许多汉字会缺横少竖。采用大点阵字体,则手机屏幕要么是面积更大,要么是分辨率更高(同一面积内可以显示更多点);并且手机的内部存储器也要更多。由于汉字数量众多,不像英文主要只有26个字母;所以支持汉字的手机,比只能显示英文字手机,其所需存储器自然要多出一个很大的数量级。
下面举例英文字母“A"的点阵,为了看的方便,我们用*来代替小黑点,并且打上了表格。我们使用最小的7*7点阵:
对于这样一个点阵,对应一个二维数组为:
int A[7][7] = { {0,0,0,0,0,0,0}, {0,0,0,1,0,0,0}, {0,0,1,0,1,0,0}, {0,1,1,1,1,1,0}, {1,0,0,0,0,0,1}, {0,0,0,0,0,0,0}, {0,0,0,0,0,0,0}, };
程序要在屏幕上打出“A"时,则只需遍历该数组,然后在元素值为0的地方,打出空格,在元素值为1的地方,打出小点即可。当然,在我们的模拟程序里,我们打出星号。 所有这些数组,都需要事先写到手机的固定存储器中,这些数据就称为“字模”。
对于“A"的例子,打印时的代码如下:
for(int row = 0;row < 7; row++) { for(int col = 0; col < 7; col++) { if(A[row][col] == 0) cout << ' '; else cout << '*'; }
//别忘了换行: cout << endl; }
结果如:
大家小时候有没刻过印章?哎!大概80年代出生的人是不会有过这种游戏了。印章分“阴文”和“阳文”。如果把上面的程序稍做修改,即在元素值为0的地方打出“*”,而在元素值为1的地方打出空格,那么输出结果就是“阴文”了。大家不妨试试。
例四: 躺着的“A”。
同样使用例三的二维数组数据: int A[7][7] = { {0,0,0,0,0,0,0}, {0,0,0,1,0,0,0}, {0,0,1,0,1,0,0}, {0,1,1,1,1,1,0}, {1,0,0,0,0,0,1}, {0,0,0,0,0,0,0}, {0,0,0,0,0,0,0}, };
请改动例三的程序,但不允许改变数组A的元素值,使之打印出一个躺着的“A”。下面是输出结果:
请大家想想如何实现,具体代码请见课程配套代码源文件。
17.3.2 二维数组在班级管理程序中应用例五: 多个班级的成绩管理
以前我们做过单个班级,或整个学校的成绩,那时都是将所有学生进行统一编号。事实上,不同的班级需要各自独立的编号。 比如初一年段有4个班级,每个班级最多40人。那么很直观地,该成绩数据对应于这样一个二维数组:
int cj[4][40];
在这里,数组的高维(第二维)和低维(第一维)具备了现实意义。例中,4代表4个班级,40代表每个班级中最多有40个学生。因此低维代表班级中学生的编号,高维代表班级的编号。这和现实的逻辑是对应的:现实中,我们也认为班级的编号应当比学员的编号高一级,对不?你向别人介绍说:“我是2班的24号”,而不是“我是24号的2班”。
一个可以管理多个班级的学生成绩的程序,涉及到方方面面,本例的重点仅在于:请掌握如何将现实中的具有高低维信息,用二维数组表达出来。并不是所有具有二维信息的现实数据,都需要明确地区分高低维,比如一个长方形,究竟”长“算”高维还是“宽”算高维?这就无所谓了。但另外一些二维数据,注意到它们的高低维区分,可以更直观地写出程序。
闲话少说,现在问,2班24号的成绩是哪个?你应该回答: cj[1][23]; ——最后一次提醒,C++中的数组下标从0开始,无论是一维二维还是更多维的数组,所以2班24号对应的是下标是1和23。
我们要实现以下管理:
1、录入成绩,用户输入班级编号,然后输入该班每个学员的成绩; 2、清空成绩,用户输入班级编号,程序将该班学员的成绩都置为0; 3、输出成绩,用户输入班级编号,程序将该班学员成绩按每行10个,输出到屏幕; 4、查询成绩,用户输入班级编号和学员编号,程序在屏幕上打出该学员成绩。 5、统计成绩,用户输入班级编号,程序输出该班级合计成绩和平均成绩。 0、退出。
看上去,这是一个稍微大点的程序了。 我们已经学过函数,所以上面的四个功能都各用一个函数来实现。另外,“让用户输入班级编号”等动作,我们也分别写成独立的函数。 四个功能中,都需要对各班级学员成绩进行处理,所以我们定义一个全局的二维数组。 下面我们一步一步实现。
第一步:定义全局二维数组,加入基本的头文件。
第一步的操作结果应是这样(黑色部分为需要加入的代码):
//-------------------------------------------------------------------------- //支持多班级的成绩管理系统 #pragma hdrstop #include <iostream.h> //--------------------------------------------------------------------------- #define CLASS_COUNT 4 //4个班级 #define CLASS_STUDENT_COUNT 40 //每班最多40个学员
//定义一个全局二维数组变量,用于存储多班成绩: int cj[CLASS_COUNT][CLASS_STUDENT_COUNT]; //提示:全局变量会被自动初始化为全0。 //所以一开始该cj数组中每个成绩均为0。
#pragma argsused int main(int argc, char* argv[]) { return 0; }
//--------------------------------------------------------------------------- 第二步:加入让用户选择功能的函数。
我们大致按从上而下方法来写这个程序。该程序首先要做的是让用户选择待执行功能。下面的函数实现这个界面。
//函数:提供一个界面,让用户选择功能: //返回值:1~5, 待执行的功能,0:退出程序 int SelectFunc() { int selected;
do { cout << "请选择:(0~5)" << endl;
cout << "1、录入成绩" << endl << "2、清空成绩" << endl << "3、输出成绩" << endl << "4、查询成绩" << endl << "5、统计成绩" << endl << "0、退出" << endl;
cin >> selected; } while(selected < 0 || selected > 5); //如果用户输入0~5范围之外的数字,则重复输入。
return selected; }
函数首先输入1到5项功能,及0:用于退出。注意我们用了一个do...while循环,循环继续的条件是用户输入有误。do...while流程用于这类目的,我们已经不是第一次了。 函数最后返回 selected 的值。
这个函数代码最好放在下面位置:
...... int cj[CLASS_COUNT][CLASS_STUDENT_COUNT];
/*
<<<< 前面SelectFunc()函数的实现代码加在此处。
*/
#pragma argsused int main(int argc, char* argv[]) ......
为了验证一下我们关于该函数是否能正常工作,我们可以先把它在main()函数内用一下:
int main(int argc, char* argv[]) { SelectFunc(); } 按Ctrl + F9 编译,如果有误,只现在就参照本课程或源代码改正。如果一切顺利,则你会发现这个函数工作得很好。那么,删掉用于调试的: SelectFunc(); 这一行,我们开始下一个函数。
第三步:辅助函数:用户输入班级编号等。
“录入、清空、统计”成绩之间,都需要用户输入班级编号。而“查询成绩”则要求用户输入班级编号及学号,所以这一步我们来实现这两个函数。
//用户输入班级编号: int SelectClass() { int classNumber; //班级编号 do
{ }
while(classNumber < 1 || classNumber >
CLASS_COUNT); }
SelectClass 和 SelectFunc 中的流程完全一样。为了适应普通用户的习惯,我们让他们输入1~4,而不是0~3,所以最后需要将classNumber减1后再返回。
另外一个函数是用户在选择“成绩查询”时,我们需要他输入班级编号和学生学号。前面的SelectClass()已经实现班级选级,我们只需再写一个选级学号的函数SelectStudent即可。并且SelectStudent和SelectClass除了提示信息不一样,没什么不同的。我们不写在此。
第四步:录入、清空、查询、统计成绩功能的一一实现。
//录入成绩 //参数 classNumber: 班级编号 void InputScore(int classNumber) { /* 一个班级最多40个学员,但也可以少于40个,所以我们规定,当用户输入-1时,表示已经输入完毕。 */
//判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
//提示字串: cout << "请输入" << classNumber + 1 << "班的学生成绩。" << endl; cout << "输入-1表示结束。" << endl;
for(int i=0; i < CLASS_STUDENT_COUNT; i++) { cout << "请输入" << i+1 << "号学员成绩:"; cin >> cj[classNumber][i]; //cj 是全局变量,所以这里可以直接用。
//判断是否为-1,若是,跳出循环: if( -1 == cj[classNumber][i]) break; } } //---------------------------------------------------- //清空成绩: void ClearScore(int classNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
for(int i=0; i < CLASS_STUDENT_COUNT; i++) { cj[classNumber][i] = 0; }
cout << classNumber + 1 << "班学生成绩清空完毕" << endl; } //---------------------------------------------------- //输出成绩: void OutputScore(int classNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
cout << "============================" << endl; cout << classNumber + 1 << "班成绩" << endl;
/* 有两点要注意: 1、要求每行输出5个成绩。 2、每个班级并不一定是40个成绩,所以只要遇到-1,则停止输出。当然,如果该班 成绩尚未录入,则输出的是40个0。 */
for(int i = 0; i < CLASS_STUDENT_COUNT; i++) { if(i % 5 == 0) //因为每行输出5个,所以i被5 整除,表示是一新行 cout << endl;
if(-1 == cj[classNumber][i]) //遇到成绩为-1... break;
cout << cj[classNumber][i] << ","; } } //---------------------------------------------------- //查询成绩: void FindScore(int classNumber, int studentNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
//判断学生编号是否在合法范围内: if(studentNumber < 0 || studentNumber >= CLASS_STUDENT_COUNT) return;
cout << classNumber + 1 << "班," << studentNumber + 1 << "号成绩:"<< cj[classNumber][studentNumber] << endl; } //---------------------------------------------------- //统计成绩: void TotalScore(int classNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
int totalScore = 0; //总分 int scoreCount = 0; //个数
//同样要注意遇到-1结束。 for(int i = 0; i < CLASS_STUDENT_COUNT; i++) { if(cj[classNumber][i] != -1) { totalScore += cj[classNumber][i]; scoreCount++; } else { break; } }
//还要注意,如果第一个成绩就是-1,则个数为0,此时无法求平均值(因为除数不能为0) if(scoreCount == 0) { cout << "该班学员个数为0" << endl; return; }
cout << "总分:" << totalScore << "平均:" << totalScore / scoreCount << endl; }
第五步:完成主函数(总体流程)
在主函数内,将上面的主要函数放到合适的流程里。(仅从这一步看,我们的开发过程又有点像是“由下而上”法了:写好了各函数,最后组织起来。事实上,几乎所有大软件的开发,都是“由上而下”与“由下而上”结合)。
int main(int argc, char* argv[]) { int selected;
do { //用户选择要执行的功能: selected = SelectFunc();
//如果选择0,则退出: if(selected == 0) break;
//根据selected来执行相应功能: switch(selected) { //1、输入成绩: case 1 : { int classNumber = SelectClass(); InputScore(classNumber); //两行代码可以合为:InputScore(SelectClass()); break; } //2、清空成绩: case 2 : { int classNumber = SelectClass(); ClearScore(classNumber); break; } //3、输出成绩: case 3 : { int classNumber = SelectClass(); OutputScore(classNumber); } //4、查询成绩: case 4 : { int classNumber = SelectClass(); int studentNumber = SelectStudent(); FindScore(classNumber,studentNumber); //以上三行也可合为:FindScore(SelectClass(),SelectStudent()); break; } //5、统计成绩: case 5 : { int classNumber = SelectClass(); TotalScore(classNumber); break; } } } while(true); //一直循环,直到前面用户输入0跳出。
} 一点题外话。代码中的注释也说明了,像:
int classNumber = SelectClass(); int studentNumber = SelectStudent();
FindScore(classNumber,studentNumber); 在C,或C++里,可以直接用一行来表示: FindScore(SelectClass(),SelectStudent());
这也是一个熟练的C或C++程序员常做的事。大家现在就把这种写法写到例子中试试,并且理解。随着我们练习的代码量的不断增多,类似这样的很多
简洁的写法,我们都会用上,如果你不写,等我们一用上,你容易感到困惑。 OK! 似乎是突然来了一个大程序?把它调通吧,下面是我运行这个程序的输出界面:
仔细地想一想,我们至少还有一个重要功能没有实现,那就是排序。呵呵,关于排序,我们需要一整章来讲它。下面,还是说说如何数组的一些其它事情吧。如果你觉得有些累,就休息30分钟。
17.4 三维和更多维数组一维和二维是最常用的数组。到了三维就用得少了。四维或更高维,几乎不使用。我们这里不多讲,仅举一些三维数组的实例。大家通过二维数组知识,就可看懂。
17.4.1 多维数组的定义与初始化
//单单定义一个三维数组: int arr[3][4][2];
//如果是在定义的同时还初始化: int arr[3][4][2] = {
};
要看懂上面的初始化,关键在于找出: 哪里体现了最低维大小: 2? 哪里体现了第二维的大小:4? 哪里体现了最高维大小:3? 我加了彩色帮你寻找。
如果你看懂,那就这样吧。有一天我们需要使用三维数组。那时再说。我很相信你现在其实也会用一个三维数组,无非是:
cout << arr[2][1][0] << endl;
初始化时,可以不省略最高维的大小,其它低维的大小则必须指明。 int arr[][4][2] = { ...... }
下面我举一个简单的例子。
17.4.2 多维数组的定义与初始化
没错,还是成绩管理。但我们仅要用一些代码来示意,让大家更实在地理解三维及更高维数组: 前面我们的成绩已经可以实现多个班级的同时管理。如果再进一步,你想实现多个年段的成绩管理怎么办?那就再来一维吧. 下面我们示例可以管初中三个年段的成绩管理:
#define GRADE_COUNT 3 //年段总数:3 #define CLASS_COUNT 4 //每个年段允许的最多班级数目 #define STUDENT_COUNT 40 //每个班级允许的最多学员人数 int cj [GRADE_COUNT][CLASS_COUNT][STUDENT_COUNT];
好!我们先插播一下说明。从这行定义,我们就应该学会高低维与现实数据的如何对应。看,在生活中,年段,班级,学号按层次分,正好是高、中、低;而三者在数组中也正是分别占用了高中低三维。这是很自然而然的做法。
现在,我们要想得到初三年段,2班,20号学员的成绩,如何办?
//让a为初三年段,2班,20号学员的成绩: int a = cj[2][1][19]; 修改呢? cj[2][1][19] = 78;
取得与设置三维数组元素的操作,就是这样而已。如果想清空每个成绩,则循环相应地变成三层:
//nd : 年段, bj : 班级, xh : 学号 for(int nd = 0; nd < GRADE_COUNT; nd++) { for(int bj = 0; bj < CLASS_COUNT; bj++) { for(int xh = 0; xh < STUDENT_COUNT; xh++) { cj[nd][bj][xh] = 0; } } }
哈哈,以我们现在才能,当初年段长投向我们的目光已经不算什么了,校长大人完全应该让我们当个教务长啊。你真的很想?那就试试把前面的那个“二维版”的成绩系统改写为“三维”版?? 改还是不改?要改可真累!算了,把初一初二初三的成绩混在一个数组里管理,其实是一个很糟的做法,并不实用,对不?。我们这里只是想让大家看到三维数组可以解决什么样的问题。
再高一维的呢?好,还是成绩管理系统。你以为我这回想做一个“跨校”的成绩管理?当然不是。一个学员只有一个成绩吗?不是啊。我们再定义一个学员可以有最多6个成绩:
...... #define SCORE_COUNT 6 int cj [GRADE_COUNT][CLASS_COUNT][STUDENT_COUNT][SCORE_COUNT]; ......
17.5 数组作为函数的参数要学习这一章,首先确保你没有忘记“函数的参数”是什么?如果您有些模糊,就先复习函数的两章。 17.5.1 数组参数默认是传址方式
数组作为函数的参数,难点和重点都在于这两点: 1、理解函数参数两种传递方式:传值、传址之间区别。 2、数组变量本身就是内存地址。
这两点我们都已讲过,但此时是我们复习——或者说进一步理解这两点时候。 现来说第一点:传值、传址的区别。如果你连什么叫“函数参数”都没有印象,那你现在需要的不是复习,而是“补习”。请回头看第13章。
现在我再举一个例子,来解释当把一个参数传给函数时,使用“传值”方式和使用“传址”有何区别:
首先假设科技发达,可以通过克隆复制出一个和你一模一样的人。 接着是个美妙的故事:有个阿拉伯公主将要嫁给你。 再来就是两种情况。 第一种情况: 先把你克隆,然后公主嫁给“你”(那个复制品)。 在这种情况下,请问:当公主和复制的人深情相吻时,不知你有何感觉?当然是没有什么感觉,尽管那个复制品和你长得一模一样,但是他的一切行为都和你无关,若一天他不幸被惹恼了国王,被砍头,不要紧,你还活着。 这种情况对应的是函数参数传递方式的第一种“传值”:传的是一个复制品,虽然值完全一样,但并不是实参本身。 第二情况: 被送到阿拉伯王宫的人就是你本人!这回,嘿嘿,和公主亲密相吻的感觉你可尽情享受,但若被砍头,则在这世界上消失的也是你。 这就是“传址”的情况:传给函数的是实参的内存地址,我们知道,变量在计算机里就是一个内存的地址,反过来,传一个内存地址,也就起到传送实参变量本身的作用。 一句话:传值方式下,传的只是实参的复制品(值一样);传址方式下,传的是实参本身。
接下来,回顾一个简单变量的作为参数的例子,同时也是检查你是否理解第一点的时候了。
void Func1(int a) { a = 100; } //------------------------ void Func2(int& a) { a = 200; } //------------------------ int main(int argc, char* argv[]) { int c = 0;
Func1(c); cout << c << endl;
Func2(c); cout << c << endl; }
请向上面main()运行以后,屏幕上输出的两个数是多少?请先回答该问题,然后上机实验。如果您答错了,或者你知道自已只是“蒙”对了,你需要去复读第12、13章讲函数的内容。 从上面的代码中我们也看到了,“传值”方式下,函数的形参没有“&";“传址”的方式下,形参前有一个“&”。这是二者语法上的区别。但是在下面,这将会有一点变化。
有关第1点的新知识来了:在C,C++中,如果函数的参数是数组,则该参数固定为传址方式。 例: void Func(int arr[5]) { ... }
Func 函数的参数是: int arr[5]。 这是第一次接触使用数组作为参数。它表示在调用Func时,需要给这个函数一个大小为5的整型数组。 在这个参数里,我们没有看到“&”。似乎这应该是一个“传值”方式的参数,但错了,对数组作为参数,则固定是以传址方式将数组本身传给函数,而不是传数组的复制品。 为什么要有这样一个例外?首先是出于效率方面的考虑。复制数组需要的时间可能和复制一个简单变量没有区别:比如这个复制就只有一个元素: int arr[1]; 但如果这个数组是1000个,或50000个元素,则需要较长的时间,对于C,C++这门追求高效的语言,太不合算。 接着从第二点上说:“数组本身就是内存地址”,也正好说明了这一点,数组作为函数的参数,传的是“地址”,并且不需要加‘&’符号来标明它是一个传址方式的参数,因为,“数组本身就是内存地址”。
请看下面的举例:
void Func(int arr[5]) { for(int i=0;i<5;i++) arr[i] = i; }
int main(int argc, char* argv[]) { int a[5];
Func(a);
for(int i=0; i<5;i++) count << a[i] << ','; }
输出将是 “0,1,2,3,4,”。这证明数组 a 传给Func之后,的的确确被Func在函数内部修改了,并且改的是a本身,而不是a的复制品。
17.5.2 可以不指定元素个数
我们定义一个数组变量时,需要告诉编译器该数组的大小(直接或间接地指定)。但在声明一个函数的数组参数时,可以不指定大小。
在 声明一个函数时: void Func(int arr[]); 及在定义它时: void Func(int arr[]) { ... }
上面中的参数:int arr[]。没有指定数组arr的大小。这样做的好处是该函数原来只能处理大小固定是5的数组,现在则可以处理任意大小的整型数组。 当然,对于一个不知大小的数组,我们处理起来会胆战心惊,因为一不小心就会越界。一般的做法是再加一个参数,用于在运行时指定该数组的实际大小:
void Func(int arr[], int size) { for(int i=0;i<size;i++) arr[i] = i; }
现在这个函数可以处理任意大小的数组,很方便。
int a[5],b[10],c[100];
Func(a,5); Func(b,10); Func(c,100);
你还可以根据需要,指定一个比数组实际大小要小的size值。比如我们只想让Func函数处理c数组中的前50个元素: Func(c,50);
说完“数组参数可以不指定大小”这一规定的好处,我们再来说这一规定的技术原理。其实说这是一项“规定”,其实说法不合理。只有那些解释性的语言(如BAISC)才会有各种规定,对于C++这样一门既灵活又严谨的,纯编译型的语言,当它的语法规定下来后,就会自然而然地产生一些特性——是语言自身实现的特性,而不是人为规定。
前面说数组做为函数参数,使用的是“传址”方式。由于传递的是数组的地址,而不是数组的所有元素,所以函数可以不知道该数组的实际大小。
假设有这么一些代码片段:
void Func(int arr[]) { .... };
...
int a[3]; Func(a);
前面是函数Func的实现,并没有执行动作。我们来看后面两句。
接下来,程序以数组a作为参数。调用函数:Func。
由于是“传址方式”,所以函数Func其实只得到了一个地址值:10001,至于这10001后面跟了多少个字节,或跟了多少个整型元素?Func无从得知,既然不知,也就无法做出限制。所以,换一段代码,你传给它一个大小数300的函数,它也能接受。
17.5.3 数组作为函数参数的上机实例
在“支持多个班级的成绩管理系统”那个例子,我们写了不少函数,但没有哪一个函数用到数组参数。这是因为,作为程序中要用到的惟一一个数组数据:; int cj[CLASS_COUNT][CLASS_STUDENT_COUNT],它被我们定义为一全局变量。全局变量不在任意函数内,所以不专属哪个函数,所有的函数都可以在函数中使用,所以我们没有必要通过参数来传递。
“成绩管理系统”的第二个功能:“清空成绩”,是一个将某一数组中的所有元素清0的过程。我们针对这一功能,首先,让我们自已写一个相应的函数。
//函数:将一个整型数组中的指定元素值全部清0: void ZeroIntegerArray(int arr[],int size) { for(int i=0;i<size;i++) arr[i] = 0; }
解释一下函数名字:Zero:归0,Integer:整数,Array:数组。
接着,我们让这个函数用在上面的“成绩管理”中的“清空成绩”。 原来的清空功能是这样实现的: //---------------------------------------------------- //清空成绩: void ClearScore(int classNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
for(int i=0; i < CLASS_STUDENT_COUNT; i++) { cj[classNumber][i] = 0; }
cout << classNumber + 1 << "班学生成绩清空完毕" << endl; } //----------------------------------------------------
你可能发现了,被清空的数组是一个二维数组:cj[][],而我们的ZeroIntegerArray(int arr[] ...)需要的参数是一维数组。
其实,在ClearScore()函数中,被清空的只是指定那个班级的所学员成绩,而不是所有班级的所有学员成绩。我们说过,二维数组是由多个一维数组组成。请理解以下代码:
//清空成绩: void ClearScore(int classNumber) { //判断classNumber是否在合法的范围内: if(classNumber < 0 || classNumber >= CLASS_COUNT) return;
ZeroIntegerArray(cj[classNumber],CLASS_STUDENT_COUNT);
cout << classNumber + 1 << "班学生成绩清空完毕" << endl; } //----------------------------------------------------
例子中的,ZeroIntegerArray()数组,清的是一个二维数组中的某一行,实参是:cj[classNumber],如果对它有疑问,请回头看本章“二维数组包含一维数组”的图及相关内容。
17.5.4 二维及更多维数组作为函数参数
函数参数也可以是二维及及更高维数组。但必须指定除最高维以后的各维大小。这一点和初始化时,可以省略不写最高维大小的规则一致:
//定义一个使用二维数组作为参数 void Func(int arr[][5]) //第二维的大小可以不指定 { ... }
//定义一个使用三维数组作为参数 void Func(int arr[][2][5]) //第三维的大小可以不指定 { ... }
17.5.5 函数的返回值类型不能是数组
最后特别指出一点:函数的返回值不能是数组类型:
int[5] Func(); //ERROR!
本意是想让函数返回一个大小为5的数组,但实际语法行不通。 由于数组作为参数时,使用的是传址方式,所以一个数组参数,可以直接得到它在函数内被修改的结果,无须通过函数返回。另外,后面我们将学习“指针”,则通过返回指针来达到返回数组的同等功能。
17.6 sizeof 用在数组上
还记得sizeof吧?它可以求一个变量某数据类型占用多少个字节。比如,sizeof(int)得到4,因为int类型占用4个字节。或者: int i; char c;
cout << sizeof(i) << "," << sizeof(c);
将输出4和1。
sizeof 用在数组上,有两个要点: 1、可以用通过它来计算一个数组的元素个数。 2、当数组是函数的参数时,sizeof对它不可用。
17.6.1 用sizeof自动计算元素个数sizeof对于一个数组变量,则会得到整个数组所有元素占用字节之和:
int arr[5];
cout << sizeof(arr);
屏幕上显示:20。因为5个元素,每个元素都是int类型,各占用4字节,总和:4 * 5 = 20。
由这个可以算出某一数组含多少个元素:sizeof(arr) / sizeof(arr[0]) = 20 / 4 = 5。从而得知arr数组中有5个元素。
这些我们都已经知道,但下面的事情有些例外。
17.6.2 sizeof对数组参数不可用
对于一个从函数参数传递过来的数组,sizeof无法得到该数组实际占用字节数。
这句ڝ |