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;
}