大家好,很高兴又和大家见面啦!前面咱们已经把函数的相关知识点学习完了,今天咱们将开始进入数组内容的学习。在本篇章中,我会给大家带来一维数组与二维数组的详细内容,接下来我们就开始今天的正题吧!
数组是一组相同类型元素的集合。
我们怎么来理解这个相同类型呢?这里我们可以借用数学的角度来理解;
在数学中,集合就是指具有某种特定性质的具体的或抽象的对象汇总而成的集体。其中,构成集合的这些对象则称为该集合的元素。
这里我们可以简单的举几个例子来说明集合:
这些集合里的元素都是满足了某一特定的条件,在计算机中,这一特定的条件就是相同的类型。
这里的类型指的就是元素的数据类型。所以我们需要有一个概念就是——
只有相同类型的元素才能组成一个集合。
在计算机语言中,我们把这种集合称为数组。
比如int类型的元素只能与int类型的元素组成集合称为整型数组;
char类型的元素只能与char类型的元素组成集合称为字符数组……
现在我们知道了什么是数组了,那我们如何用计算机语言来创建一个数组呢?下面我们就来介绍一下数组的创建方式;
对于一个数组的创建,我们需要有3个要素——数组元素类型、数组名以及数组的空间大小或者说是数组的元素个数。数组创建的格式如下所示:
//数组的创建
type_t arr_name[const_n];
//type_t——数组的元素类型;
//arr_name——数组的名字;
//const_n——是一个常量表达式,用来指定数组的大小;
按照这个格式,我们就可以尝试着创建出字符数组、整型数组、浮点型数组……
//字符数组
char ch[10];
//整型数组
short / int / long / long long arr[20];
//浮点型数组
float / double arr[30];
在这个格式中我们需要注意几个点:
在C99之后引入了变长数组的概念,允许我们可以使用变量指定数组大小。也就是说在支持C99标准的编译器比如gcc编译器是可以在创建数组时使用变量来表示数组大小的;
博主使用的编译器是VS2019,这个编译器下是不支持变长数组的,但是我们在创建数组时除了上述的这种按格式创建数组外,还可以像下面这种格式来创建数组:
type_t arr_name[] = { array_element };
//type_t——数组的元素类型;
//arr_name——数组的名字;
//array_element——数组元素;
通过这种格式创建的数组,其数组的大小与数组的元素个数是相等的,数组的大小确定后同样也是不可以更改的。
如下所示:
从这个监视窗口中我们可以看到,此时数组内的6个元素都已经完成的赋值,但是在进行第7个元素的赋值后系统报错了报错内容是数组arr的栈区损坏了。也就是说,通过这种格式创建的数组并不是代表它的大小可以被改变,数组的大小与数组定义时的元素各数是相同的。
这种省略数组大小的格式我们可以理解为是是以元素的个数表示数组大小。这种形式其实是直接在创建数组时就给数组进行初始化。下面我们就来探讨一下什么是数组的初始化;
在创建数组的同时给数组的内容一些合理初始值。
我们在对数组进行初始化时有两种形式——完全初始化和不完全初始化。
//数组的初始化
//整型数组
int main()
{
int arr1[5] = { 1,2,3,4,5 };//完全初始化
int arr2[] = { 6,7,8,9,10 };//完全初始化
int arr3[5] = { 11,12,13 };//不完全初始化
return 0;
}
在这个代码中我们可以看到,通过第一种格式创建的数组是可以进行完全初始化和不完全初始化的,但是对于第二种省略数组大小的创建格式来说,它就只有完全初始化这一种方式。
前面我们是以整型数组来举例的,下面我们再来看一个代码:
//字符数组
int main()
{
char ch1[] = "abcd";
char ch2[] = { 'a','b','c','d' };
return 0;
}
大家思考一下,这两个字符数组一样吗?
为了搞清楚这个问题,接下来我们来介绍一下数组内的元素;
对于整型数组来说,我们可以很容易理解,数组内的元素就是对应类型的数字,但是对于字符数组来说,它的元素是有两种书写形式的:
这种通过双引号引起的一个或多个字符被称为字符串。
那这字符串和单个字符又有什么区别呢?下面我们来探讨一下它们之间的区别。
这里我们需要引入一个库函数——计算字符串长度的库函数——strlen。
它的作用就是通过计算字符的个数来计算字符串的长度,我们在使用时需要引用头文件string.h。
下面我们就来看一下这两个数组的区别;
从这个结果中我们可以看到,不仅是计算的字符串长度不同,而且打印出来的字符串也是不一样。
为什么会有这种区别呢?下面我们来进行调试通过监视窗口观察一下:
调试的步骤:
F 10 − − > 调试 − − > 窗口 − − > 监视 − − > 监视 1 F10-->调试-->窗口-->监视-->监视1 F10−−>调试−−>窗口−−>监视−−>监视1
我们在打开监视窗口后在输入栏输入我们想要观察的对象就可以了,接下来我们就可以通过监视窗口观察两个数组的元素了:
在监视窗口中,我们可以得到以下几个信息:
由以上信息我们可以做个猜想,是不是只要我们在ch2中加入这个\0,那这两个数组存放的内容就一致了呢?下面我们就来实践一下:
可以看到此时我们打印的内容就完全一样了,现在我们就可以得到结论:
- 由双引号引起的字符串是由看的到的字符与看不到的\0组成的
- \0是字符串的终止符
那这个\0是怎么来的呢?如果此时我们只有一个双引号“”,朋友们你们说它里面有没有字符呢?
这就是我们要介绍的一个新的知识点——空字符串;
为了探讨一下这个空字符串里面的元素,下面我们可以通过监视窗口来观察一下这个空字符串:
可以看到在这个ch数组中是有元素的,而且只有一个元素,这个元素就是\0;
也就是说双引号其实是自带一个\0的。
现在对于字符串和字符我相信大家都已经了解了,下面我们再来了看一个代码:
//字符数组
#include
int main()
{
char ch1[5] = "abcd";
char ch2[5] = { 'a','b','c','d'};
//通过strlen来计算数组的字符串长度
printf("%d %d\n", strlen(ch1), strlen(ch2));
//%s——以字符串的格式进行打印
//将数组的字符以字符串的形式输出
printf("%s %s\n", ch1, ch2);
return 0;
}
这两个数组存放的元素有没有区别呢?下面我们来看一下运行结果:
从运行结果中我们可以看到,两个数组的打印内容完全一致,为什么会这样呢?下面我们就来介绍一下完全初始化和不完全初始化的区别;
现在我们对这个代码直接通过监视窗口来查看这两个数组里的元素:
从监视窗口中我们可以看到此时的两个数组存储的元素是一致的,都是5个元素而且都是由字符a/b/c/d和\0组成的。
那现在问题就来了,我们在数组ch2中只给4个元素进行了初始化呀,为什么第五个元素变成了\0?
为了解开这一疑惑,我们再来看一组代码:
//不完全初始化
int main()
{
char ch1[5] = { 'a' };
char ch2[5] = { 'a','b' };
char ch3[5] = { 'a','b','c' };
char ch4[5] = { 'a','b','c','d'};
return 0;
}
在这个代码中,我们对四个数组都进行了不完全初始化,下面我们来通过监视窗口看一下数组里的元素的情况:
从监视窗口中我们可以看到,这四个数组中未被初始化的元素都变成了\0,那是不是只有字符数组这样呢?下面我们来测试一下整型数组和浮点型数组:
此时的数组并未进行初始化,我们可以看到数组里的内容都是随机值,下面我们给数组进行初始化再看看:
现在我们已经完成了这三个数组的不完全初始化,此时我们可以看到不管是字符数组还是整型数组亦或是浮点型数组除了首元素被初始化为确定值以外,其它的元素同时也被初始化为0;
也就是说所谓的不完全初始化其实也是完全初始化,只不过初始化的方式与完全初识化不一样,完全初始化是每个元素都被赋予了一个确定的值,而不完全初始化未被赋予确定值的元素会被赋予0;
那现在我们就可以得到结论:
现在我们已经完成了数组的创建和初始化的过程,接下来我们就要开始对数组进行使用了;
对于数组的使用,我们需要介绍一个操作符:[]——下标引用操作符。它其实就是数组访问数组元素的操作符。通过这个操作符,我们对数组就有两种使用方式:通过数组下标访问数组元素、通过数组下标计算数组大小。
在介绍通过下标来访问数组元素之前我们先需要了解什么是数组下标;
下面我们还是通过代码来介绍数组的下标:
从监视窗口中我们可以得到以下的信息:
现在我们知道下标是什么了,那我们如何来通过下标来访问元素呢?
在使用数组下标来访问数组元素的格式很简单:
//访问格式
//数组名+下标引用操作符[]+元素下标
//书写形式
//数组名[元素下标]
//如:arr[0]/arr[1]/arr[2]……
下面我们来尝试着通过下标来将数组的元素打印在屏幕上:
可以看到我们现在确实可以通过下标来访问数组的各个元素。
这里我们需要注意一件事:
!!!通过下标引用操作符访问数组元素和数组定义是两码事
下面我们来看一下它们的异同点;
访问数组元素和定义数组有以下几个异同点,我们借助代码来进行说明:
int arr[5] = { 1,2,3,4,5 };
arr[0];
arr[1];
arr[2];
arr[3];
arr[4];
- 数组名相同——都是arr;
- 括号相同——都是[]
- 定义数组时有数组元素的类型,通过下标访问数组元素时,没有元素类型;
- 定义数组时中括号里的数字代表的是数组大小,通过下标访问数组元素时,中括号里的数字代表的是元素下标;
- 数组的大小比元素最大的下标多1;
所以大家要清楚对于int arr[4];
与arr[4];
这是两个意思,前一个是在创建数组,后一个是在通过数组下标引用数组元素;
现在我们知道了通过下标可以访问数组元素,接下来我们来介绍第二种用法——通过数组下标计算数组大小;
看到这个作用,可能就会有朋友奇怪了,为什么我们需要通过数组下标来计算数组的大小呢?我们在创建数组时中括号的数组不就代表着数组大小吗?别着急,下面我们来看一下这个代码:
//通过数组下标计算数组大小
int main()
{
char ch[] = "asdfsandgjinwengasdhfasflsdannsiadnf";
return 0;
}
对于这个数组,大家能第一时间说出它的大小吗?这只是一个例子,可能之后我们还会碰到元素更多的数组,到那时我们想要计算数组的大小不可能说一个元素一个元素的去数吧,这里就需要我们借助下标来计算数组的大小了。
那我们应该如何计算呢?下面我们需要介绍一个C语言的操作符——sizeof;
sizeof是用来计算操作数所占内存空间大小的操作符,单位是字节。
这里的操作数可以是变量、数据类型也可以是数组。
它的使用格式也很简单——sizeof(操作数);
接下来我们就来尝试着借助操作数来计算数组的大小以及数组元素的大小;
从这个测试结果中我们可以看到,数组ch所占内存空间大小为37,ch的首元素所占空间大小为1;数组arr所占内存空间大小为84,arr的首元素所占空间大小为4;
我们在使用sizeof这个操作符计算数组所占空间大小时,它其实是求的数组中所有元素所占空间的总大小。
也就是说,如果我有了数组中所有元素所占空间的总大小以及一个元素所占空间大小时,我只需要用总大小除以一个元素所占空间大小是不是就能得到元素的总个数也就是数组的大小了呢?下面我们就来测试一下:
//一维数组的使用
//通过数组下标计算数组大小
int main()
{
char ch[] = "asdfsandgjinwengasdhfasflsdannsiadnf";
int sz1 = sizeof(ch);//整个数组所占内存空间大小
int sz2 = sizeof(ch[0]);//数组首元素所占内存空间大小
printf("sz1 = %d\n", sz1);
printf("sz2 = %d\n", sz2);
printf("sz_ch = %d\n", sz1/sz2);
int arr[] = { 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,11,1,1 };
int sz3 = sizeof(arr);//整个数组所占内存空间大小
int sz4 = sizeof(arr[0]);//数组首元素所占内存空间大小
printf("sz3 = %d\n", sz3);
printf("sz4 = %d\n", sz4);
printf("sz_arr = %d\n", sz3 / sz4);
int test[5] = { 1,2,3,4,5 };
int sz5 = sizeof(test);//整个数组所占内存空间大小
int sz6 = sizeof(test[0]);//数组首元素所占内存空间大小
printf("sz5 = %d\n", sz5);
printf("sz6 = %d\n", sz6);
printf("sz_test = %d\n", sz5 / sz6);
return 0;
}
在这个代码中我们通过数组test来验证咱们答案的准确性。对于数组test,我们可以看到,它是一个大小为5的整型数组,如果计算的结果也是5,那就说明我们的想法是正确的。下面我们来看一下运行结果;
从测试结果中可以看到我们确实可以通过将数组所占空间大小除以数组元素所占空间大小从而来计算数组的大小。
那我们现在可以出结论:
数组大小 = 数组所占空间大小 / 数组元素所占空间大小 数组大小=数组所占空间大小/数组元素所占空间大小 数组大小=数组所占空间大小/数组元素所占空间大小
s z a r r = s i z e o f ( a r r ) / s i z e o f ( a r r [ 0 ] ) sz_{arr}=sizeof(arr)/sizeof(arr[0]) szarr=sizeof(arr)/sizeof(arr[0])
这里的数组元素可以是数组中的任一元素,之所以取首元素是因为一个数组肯定会有下标为0的首元素,但不一定会有下标为其它的元素。
如:现在有一个数组
int arr[]={1,2,3,4,5};
这个数组中如果我们要计算它的大小我们可以使用下标0、1、2、3、4这五个下标中的任意一个;
通过下标引用操作符来访问也就是arr[0]
、arr[1]
、arr[2]
、arr[3]
、arr[4]
这五个元素的任意一个;
但是不能使用arr[5];
因为在这个数组中并没有下标为5的元素
为了避免出错,建议大家在计算数组大小时尽量使用arr[0]
现在我们已经介绍完了数组的创建、初始化以及使用,接下来我们来看看数组在内存中是如何存储的;
我们要介绍数组在内存中的存储之前,我们需要先了解一下内存的相关知识点;
计算机要存储数据的话有以下几种途径,按访问速度由快到慢来排列分别是:
寄存器 > 高速缓存 > 内存 > 硬盘 寄存器>高速缓存>内存>硬盘 寄存器>高速缓存>内存>硬盘
它们的存储空间大小是依次增大的,寄存器的存储空间大小最小,硬盘存储空间大小最大。
计算机在进行运算时是先从内存中提取数据然后再将数据输送到中央处理器(CPU)进行运算。
刚开始的时候内存的读取速度和CPU处理数据的速度是非常匹配的,但是随着时代的发展,CPU的处理速度越来越快,快到内存的访问速度跟不上了,后面为了解决这个问题,就开始出现了有更快访问速度的高速缓存和比高速缓存访问速度更快的寄存器。
在这之后CPU在处理数据时会先在寄存器里拿取需要处理的数据,如果没有,再去访问高速缓存,还是没有,再去访问内存,像这样的一个访问流程大大提高了计算机的运行速度。
寄存器中的数据又是怎么来的呢?它是先由内存里的数据下载到高速缓存,再由高速缓存下载到寄存器这样一个流程。
这里就需要提到register——寄存器关键字。
这个关键字是干嘛用的呢?你可以理解为它就是将数据下载到寄存器里的通道;
比如我想定义一个变量int a = 10;在后面的代码中我需要多次使用它,为了更快的读取这个数据,我就可以将它定义为寄存器变量——register int a = 10;
但是有一个问题,前面也提到了寄存器是空间最小的,如果我把所有内容都放到寄存器里面,它也装不下呀,那怎么办呢?
这里我们就要知道register这个通道它不是一路通畅的,它是会经过筛选的,它会筛选出真正需要的数据,它筛选的途径又是什么呢?
它筛选的途径就是咱们使用的编译器,我们在定义寄存器变量后,这些被定义的变量就像是拿到一张入场券一样,它们要先到编译器的面前接受审核,审核通过了,才能进入寄存器。
所以我们又可以将register定义的变量称为建议将其定义成寄存器变量。
上面的内容我们只需要了解计算机有寄存器、高数缓存、内存、硬盘这四种存储方式和register整个寄存器关键字的作用就行了,不需要去深究,这里我就不多说了。
通过前面的内容我们了解到了下面几点:
这里我们可以知道,内存它是一个空间,它是一个可以存放和读取数据的空间;
我们可以将内存想象成冰箱,里面可以存放各种各样的东西。
我们为了更加高效的使用冰箱,我们就将冰箱分成了不同的区块——有冷藏区和冷冻区;
冷藏区又会根据自己的喜好不同分成不同的小分区,有存放蔬菜的,有存放水果的,有存放饮料的,有存放零食的等等;
为了快速的找到这些分区我们会给他们进行取名,或者是编号。
比如存放蔬菜的我把它叫做蔬菜区或者把它叫做1号,存放水果的我把它叫做水果区或者把它叫做2号……
我们下次在使用这些分区时只需要找到它们对应的名称或者编号是不是就可以了。
在我们的内存中也同样如此:
内存被划分成了一个个小的内存单元,每个内存单元都有它相应的编号,这些编号我们称为内存单元的地址。
大家再思考一个问题,我们在给冰箱分区的时候是不是每个空间都会有一定的大小啊,那在内存中一个内存单元又有多大呢?这里我们直接记结论——每个内存单元的大小是1个字节;
既然在内存中,每一个内存单元都有自己的地址,那地址的表现形式是什么呢?
我们可以通过%p——以地址的格式进行打印和操作符&——取地址操作符来看一下地址的表现形式,如图所示:
我们可以看到,这个地址打印出来,又是数字又是字母的,这是什么东西呢?
其实这种表现形式是通过16进制来表现的。那什么是十六进制呢?下面我们就来介绍一下;
十六进制是在十进制的基础上加上了字母 A − F A-F A−F,这些字母分别代表 11 − 15 11-15 11−15;
十进制是逢十进一,而十六进制则是逢十六进一。十六进制的数按从小到大排列分别是:
// 十六进制
十六进制数:0 1 2 3 4 5 6 7 8 9 A B C D E F
十进制数:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
计算机在存储数据的时候,肯定不是按十六进制来存储的,计算机存储数据只能通过二进制来存储,但是在向我们展示的时候则是通过十六进制来进行展示,那这个十六进制转化为二进制又是多少呢?我们可以借助程序员计算器来看一下:
可以看到,这个十六进制的数转化为二进制时是:1000 1111 1111 1010 1001 1100。
这时候有朋友就会问了,地址在32位系统中不应该是32个比特位吗?为什么这里只有24位,还有八位呢?
这个问题问的非常好,剩下的八位去哪了呢?我们不妨再仔细看一下打印出来的地址前面两位是什么?
它完整的地址应该是008FFA9C,这里每个数值对应4个比特位,前面我们才提到十六进制的每一个数对应的十进制的数分别是什么,下面我们就来将其转换成二进制来看看:
十六进制 十进制 二进制
0 0 0000
0 0 0000
8 8 1000
F 15 1111
F 15 1111
A 10 1010
9 9 1001
C 12 1100
现在我们再跟刚刚转换的值对比一下,有没有发现是不是漏掉了两个0所对应的二进制数值呀,所以这里完整的二进制位应该是:
0000 0000 1000 1111 1111 1010 1001 1100
有朋友就会问了,这个十六进制、十进制、二进制它们究竟是如何转化的呢?我们总不可能手上都带着电脑吧,没有带电脑手机的时候我们应该如何进行进制之间的转化呢?下面我们就来介绍一下进制的转化过程;
二进制转化成十进制,这里我拿1110来举例:
1110 转换为十进制 = 1 × 2 3 + 1 × 2 2 + 1 × 2 1 + 0 × 2 0 = 8 + 4 + 2 + 0 = 14 1110转换为十进制=1×2^3+1×2^2+1×2^1+0×2^0=8+4+2+0=14 1110转换为十进制=1×23+1×22+1×21+0×20=8+4+2+0=14
我们可以看到这里的每一位都代表着2的n次方,从右往左依次是0/1/2/3……
二进制位越多,那2的次方也就越多,32个二进制位也就是最高位是231;
十进制转化成二进制,这里我们还是用14来举例:
14 / 2 = 7 … 0 14/2=7…0 14/2=7…0
7 / 2 = 3 … 1 7 /2=3…1 7/2=3…1
3 / 2 = 1 … 1 3 /2=1…1 3/2=1…1
1 / 2 = 0 … 1 1 /2=0…1 1/2=0…1
大家通过这个计算有没有发现什么?
14对应的二进制位从右到左是不是通过除以2取的余数呀,第一次除以2的余数就是二进制位的第一位,第一次除以2的余数就是二进制位的第二位……
以此类推,第几次除以2所得的余数就是第几位的二进制位;
下面我们可以得到结论:
二进制转化成十进制 = 所有二进制位的值 × 2 对应位数 − 1 之和 二进制转化成十进制=所有二进制位的值×2^{对应位数-1}之和 二进制转化成十进制=所有二进制位的值×2对应位数−1之和
十进制转化成二进制 = 十进制数 / 2 取余数 十进制转化成二进制=十进制数/2取余数 十进制转化成二进制=十进制数/2取余数
十六进制转化成十进制,这里我拿9C来举例:
9 C 转化成十进制 = 9 × 1 6 1 + C × 1 6 0 = 144 + 12 = 156 9C转化成十进制=9×16^1+C×16^0=144+12=156 9C转化成十进制=9×161+C×160=144+12=156
我们可以看到,十六进制转十进制和二进制转十进制是一样的,只不过二进制是2的n-1次方,十六进制则是16的n-1次方;
十进制转化成十六进制也是一样,我们同样还是拿156来举例:
156 / 16 = 9 … 12 156/16=9…12 156/16=9…12
9 / 16 = 0 … 9 9/16=0…9 9/16=0…9
12对于的十六进制的数是C,所以最后我们可以得到的十六进制数就是9C;
下面我们可以得到结论:
十六进制转化成十进制 = 所有十六进制位的值 × 1 6 对应位数 − 1 之和 十六进制转化成十进制=所有十六进制位的值×16^{对应位数-1}之和 十六进制转化成十进制=所有十六进制位的值×16对应位数−1之和
十进制转化成十六进制 = 十进制数 / 16 取余数 十进制转化成十六进制=十进制数/16取余数 十进制转化成十六进制=十进制数/16取余数
我相信现在大家对十进制、十六进制、二进制的相互转化应该是没什么问题了。接下来我们继续介绍数组及数组元素的地址;
我们通过下面的代码来看一下一维数组是如何在内存中存储的:
//一维数组在内存中的存储
int main()
{
char a[] = "abc";
int sz1 = sizeof(a) / sizeof(a[0]);//数组a的大小
printf("&a=%p\n", &a);//数组a的地址
for (int x = 0; x < sz1; x++)
{
printf("&a[%d}=%p\n", x, &a[x]);//打印数组a各元素的地址
}
short b[] = { 1,2,3,4 };
int sz2 = sizeof(b) / sizeof(b[0]);
printf("\n&b=%p\n", &b);
for (int y = 0; y < sz2; y++)
{
printf("&b[%d}=%p\n", y, &b[y]);
}
int c[] = { 1,2,3,4 };
int sz3 = sizeof(c) / sizeof(c[0]);
printf("\n&c=%p\n", &c);
for (int z = 0; z < sz3; z++)
{
printf("&c[%d}=%p\n", z, &c[z]);
}
return 0;
}
这里我分别定义了字符数组a、短整型数组b、和整型数组c,我们既然要了解数组在内存中的存储,那我们就需要知道它们在内存中的地址,我们通过数组的地址与数组元素的地址来说明它们在内存中是如何存储的:
从这个打印结果我们可以看到数组的地址与数组第一个元素的地址相同,
在char类型的数组中,元素的地址相差1;
在short类型的数组中,元素的地址相差2;
在int类型的地址中,元素的地址相差4。
下面我们来看一下这些数据类型在内存中所占空间大小:
可以看到,在内存中不同类型所占空间大小如下:
char类型在内存中所占空间大小刚好是1个字节;
short类型所占空间大小是2字节;
int类型所占空间大小是4字节;
这样一对比有没有发现什么呀?
数组元素的地址之间的差值与元素的数据类型所占空间大小相等。
也就是说不管是字符数组还是整型数组,数组元素的地址都是紧挨着的,而且数组元素的地址是从低地址到高地址进行连续存放的。现在我们就可以得出以下结论:
现在我们就介绍完了一维数组的全部内容,接下来我们来介绍一下二维数组;
对于二维数组,我是这样理解的:
一维就是一条线,二维就是一个面,那一维数组就是只有一行或者一列的数组,而二维数组则是拥有行和列的数组。
既然我们理解的二维数组具有行和列,那我们就需要有两个下标来进行表示,如:
//二维数组的创建
char a[1][2];
short b[1][2];
int c[1][2];
和一维数组的创建方式一样,有数组元素类型、数组名、以及数组的大小,但是这里的大小由两部分组成,这里我们理解的是行与列两部分,具体是不是呢?我们接着往下看;
和一维数组一样,二维数组同样也是分为两种初始化:完全初始化和不完全初始化。
数组初始化的元素个数与数组大小相同;
数组初始化的元素个数小于数组大小,未被初始化的元素默认为0;
在一维数组中,我们知道了初始化就是在创建数组时给数组的内容一些合理初识值,那二维数组又应该怎样赋值呢?我们通过代码来说明二维数组的初识化:
在代码中我们先定义了一个二行三列的二维数组,随即就给它赋值了4个元素,从调试中我们可以看到,
各个元素的下标分别是: 00 、 01 、 02 、 10 、 11 、 12 00、01、02、10、11、12 00、01、02、10、11、12;
对应的元素名称为: a [ 0 ] [ 0 ] 、 a [ 0 ] [ 1 ] 、 a [ 0 ] [ 2 ] 、 a [ 1 ] [ 0 ] 、 a [ 1 ] [ 1 ] 、 a [ 1 ] [ 2 ] a[0][0]、a[0][1]、a[0][2]、a[1][0]、a[1][1]、a[1][2] a[0][0]、a[0][1]、a[0][2]、a[1][0]、a[1][1]、a[1][2]
有没有一种熟悉感,是不是和线性代数学的行列式很相似啊,既然这样那我们是不是可以把这个数组的元素用图像表示出来呢?
这里我们可以总结一下几点内容:
那我能不能把1、2赋值给第一行的两个元素,把3、4赋值给第二行的两个元素呢?答案是可以的。如图所示:
这里的初始化方式我是这么理解的,既然二维数组分行和列的话,通过元素的下标我们可以将行相同的元素看做一个整体,或者说看做一个一维数组也就是int a0[3];
和int a1[3];
两个数组;
那我的二维数组我就可以写成
int a[2][3]={a0[3], a1[3]};
//int——数组元素类型;
//a——二维数组名;
//2——一维数组的个数;
//3——一维数组的元素个数;
//a0——数组首元素数组名;
//a1——数组第二个元素数组名;
现在我们再来给这个数组初始化的话是不是就相当于分别给a0[3]
和a1[3]
这两个数组初始化呢?
所以我们只需要把需要赋给它们的初始值用大括号括起来就OK了。也就是:
a0[3]={1, 2};
a1[3]={3, 4};
int a[2][3]={{1, 2}, {3, 4}};
接下来有朋友可能就会提问了,在一维数组中我们有提到过可以省略数组大小直接给数组进行初始化的方式,也就是在创建数组时,不给定数组大小,只给定数组初始化的元素,如a[]={1,2,3,4,5,6}
。
那在二维数组中有没有这种创建数组的方式呢?下面我们来测试一下,分别从省略行和列、省略行、省略列来进行探讨:
在省略行和列时,系统会报错说明a缺少下标,并在第二个中括号下面标注了一下;
我们在省略行时,代码成功编译,并且根据列的大小将元素划分成了两组;
我们在省略列时,系统再次报错,这一次报错了两个内容,一个缺少下标,一个初始值设定项值太多,并在代码的第二个中括号和元素的第四个元素下做了标注。
从上面的结果,我们可以得出以下结论:
二维数组的初始化,我相信各位朋友都了解了,接下来我们来看一下二维数组是如何使用的;
在一维数组中,我们尝试过通过下标来访问各个元素,并将元素打印出来,那在二维数组中又可以不可以呢?下面我们来尝试一下:
//通过下标访问二维数组的元素
int main()
{
int a[3][4] = { {1,2,3},{4,5,6},{7,8,9,10} };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
在这个代码中,我们通过变量i代表数组的行下标,也就是一维数组的个数,变量j代表数组的列下标,也就是一维数组的元素个数。接下来我们运行一下:
从结果我们可以看到,在二维数组中我们依旧可以通过下标来访问数组的各个元素;
和一维数组一样,我们也是借助sizeof——计算操作数所占内存空间大小操作符,单位为字节来计算二维数组大小:
从结果我们可以看到,二维数组同样也能够通过下标来计算二维数组的大小;
在前面我们也提到了,我们在初始化数组时,可以省略第一个值,但是不能省略第二个值,而且通过两个下标的乘积我们可以确定数组的大小,那我在省略第一个值的情况下,我能不能通过下标来计算第一个值呢?
这里我们可以看到,我们可以通过第二个值来计算第一个值。
既然已经知道了二维数组时如何使用的了,那我们再来探讨一下,二维数组在内存中又是如何存储的;
在一维数组中我们知道了数组在内存中通过地址进行存储,地址又通过十六进制的形式被打印出来,在一维数组中,数组中的元素是由低地址到高地址连续存放的,那在二维数组中,又会是怎样一个情况呢?我们通过代码来看一下:
//通过下标访问二维数组的元素
int main()
{
int a[][4] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&a = %p\n", &a);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("&a[%d][%d] = %p\n", i, j, &a[i][j]);
}
}
return 0;
}
在这个代码中,我们先通过二维数组的两个下标来访问二维数组的每一个元素,再通过&——取地址操作符来将每个元素的地址给取出来并通过%p——以地址的格式进行打印将元素的地址给打印出来;
从打印的结果中我们可以看到,整型二维数组a的地址与首元素的地址相同,而且每个元素的地址都是相差4个字节,从这里我们可以得出以下结论:
从这个结论中我们对二维数组的理解要稍微变化一下了,前面我们对二维数组的第一个理解是二维数组是由行和列组成的,所以我们对二维数组的创建是:
//二维数组的创建
type_t arr_name[row][arow]
type_t——数组元素类型
arr_name——数组名
row——行
arow——列
之后我们通过对二维数组的行看做一个整体,将二维数组的元素理解为多个一维数组,所以我们对二维数组的创建是:
//二维数组的创建
type_t two_dim_arr_name[one_dim_arr_num][one_dim_array_element_num]
type_t——数组元素类型
two_dim_arr_name——二维数组名
one_dim_arr_num——一维数组名
one_dim_array_element_num——一维数组元素个数
通过二维数组在内存中是连续存放的这个信息,那对二维数组的理解应该变成区域数量和区域大小会更加合适,也就是:
//二维数组的创建
type_t arr_name[zone_num][zone_size]
type_t——数组元素类型
arr_name——数组名
zone_num——区域数量
zone_size——区域大小现
我们现在再回过头来理解二维数组的创建:
//二维数组的创建
char a[1][2];
//char——字符类型;
//a——数组名;
//1——有1个分区;
//2——分区大小为2,也就是每个分区里面有两个元素
short b[3][4];
//short——短整型类型;
//b——数组名;
//3——有3个分区;
//4——分区大小为4,也就是每个分区里面有4个元素
int c[5][6];
//int——整型类型;
//c——数组名;
//5——有5个分区;
//6——分区大小为6,也就是每个分区里面有6个元素
那现在就能对一些问题进行合理的解释了:
为什么我们在初始化二维数组时第一个值可以省略,但是不能省略第二个值?
以二维数组的元素是一维数组的理解来解释这个问题就是:
综上所述,所以我们可以得到:
我们在定义二维数组时,数组元素的个数可以省略,但是必须要确定每个元素的大小;
为什么第一个值与第二个值相乘等于二维数组的大小?
所以我们可以得到:
二维数组的大小 = 分区的个数 × 一个区域的大小 二维数组的大小=分区的个数×一个区域的大小 二维数组的大小=分区的个数×一个区域的大小
二维数组有三种理解方式:
这里我们以多个大小相同的分区的集合这种理解来对二维数组的知识点进行汇总。
二维数组是根据数组的区域个数与每个区域的大小来进行创建的,创建二维数组的结构如下:
//二维数组的创建
type_t arr_name[zone_num][zone_size]
type_t——数组元素类型
arr_name——数组名
zone_num——区域数量
zone_size——区域大小
在对二维数组进行初始化时,有两种方式:
直接初始化时,数组会根据区域大小依次将区域内的元素进行初始化,未被初始化的元素由0初始化,如:
//直接初始化
int a[2][3] = { 1,2,3,4 };
在创建二维数组时,我们可以通过省略分区的数量来进行二维数组的创建,此时的二维数组进行的也是直接初始化:
分区域初始化时,我们需要用大括号将各区域分开,未被初始化的元素由0初始化,如:
//分区域初始化
int b[3][4] = { {1,2},{3,4} };
在二维数组中我们可以:
二维数组在内存中的存储与一维数组相同:
数组的下标是由范围限制的。
C语言本身是不做数组下标的越界检查,编译器也不一定报错;
但是编译器不报错,并不意味着程序是正确的,所以程序员写代码时最好自己做越界的检查。
总结:我们自己在创建数组时,要注意元素的个数不要超过数组的大小,避免数组越界。 总结:我们自己在创建数组时,要注意元素的个数不要超过数组的大小,避免数组越界。 总结:我们自己在创建数组时,要注意元素的个数不要超过数组的大小,避免数组越界。
在上一个篇章中我们介绍了函数的相关知识点,往往我们在写代码时,会将数组作为参数传给函数,我们在介绍函数传参的时候有介绍过两种传参方式——传值与传址。
那我们在将数组作为参数进行传参时,传的是什么内容呢?下面我们就来探讨一下数组名的含义;
在介绍这个知识点之前,我们先看一个代码:
int main()
{
char a[] = "abcdefg";
//将数组a以字符串的形式打印出来
printf("%s\n", a);
//将数组a以地址的形式打印出来
printf("%p\n", a);
//将数组a的地址打印出来
printf("%p\n", &a);
//将数组a的首元素地址打印出来
printf("%p\n", &a[0]);
return 0;
}
大家说这个结果会是什么样的呢?下面我们一起来看一下这个代码的运行结果:
在这个结果中我们可以得到一下结论:
在前面我们有介绍过数组中的元素在内存中是由低地址到高地址连续存放的,每个元素的地址间相差的大小等于元素类型所占空间的大小。
既然这样,那我们不妨尝试一下通过给数组名加上一个元素类型的大小、给数组的地址加一个元素类型的大小以及给首元素的地址加一个元素类型的大小,我们创建数组的元素类型是char,这个类型所占空间大小为1,所以下面我会给数组名、数组的地址以及首元素的地址分别加上1,看看会是什么结果:
这个结果就有点意思了,我们从结果中可以看到,将数组名和首元素的地址+1得到的地址与第二个元素的地址相同,但是在数组的地址加上1后得到的地址比元地址多了8个字节。
按照数组元素的地址是连续存放的我们可以得到数组的第8个元素,也就是\0的地址应该比第一个元素的地址多7个字节,但是这里却多了8个字节,这也就表明,此时打印出来的地址并不是数组a里面任何一个元素的地址,这里我们画图表示的话应该是:
从图中我们可以看到,打印出来的地址是跟数组a连续存放的一个地址,也就是说我们将a的地址取出来的时候,取的是整个数组的地址,当数组地址+1后得到的是与数组连续存放的一个地址。此时我们可以得到结论:
int main()
{
char a[] = "abcdefg";
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%c ", *(a + i));
printf("&a[%d]=%p\n", i, a + i );
}
printf("\n");
int b[] = { 1,2,3,4,5,6,7,8,9 };
for (int i = 0; i < sizeof(b) / sizeof(b[0]); i++)
{
printf("%d ", *(b + i));
printf("&b[%d]=%p\n", i, b + i );
}
return 0;
}
在这个代码中,我们通过数组名+元素下标找到各元素的地址,然后通过*——解引用操作符将地址中的元素提取出来,运行结果如下所示:
这个结果也进一步证实了我们的结论,通过数组名可以访问数组各元素的地址。
这里有一点要注意,当我们用sizeof(数组名)来求数组所占空间大小时,此时的数组名代表的是整个数组的所有元素,求出来的值是所有元素共占空间大小。
我对这个知识点还有一个理解,这里分享给大家:
数组的内容我们基本上介绍完了,下面我们来进行实战来进一步巩固数组与函数的相关知识点;
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
简单点理解就是冒泡排序是一种排序的方法,可以将一组数按升序(从小到大)也可以按降序(从大到小)进行排序。
排序的实现是通过不断重复两数之间比较大小并进行换位,直到所有数完成升序或者降序排列才停止。
在介绍完冒泡排序后,我们就要开始进行代码编写的设计了。从上述的内容我们不难想到,完成这个问题可以通过循环实现,那现在我们来尝试编写一下代码来实现冒泡排序:
//冒泡排序
int main()
{
int a[] = { 3,4,6,5,1,7,2,9,8 };
//通过冒泡排序完成升序排列
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
int j = 0;
for (j = i+1; j < sizeof(a) / sizeof(a[0]); j++)
{
if (a[i] > a[j])
{
int k = a[i];
a[i] = a[j];
a[j] = k;
}
}
printf("%d ", a[i]);
}
return 0;
}
这是通过在主函数里面编写代码,这样的编写思路又是什么呢?
下面我通过图解来给大家解析一下我的编写思路:
在明确了设计思路后,我们开始进行函数的设计,并通过函数来完成排序:
第一步,我们在主函数内部要定义一个需要进行冒泡排序的数组,然后设计一个函数将其进行排序:
前面我们学习了数组名的含义,现在我们可以看到,通过数组传参后,数组将首元素给传送了过去,但是,只有一个元素我们也无法比较呀。所以,此时我们还要将元素的总个数也同时传送给函数:
函数中有了元素个数之后,我们就可以通过元素地址来访问数组中的每一个元素了,接下来就要开始进行排序了:
//冒泡排序
//排序的功能不需要返回值
//因为数组传参传来的是首元素地址,这里我可以通过指针接收,也可以通过数组来接收,我选择用数组接收
void sort(int x[],int y)
{
for (int i = 0; i < y; i++)
{
int j = 0;
for (j = i+1; j < y; j++)
{
if (x[i] > x[j])
{
int k = x[i];
x[i] = x[j];
x[j] = k;
}
}
}
}
int main()
{
//定义需要进行冒泡排序的数组
int a[] = { 3,4,6,5,1,7,2,9,8 };
//求出数组的大小
int sz = sizeof(a) / sizeof(a[0]);
//通过数组传参将元素一个一个传送给函数
sort(a, sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
return 0;
}
排序的实现我们根据第一次在主函数编写的排序的实现过程来进行编写就行,最终就可以完成冒泡排序的功能:
现在咱们的冒泡排序就完成了,但是这个代码还是不够完美,我们可以给它优化一下;
确定好了优化方向,我们来看一下优化后的排序流程
现在我们可以看到,通过这种方式,我们对于这个数组只需要4次循环,在第5次循环时,就以及完成了全部排序,这时即可跳出函数,大大提高了效率,下面我们就顺着这个思路去编写代码:
//冒泡排序
//排序的功能不需要返回值
//因为数组传参传来的是首元素地址,这里我可以通过指针接收,也可以通过数组来接收,我选择用数组接收
int sort(int x[],int y)
{
int c = 0;//循环次数记录
//需要排序的循环次数
for (int i = 0; i < y - 1; i++)
{
int flag = 1;//判断是否需要排序的标志,此时默认数组不需要排序;
int j = 0;
//每一次循环需要进行比较的次数
for (j = 0; j < y - 1 - i; j++)
{
if (x[j] > x[j+1])
{
int k = x[j];
x[j] = x[j+1];
x[j+1] = k;
//当在一次循环中出现了两数交换,说明数组不是升序排列;
flag = 0;
}
}
if (1 == flag)
{
//当走完一次循环,未出现两数交换,说明数组已经是升序排列;
break;
}
c++;
}
return c;
}
int main()
{
//定义需要进行冒泡排序的数组
int a[] = { 3,4,6,5,1,7,2,9,8 };
//求出数组的大小
int sz = sizeof(a) / sizeof(a[0]);
//通过数组传参将元素一个一个传送给函数
int c = sort(a, sz);
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
printf("\n%d", c);
return 0;
}
下面我们来看一下,是否是只进行了5次循环:
从运行结果我们可以看到,经过优化后的函数,在排序上确实提高了效率。
我们最后再总结一下冒泡排序的编写思路:
if(arr[i]>arr[i+1])
,满足条件则进行换位,不满足则继续比较下一个元素;i;
j;
到这里咱们本章的内容就全部结束了,现在我们来回顾一下整个篇章的内容;
在本章内容中,我们首先介绍了——一维数组的创建、初始化、使用和在内存中的存储。通过这个内容我们了解了:
随后我们就介绍了——二维数组的创建、初始化、使用和在内存中的存储。通过这个内容我们了解了:
之后我们又提了一下什么是——数组越界,并详细介绍了——数组作为函数参数的相关知识点。在这里我们知道了:
&arr_name
此时的数组名代表的是整个数组的地址;sizeof(arr_name)
这里的数组名代表的是整个数组;最后我们通过——冒泡排序来进一步巩固了函数与数组的相关知识点。
这一篇内容是对C语言数组内容的一个总集篇,里面涉及的内容也是比较全面的,希望对各位刚刚学习C语言的朋友,和想要回顾C语言相关知识点的朋友提供一点帮助。各位如果在学习的过程中遇到了什么问题,都可以在评论区留言或者私信我,我在看到消息后也会第一时间回复的。
接下来我也会陆续的将操作符、指针、结构体等内容编写好后发出来,各位朋友记得关注哦!!!
最后,感谢各位的翻阅,咱们下一篇再见!!!