首页 服务器 编程 必备知识 搜索引擎 圩日手册
站内搜索
最近浏览
推荐文章
热文排行

第十七章 数组(二)


17.1 数组与内存

  17.1.1 数组的内存结构

  17.1.2 数组的内存地址

  17.1.3 数组元素的内存地址

  17.1.4 数组访问越界

17.2 二维数组

  17.2.1 二维数组基本语法

  17.2.2 二维数组初始化

  17.2.3 二维数组的内存结构

  17.2.4 二维数组的内存地址

17.3 二维数组实例

  17.3.1 用二维数组做字模

  17.3.2 二维数组在班级管理程序中应用

17.4 三维和更多维数组

  17.4.1 多维数组的定义与初始化

  17.4.2 多维数组的示例

17.5 数组作为函数的参数

  17.5.1 数组参数默认是传址方式

  17.5.2 可以不指定元素个数

  17.5.3 数组作为函数参数的上机实例

  17.5.4 二维及更多维数组作为函数参数

  17.5.5 函数的返回值类型不能是数组

17.6 sizeof用在数组上

  17.6.1 用sizeof自动计算元素个数

  17.6.2 sizeof对数组参数不可用

 

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字节,如果是一个charbool 类型的数组,则每个元素的大小是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 二维数组的内存结构

 

从逻辑上讲,一维数组像一个队列,二维数组像一个方阵,或平面:

 

 

[0]
[1]
[2]
[3]

一维数组是“一长串”

[0][0] [0][1] [0][2]
[1][0] [1][1] [1][2]
[2][0] [2][1] [2][2]
[3][0] [3][1] [3][2]

这是一个二维数组,4行3列,像一个4*3的平面

 

一维数组的逻辑结构和它在内存里的实际位置相当一致的。但到了二维数组,我们应该想,在内存真的是排成一个“平面”吗?这不可能。内存是一种物理设备,它的地址排列是固定的线性结构,它不可能因为我们写程序中定义了一个二维数组,就把自已的某一段地址空间重新排成一个“平面”。后面我们还要学更高维数组,比如三维数组。三维数组的逻辑结构像一个立方体。你家里有“魔方”吗?拿出来看看,你就会明白内存更不可能把自已排出一个立方体。

 

结论是:内存必须仍然用直线的结构,来表达一个二维数组。

 

比如有一个二维数组:

 

char arr[3][2] =  //一个3行2列的二维数组。

{

   {'1','2'},

   {'3','4'},

   {'5','6'}

};

 

它的内存结构应为:

 

下标 内存地址

(仅用示意)

元素值
[0][0] 100001 '1'
[0][1] 100002 '2'
[1][0] 100003 '3'
[1][1] 100004 '4'
[2][0] 100005 '5'
[2][1] 100006 '6'

(二维数组 char arr[3][2] 的内存结构:每个元素之间的地址仍然连续)

 

也就是说,二维数组中的所有元素,存放在内存里时,它们的内存地址仍然是连续的。假如另有一个一维数组:

 

char arr[6] = {'1','2','3','4','5','6'}; //一维数组

 

这个一维数组的内存结构:

下标 内存地址

(仅用示意)

元素值
[0] 100001 '1'
[1] 100002 '2'
[2] 100003 '3'
[3] 100004 '4'
[4] 100005 '5'
[5] 100006 '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

   {
           cout << "
请输入班级编号:(1~4)";
           cin >> classNumber;

   }

    while(classNumber < 1 || classNumber > CLASS_COUNT);

      return classNumber - 1;

}

 

SelectClass SelectFunc 中的流程完全一样。为了适应普通用户的习惯,我们让他们输入1~4,而不是0~3,所以最后需要将classNumber减1后再返回。

 

另外一个函数是用户在选择“成绩查询”时,我们需要他输入班级编号和学生学号。前面的SelectClass()已经实现班级选级,我们只需再写一个选级学号的函数SelectStudent即可。并且SelectStudentSelectClass除了提示信息不一样,没什么不同的。我们不写在此。

 

第四步:录入、清空、查询、统计成绩功能的一一实现。

 

//录入成绩

//参数 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 整除,

[wangjy17908]
添加时间:2008-04-12
版权所有(C)2005-2015