-- 简书作者 谢恩铭 转载请注明出处

第二部分第三课:数组


结束了上一课“指针”的有点艰难的旅程(其实上一课没有讲很深),C语言探索之旅 | 第二部分第二课:进击的指针,C语言的王牌!,这一课我们来学习“数组”这个C语言的重点。

我们将继续“一路向北”,“指”哪打哪。

为什么这么说呢,因为这一课我们还要涉及指针的知识,就如上一课说的,指针的使用几乎是贯穿C语言的,而且我们也会步步深入指针的学习。

不然指针怎么能被称为C语言的精华呢?所以“指针啊,天天见”,您以为指针会这么“放过”你么... Too young, too naive :P

想要现在逃避吗?那可不是成功者的表现哦。

很多学C语言的朋友,都觉得指针和数组貌似有点类似,又好像不同。有点扑朔迷离的感觉,“情深深,雨濛濛”,纠葛不清,难分难舍。

所以这一课我们就来解惑:到底指针和数组有什么联系和区别呢。

学完这一课相信会有些许拨云见雾的感觉。

在这一课中,我们一起学习如何创建“数组”这种数据类型(或者说是数据结构)。数组在C语言中也是极为重要的内容,所以大家不能因为过了指针那一坎,就不正襟危坐了。

我们会首先解释一下数组在内存中的机制(配图),对内存的解释始终是很重要的。因为理解好了内存的机制,C语言才能学得扎实。

所以很推荐大家花些时间去学习王爽老师编写的《汇编语言》第三版,对于理解C语言和计算机原理是很有帮助的,汇编语言可能不必学得很深,入门就好。

可以看我之前写的文章 学习汇编对编程有什么帮助?如何学习

一个程序员如果能很好地知道自己的程序背后的机理,方能写出稳定、健壮的程序。

内存中的数组


数组是在内存中具有连续地址的一系列相同类型的变量的集合

好吧,我知道这个定义“学究气太重,腐儒味更甚”。

简单地说,数组就是“巨大的变量”(怎么听起来那么变扭,幸亏我加了一个“的”字...),其中可以存储一系列相同类型的变量(long,double,int,char,等)。

数组中变量(可以称为数组元素或成员)的数目是固定的(当然也可以构造动态数组,以后再说),它可以包含2个,3个,10个,25个,2500个,甚至更多变量,由你决定存放数目。

下图展示了一个由四个元素组成的数组,首元素的地址是1600:

当你要创建包含4个元素的数组时,其实是首先向操作系统这个“大管家”发出请求:“能否给我在内存中分配一块地址,以存放这四个元素”。

操作系统一般都会应声而起,随传随到,乖乖分配你要的地址。

但是对于数组来说,这四个元素的存放地址是连续的,中间没有间隔,这也是数组的一个特点。

各个元素之间“亲密无间”。如上图所示,四个元素的地址分别是:1600,1601,1602,1603。

每一个地址的区块上存放相同类型的一个数字(说到底所有数据对于计算机来说都是数字么)。

如果数组是int类型的,那么每一个数组元素的地址块上就存放了一个int类型的数。我们不能在一个数组里既存放int型又存放double型,鱼与熊掌不可兼得也~

小结一下,对于数组:

  1. 当一个数组被创建时,它就占用了内存上地址连续的一块空间,数组的元素之间是一个接一个的。

  2. 数组的所有元素(成员)都必须是同一类型的数据,例如int型的数组,就只能存放int型的变量,而不能有其他变量。

定义一个数组


我们来学习如何定义一个数组。首先来定义一个包含4个int类型数据的数组:

int array[4];

你会说:原来这么简单……

是啊,就是这么简单,只需要在中括号里写上你需要的元素个数,一个数组就创建好了。

数组成员的个数一般来说没有限制,当然这取决于你的内存大小。

接下来,我们如何访问每一个数组成员呢?

也很简单,

array[成员编号]

注意:成员编号是从0,1,2,这样一直到数组元素个数减一。还记得以前说过电脑数数是从0开始的吗,因为电脑是用二进制的。

所以数组的第一个元素就是array[0],依此类推。所以上面的包含4个成员的数组,它的成员编号是没有4的,而是0,1,2,3。

如果我要将数组中的成员的值赋为像上图中一样,我可以这么做:

int array[4];

array[0] = 10;
array[1] = 23;
array[2] = 505;
array[3] = 8;

你会说:我可没看到数组和指针有什么联系啊。

事实上,如果你只写array,那就是一个指针,是指向数组首元素的首地址的一个指针。

例如:

int array[4];

printf("%d", array);

结果输出

1600

当然这里的1600是照应上面图示中来的,实际上你会得到其他的地址值。

如果你带有下标地访问,那会得到数组的对应那个下标的成员:

printf("%d", array[0]);

结果输出

10

对其他的下标也是类似。因为我们知道了单独用数组名,是表示一个指针,所以我们也可以这样来获得数组的首元素的值:

printf("%d", *array);

结果输出
10

类似地,我们也可以得到数组的第二个元素的值,通过这样:

*(array + 1)

所以这两个表达式的结果是一样的,都是23 :

array[1]

*(array + 1)

元素个数可变的数组


C语言有好多个版本(几乎所有编程语言都是),在最近的版本C99里,允许创建大小可变的数组,也就是元素的个数是一个变量:

int variable = 5;

int array[variable];

但是这个新特性可不是所有的C编译器都认识,所以有些版本的编译器就会在第二行出错。

我们课程里参考和基于的C语言标准是C89,所以我们的课程里就不允许有大小可变的数组了。

我们需要达成协议:
数组的元素个数(中括号里的数)必须是一个常量,不能是变量,连const变量也不行。
数组需要有一个固定的大小。

你会问:难道就真的不能创建元素个数可变的数组了吗?

答案是:是可以创建元素数目一开始不确定的数组的,即使在C89里。

但是要达到这样的目的,我们要使用另一种技术:动态分配。之后的课程会讲到。

遍历一个数组


假如我们现在要显示数组中每一个成员的值。

我当然可以一个一个用printf输出,但是这样的话可能代码就太多了。最好还是用一个循环来显示,比如常用的for循环:

int main(int argc, char *argv[])
{
  int array[4], i = 0;

  array[0] = 10;
  array[1] = 23;
  array[2] = 505;
  array[3] = 8;

  for (i = 0 ; i < 4 ; i++)
  {
    printf("%d\n", array[i]);
  }

  return 0;
}

程序输出:

10
23
505
8

我们的for循环借着一个称为i的变量来遍历我们的数组,其实i是很常用的变量名,大部分程序员都喜欢将其用于遍历数组,因为i是index(英语“下标”的意思)的首字母。

大家应该发现了:我们在定义一个数组时,在中括号[]里不能放一个变量(数组的成员个数需要确定),但是在遍历数组时却可以在中括号里放置变量。

注意:不要尝试访问array[4],因为你会得到任意数据,或者得到一个错误,因为这个地址已经产生了“数组越界”,操作系统就会中止你的程序,因为你的程序尝试访问一个没有权限访问的地址。

初始化数组


现在既然我们已经知道如何遍历一个数组了,那么我们应该也能很轻松地初始化一个数组了:我们可以用for循环来将数组的各个成员都初始化为0

int main(int argc, char *argv[])
{
  int array[4], i = 0;

  // 数组的初始化
  for (i = 0 ; i < 4 ; i++)
  {
    array[i] = 0;
  }

  // 打印数组各个成员来确定数值
  for (i = 0 ; i < 4 ; i++)
  {
    printf("%d\n", array[i]);
  }

  return 0;
}

输出:

0
0
0
0

另一种初始化数组的方式


看了上面的初始化方式,觉得还是不过瘾,我们须要知道还有另一种初始化的方式,就是这样写:

数组名[4] = {数值1, 数值2, 数值3,数值4};

简单说来,就是把各个成员的数值写在大括号里,用逗号隔开,如下:

int main(int argc, char *argv[])
{
  int array[4] = {0, 0, 0, 0}, i = 0;

  for (i = 0 ; i < 4 ; i++)
  {
    printf("%d\n", array[i]);
  }

  return 0;
}

输出也是:

0
0
0
0

实际上,也可以更简便。就是写上前几个成员的初始值,后面的成员的值,假如你没给出初值,是会自动初始化为0的:

int array[4] = {10, 23}; // 初始化的值 : 10, 23, 0, 0

第一个成员取到的值是10,第二个是23,第三和第四都初始化为0了。

那么如何简便地把数组的所有成员都初始化为0呢,只需要这样写:

int array[4] = {0};

这样,所有的成员都初始化为0了。

把数组传递给函数


在我们写程序的时候可能会需要把一个数组的所有成员的值显示出来,那为什么不把这个功能写成一个函数呢?

借着这个小程序,我们也可以学习如何将一个数组作为参数传递给函数。

我们需要传递两个参数给函数:数组(实际是数组的地址)和数组的大小。

我们之前说过,数组名直接用的话其实是一个指针,指向数组的首元素的首地址!

所以我们可以这样来写我们的程序:

void display(int *array, int arraySize);

int main(int argc, char *argv[])
{
  int array[4] = {10, 15, 3};

  // 显示数组内容
  display(array, 4);

  return 0;
}

void display(int *array, int arraySize)
{
  int i;

  for (i = 0 ; i < arraySize ; i++)
  {
    printf("%d\n", array[i]);
  }
}

程序输出:

10
15
3
0

上面的函数display看上去好像和我们之前在指针那一课的函数没什么区别,这个函数的第一个参数是一个指向int型的指针(我们的数组名 array)。

第二个参数是数组的大小(成员个数),为了知道我们的for循环什么时候中止。

也有另一种方式来表明一个函数接受一个数组作为参数,这样写:

void display(int array[], int arraySize);

这次我用了中括号,来表明参数接受一个数组,但其实传递给函数的还是数组的首元素,没有传递整个数组过去(因为拷贝整个数组是很大的开销)。

这样写的好处是不会让读者误以为接受的参数是一个普通的指针,当然这一次就不用在中括号里面写数组的大小了。

我写程序时两种方式都用,但一般为了不混淆,还是用中括号的方式多一些。

一些小练习


学了今天的课,想让大家自己实现一些和数组有关的函数。

这里只给出练习题的描述,大家需要自己思考如何实现这些函数。之后可以在我们的程序员联盟QQ群和微信群里讨论。

  • 练习1
    求数组的平均值。函数模板:
double arrayAverage(int array[], int arraySize);
  • 练习2
    写一个拷贝数组的函数,这个函数有三个参数,第一和第二个参数是数组名,第三个参数是数组大小。将第一个参数(数组)的内容拷贝到第二个参数(数组)里。函数模板:
void copyArray(int originalArray[], int copyArray[], int arraySize);
  • 练习3
    写一个函数,有三个参数,第一个是一个数组,第二个是数组大小,第三个是最大值。如果这个数组里有成员的值大于最大值,则将此成员的值变为0。函数模板:
void arrayMax(int array[], int arraySize, int valueMax);
  • 练习4
    这道练习题是最难的。写一个函数,来给数组按成员数值从小到大排序,比如,数组原先是[15, 81, 22, 13],排序后变为[13, 15, 22, 81] 。函数模板:
void orderArray(int array[], int arraySize);

加油吧,欢迎交流讨论。可以把写好的代码发到留言区。

总结


  1. 数组是一系列相同类型数据的集合,在内存中数组的各个成员是紧挨着的。

  2. 一般来说,数组的大小(成员的数目)必须是固定的,不能由一个变量来代替数组的大小(维度)。

  3. 一个int类型的数组的成员都必须是int类型,依此类推。

  4. 数组的每个成员的下标是从0开始,像 array[0], array[1], array[2] 这样。

第二部分第四课预告:


今天的课就到这里,一起加油咯。

下一次我们学习C语言探索之旅 | 第二部分第四课:字符串