C语言编程课程:深入数组与指针,掌握C语言精华
内容简介

1、课程大纲
2、第二部分第三课:数组
3、第二部分第四课预告:字符串
课程大纲
我们的课程包含四大部分。每一部分结束后,都会设置练习题,并且会将答案公布出来。同时,还会带领大家使用 C 语言来编写三个游戏。
[id_8057140[id_1327233952]]
C语言高级技术
用基于C语言的SDL库开发2D游戏
数据结构
第二部分第三课:数组
上一课是“指针”的旅程,有点艰难,不过其实没讲很深。这一课我们开始学习“数组”,它是 C 语言的重点。我们将继续前行,朝着某个方向前进,精准地到达目标。
为什么这么讲呢?因为这一课会涉及指针的知识。就如同上一课所说,指针的使用几乎贯穿整个 C 语言。并且我们会逐步深入地学习指针。如果不这样,又怎么能称之为 C 语言的精华呢?所以“指针啊,天天见”,你可别以为指针会轻易放过你,呵呵……
想要现在逃避吗?那可不是成功者的表现啊。
很多学习 C 语言的朋友认为,指针和数组看起来有一点相似之处,然而似乎又存在不同之处。他们有着一种难以捉摸的感觉,仿佛“情深深,雨濛濛”那般,相互纠结缠绕,难以清晰分辨,难以割舍分离。
所以这一课我们要进行解惑:指针和数组到底有怎样的联系呢?它们又有哪些区别呢?学完这一课后,相信会有一些能够拨开云雾、看清真相的感觉。“绿草萋萋,白雾迷离”,不禁让我想起了我很欣赏的歌手李健在《我是歌手》第三季中所唱的《在水一方》的歌词。
小编你又顽皮了...
言归正传。在这一课里,我们一同学习怎样创建“数组”这种数据类型,也就是数据结构。数组在 C 语言中是非常重要的部分,因此大家不能因为已经过了指针那一关,就不再认真对待了。
我们会先解释数组在内存中的机制,并且会配上相应的图。因为对内存的解释是很重要的。只有理解好了内存的机制,才能把 C 语言学得扎实。所以非常推荐大家花费一些时间去学习王爽老师编写的《汇编语言》第三版,这对于理解 C 语言和计算机原理很有帮助。汇编语言不需要学得很深,入门即可。
一个程序员能够很好地了解自己程序背后的机理,这对于写出稳定的程序是必不可少的条件;一个程序员能够很好地了解自己程序背后的机理,这对于写出健壮的程序也是必不可少的条件。
内存中的数组
《数组是在内存中具有连续地址的一系列相同类型的变量的集合》
好吧,我知道这个定义“学究气太重,腐儒味更甚”。
简单来说,数组可以被称作“巨大的变量”,不过这样说感觉有些别扭,幸好我加了一个“的”字。并且数组能够存储一系列相同类型的变量,比如 long 类型、double 类型、int 类型、char 类型等。
数组中的变量,也就是数组元素或成员,其数目是固定的。当然,也可以构造动态数组,这方面的内容稍后再讲。数组可以包含 2 个变量,也可以包含 3 个变量,还可以包含 10 个、25 个、2500 个甚至更多变量,具体存放数目由你决定。
下图展示了一个数组,该数组由四个元素构成。首元素的地址为 1600。
当要创建包含 4 个元素的数组时,会先向操作系统这个“大管家”提出请求。请求内容为:能否在内存中给分配一块地址,用来存放这四个元素。操作系统通常会立刻响应,随叫随到,乖乖地分配所需的地址。而对于数组而言,这四个元素的存放地址是连续的,中间不存在间隔,这是数组的一个特性。各个元素之间关系紧密,没有间隔。上图显示,四个元素的地址分别为:第一个元素地址是 1600,第二个元素地址是 1601,第三个元素地址是 1602,第四个元素地址是 1603。
每一个地址的区块会存放相同类型的一个数字。(毕竟说到底所有数据对于计算机而言都是数字呀。)如果数组是 int 类型的,那么每一个数组元素的地址块上就会存放一个 int 类型的数。我们不能在一个数组中既存放 int 型的数据,又存放 double 型的数据。
小结一下,对于数组:
当创建一个数组时,它会占用内存上的一块空间,且这块空间的地址是连续的。数组的各个元素是依次排列的,一个接着一个。
数组的所有元素必须是同一类型的数据。比如是 int 型的数组,就只能够存放 int 型的变量,不能有其他类型的变量。
定义一个数组
我们来学习怎样去定义一个数组,首先我们来定义一个有着 4 个 int 类型数据的数组。
int array[4];
你会说:原来它是如此简单……的确,确实就是这么简单,仅仅需要在中括号里把你所需要的元素个数书写下来,这样一个数组就被创建好了。数组成员的个数通常情况下是没有限制的,当然这是由你的内存大小所决定的。
接下来,我们如何访问每一个数组成员呢?
也很简单,array[成员编号]
成员编号是从 0 开始,一直到数组元素个数减一。之前说过电脑数数是从 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 循环那样:
主函数 main 带有两个参数,一个是整型的 argc,另一个是字符指针类型的 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
我们的 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;
输出:
另一种初始化数组的方式
看了上述的初始化方式后,感觉仍不满足,我们需要知晓还有另一种初始化的方式,其写法如下:
数组名[4] = {数值1, 数值2, 数值3,数值4};
简单来说,要将各个成员的数值写在大括号内,并且用逗号进行隔开,示例如下:
int main(int argc, char *argv[])
定义了一个整数数组 array,其包含 4 个元素,初始值都为 0,同时定义了一个整数 i 并初始化为 0。
for (i = 0 ; i < 4 ; i++)
printf("%d\n", array[i]);
return 0;
输出也是:
实际上,有更简便的方法。可以先写出前几个成员的初始值,对于后面的成员,如果没有给出初值,那么它们会自动被初始化为 0。
int 数组 array 有 4 个元素,分别被初始化为 10 和 23,其余两个元素初始化为 0
第四个成员也初始化为 0。
要简便地将数组的所有成员都初始化为 0 ,做法是这样的:
int array[4] = {0};
这样,所有的成员都初始化为0了。
把数组传递给函数
我们在写程序时可能会有把一个数组的所有成员的值显示出来的需求,那么为何不将这个功能写成一个函数呢?
我们可以借助这个小程序来学习怎样把一个数组当作参数传递给函数。
我们需要向函数传递两个参数,一个是数组,实际上是数组的地址;另一个是数组的大小。
我们之前讲过,直接使用数组名的话,它实际上是一个指针,这个指针指向数组的首元素的首地址。因此,我们能够这样来编写我们的程序:
定义一个函数 display,它接受一个整型指针 array 和一个整型数组大小 arraySize 作为参数。
int main(int argc, char *argv[])
int array[4] = {10, 15, 3};
// 显示数组内容
dispaly(array, 4);
return 0;
显示一个整数数组及其大小。这个整数数组由一个指向整数的指针所指向,数组的大小作为另一个参数传递进来。具体来说,有一个名为“array”的整数数组,通过“arraySize”这个参数来确定其大小,然后通过“display”函数来进行显示操作。
int i;
for (i = 0 ; i < arraySize ; i++)
printf("%d\n", array[i]);
程序输出:
10
15
上面的函数 display 看起来与之前在指针那一课的函数似乎没有区别。这个函数的第一个参数是一个指向 int 型的指针,也就是我们的数组名 array。第二个参数是数组的大小,即成员个数,这样做是为了知晓我们的 for 循环在何时中止。
也有另一种方式来表明一个函数接受一个数组作为参数,这样写:
函数 display 接收一个整数数组 array 和该数组的大小 arraySize 作为参数。
这次我使用了中括号,其作用是表明参数能够接受一个数组。然而实际上,传递给函数的依然是数组的首元素,并没有将整个数组传递过去,因为拷贝整个数组会是一个很大的开销。这样进行书写的好处在于,不会让读者误以为所接受的参数是一个普通的指针。并且,这一次也就不需要在中括号里面写入数组的大小了。
我在写程序的时候,会同时使用两种方式。不过,通常为了避免混淆,使用中括号的方式会更多一些。
一些小练习
学了今天的课,想让大家自己实现一些和数组有关的函数。
这里仅给出练习题的描述,大家需自行思考怎样实现这些函数。之后可于我们的程序员联盟的 QQ 群和微信群中进行讨论。
练习1
求数组的平均值。函数模板:
定义一个函数 double arrayAverage,它接收一个整数数组 array 和数组大小 arraySize 作为参数;
练习2
编写一个用于拷贝数组的函数,该函数具备三个参数。其中,第一个参数为数组名,第二个参数也为数组名,第三个参数则是数组的大小。接着,把第一个参数所对应的数组的内容复制到第二个参数所对应的数组中。函数的模板如下:
定义一个函数 copyArray,它接受三个参数,分别是原始数组 originalArray、目标数组 copyArray 和数组大小 arraySize,用于将原始数组的内容复制到目标数组中。
练习3
创建一个函数,它具备三个参数。第一个参数是一个数组,第二个参数是该数组的大小,第三个参数是最大值。倘若这个数组当中存在某个成员的值比最大值还要大,那么就把这个成员的值修改为 0 。
定义一个函数 void arrayMax,它接受一个整数数组 array、数组大小 arraySize 和一个整数 valueMax 作为参数。
练习4
这道练习题属于最难的那一类。需要编写一个函数,其作用是对数组按照成员的数值进行从小到大的排序。例如,原本的数组是[15, 81, 22, 13],经过排序后就会变成[13, 15, 22, 81]。函数的模板为:
定义一个函数名为 orderArray,该函数接收一个整型数组 array 和数组的大小 arraySize 作为参数。
加油吧,欢迎交流讨论。
总结
数组是由一系列相同类型的数据所组成的集合。在内存当中,数组的各个成员是相互挨着的。
通常来讲,数组的大小也就是其成员的数目,这种大小必须是固定不变的,不能够用一个变量去替代数组的大小或者维度。
每一个int类型的数组的成员都必须是int类型,依此类推
数组的每个成员都有下标,且下标从 0 开始,例如 array[0]、array[1]、array[2] 等。
第二部分第四课预告:
今天的课就到这里,一起加油咯。
下一次我们学习第二部分第四课:字符串