c语言三维数组两班成绩,數組

第十七章 数组(二)

17.1 数组与内存

变量需要占用内存空间,内存空间有地址。不同数据类型的变量,可能占用不同的内存大小及有不同的内存结构。

以前我们所学都称为“简单数据类型”,如: int,char,float,double,bool。像 char,bool,只占用一个字节,所以我们不去管它的的“结构”,其余如 int,float,double占用多个字节,但比较简单,适当的时候我们会去探讨4个字节是如何组成一个整数。

后来我们学习了数组。数组变量占用内存的大小是不定的,因为不同的数组变量除了类型可以不同,还可以拥有不同个数的元素,这两点都影响它的大小。

因此,数组是我们第一个要着力研究它的结构的数据类型。和后面我们还要学习的更多数据类型相比,数组的结构还是相当简单的。简单就简单在它的各个元素大小一致,整整齐齐地排列。

17.1.1 数组的内存结构

变量需要占用内存空间,内存空间有地址。

声明一个整型变量

int a;

系统会为该变量申请相应大小的空间,一个int类型的变量时,需要占用4个字节的空间,如下图:

0818b9ca8b590ca3270a3433284dd417.png

也就是说,一个 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

...

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[" <

...

输出结果:

0818b9ca8b590ca3270a3433284dd417.png

第一个要注意的的是头两行告诉我们,整个数组变量arr 的地址,和第一个元素arr[0] ,二者的地址完全一样。

事实上,数组和元素,是对同一段内存的两种不同的表达。把这一段内存当成一个整体变量,就是数组,把这段内存分成大小相同的许多小段,就是一个个数组元素。

请参看下图:

0818b9ca8b590ca3270a3433284dd417.png

(分开一段段看是一个个元素,整体看称为一个数组 ,但二者对应的是同一段内存 )

第二个要注意的,大家算算相邻的两个元素之间地址差多少?比如&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;  // 换行

}

请大家分别上机试验这两段代码,对比输出结果,明白二维数组中各元素次序。下面是完整程序中,后一段代码的输出:

0818b9ca8b590ca3270a3433284dd417.png

现在说初始化时,如何省略指定二维数组的大小。

回忆一维数组的情况:

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 二维数组的内存地址

了解了二维数组的内存结构,我们再来说说几个关于二维数组地址问题,会有些绕,但并不难。嗯,先来做一个智力测试。

以下图形中包含几个三角形 ?

0818b9ca8b590ca3270a3433284dd417.png

正确答案是: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;

0818b9ca8b590ca3270a3433284dd417.png

上图表明: arr, arr[0], &arr[0][0] 都指向了同一内存地址。即: arr == arr[0] == &arr[0][0]。

另外: arr[1] == &arr[1][0] 及   arr[2] == &arr[2][0]。

我们可以有这些推论: 二维数组中的每一行,相当于一个一维数组。 或者说,一维数组是由多个简单变量组成,而二维数组是由多个一维数组组成。

示意图:

0818b9ca8b590ca3270a3433284dd417.png

例子:

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;

}

结果如:

0818b9ca8b590ca3270a3433284dd417.png

大家小时候有没刻过印章?哎!大概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”。下面是输出结果:

0818b9ca8b590ca3270a3433284dd417.png

请大家想想如何实现,具体代码请见课程配套代码源文件。

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

//---------------------------------------------------------------------------

#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即可。并且 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! 似乎是突然来了一个大程序?把它调通吧,下面是我运行这个程序的输出界面:

0818b9ca8b590ca3270a3433284dd417.png

仔细地想一想,我们至少还有一个重要功能没有实现,那就是排序。呵呵,关于排序,我们需要一整章来讲它。下面,还是说说如何数组的一些其它事情吧。如果你觉得有些累,就休息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 数组作为函数的参数

要学习这一章,首先确保你没有忘记“函数的参数”是什么?如果您有些模糊,就先复习函数的两章。

数组作为函数的参数,难点和重点都在于这两点:

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

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 )

0818b9ca8b590ca3270a3433284dd417.png

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

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

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

代码

说明

内存示意图

Func(a)

以数组a为参数,调用函数Func():

0818b9ca8b590ca3270a3433284dd417.png

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

在“支持多个班级的成绩管理系统”那个例子,我们写了不少函数,但没有哪一个函数用到数组参数。这是因为,作为程序中要用到的惟一一个数组数据:; int cj[CLASS_COUNT][CLASS_STUDENT_COUNT] ,它被我们定义为一全局 变量。全局变量不在任意函数内,所以不专属哪个函数,所有的函数都可以在函数中使用,所以我们没有必要通过参数来传递。

“成绩管理系统”的第二个功能:“清空成绩”,是一个将某一数组中的所有元素清0的过程。我们针对这一功能,首先,让我们自已写一个相应的函数。

// 函数:将一个整型数组中的指定元素值全部清0:

void ZeroIntegerArray(int arr[],int size)

{

for(int i=0;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个字节即可。再打比方吧,如果 你想你现在居住的三房二厅的房子送给我,你有两种办法:第一是把整个房子用倚天屠龙刀从楼幢里切出,然后买一巨大的信封(意指占用很大内存),寄给我(意 指将整个实际数组传给函数);另一种方法是将你的家钥匙用个小信封寄过来。

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

“传值”传复制

“传址”传地址

数组当参数

固定传地址

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值