第十七章 数组(二)

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 整除,表示是一新行

          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] =

{
   {

     {1,2},

     {3,4},

     {5,6},

     {7,8}

   },

   {

     {11,12},

     {13,14},

     {15,16},

     {17,18}

   },

   {

     {21,22},

     {23,24},

     {25,26},

     {27,28}

   }

};

 

要看懂上面的初始化,关键在于找出:

哪里体现了最低维大小: 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的实现,并没有执行动作。我们来看后面两句。

 

 
代码 说明 内存示意图
int a[5]; 在内存里申请了一个大小为3的整型数组:

(字节数: 3 * sizeof(int) = 12

         (10001...表示该字节内存的地址)

 

 

接下来,程序以数组a作为参数。调用函数:Func

 

由于是“传址方式”,所以函数Func其实只得到了一个地址值:10001,至于这10001后面跟了多少个字节,或跟了多少个整型元素?Func无从得知,既然不知,也就无法做出限制。所以,换一段代码,你传给它一个大小数300的函数,它也能接受。

 

 
代码 说明 内存示意图
Func(a) 以数组a为参数,调用函数Func():

 

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无法得到该数组实际占用字节数。

 

这句话什么意思呢?看看这个例子:

 

void Func(int arr[])

{

   cout << sizeof(arr) << endl;

}

 

int main(int argc, char* argv[])

{

    int b[20];

 

    Func(b);

 

    system("pause");

 

    return 0;

}

 

程序将输出:4,而实际上,b占用的字节数应为:4 * 20 = 80;大家可以在调用Func(b)之前加一行:

 

cout << sizeof(b) << endl;

Func(b);

 

这样更可以发现,sizeof直接对b取值,得到的是正确的大小,而将b传给Func后,在Func内部对该参数取值,则只能得到:4。

为什么?

这正是我们前面说的,数组作为参数传给函数时,采用的是传址方式,固定只送了数组的地址过去,而用于存放数组的地址,仅需4个字节即可。再打比方吧,如果你想你现在居住的三房二厅的房子送给我,你有两种办法:第一是把整个房子用倚天屠龙刀从楼幢里切出,然后买一巨大的信封(意指占用很大内存),寄给我(意指将整个实际数组传给函数);另一种方法是将你的家钥匙用个小信封寄过来。

 

整来整去,我们一直在复习函数参数的内容啊?不过有什么办法呢?当涉及到把数组作为参数,就不得不直面对“传址”的理解。归结为一首儿歌吧:

 

“传值”传复制

“传址”传地址

 数组当参数

 固定传地址

 

这一章到此结束,内容很多,在老师不讲课的期间,你应该如何做才能学透本章呢?

请自已给自已出一些“一维或二维数组”的题目,如何出不了,就做作业吧。