博客大纲
一维数组
数组的定义:
数组是存放同一类型数据的集合
可以看出数组有两个基本要求:
1.存放的数据类型相同
2.有一个及以上的元素个数
数组是一种c语言中的自定义类型,也是大部分c语言学习者最早接触到的自定义类型
接下来我们来讲解最基本的一维数组:
创建:
一维数组的创建语法如下:
type arr_name[数字常量]
type:
我们刚刚提到,数组是存放同一类型的数据的,此处的type就是来规定此数组存放哪一类数据。它可以是char,short,int等等
arr_name:
即数组的名字,数组也是一种变量,是变量就有相应的变量名,便于后续访问。此处变量名要放在type与[]之间
[常量值]:
这个方括号括起的数字,表示这个数组存放的元素的个数
比如我们想创建一个score数组,来存放5个人的得分
int score[5]
这个代码的意思就是:创建一个名为score的变量,用于存放5个int类型数据
初始化
在我们创建数组的时候,若只是和刚刚一样,输入int score[5];那我们只是在内存开辟了5个int带大小的空间而已,数组内部没有存入想要的值,这就涉及到了初始化的问题,即在创建数组时为其赋值
完全初始化:
int score[5]={1,2,3,4,5};
以上语法,就完成了对数组内部五个数据的初始化,从第一个数据到第五个数据,依次被赋值为1.2.3.4.5,这个过程将五个元素全部初始化,称为完全初始化。
不完全初始化:
int score[5]={1,2,3};
上述代码中,我们score中有五个元素,初始化时只赋值了三个元素。相当于我们只对前三个元素赋了值,从第四个元素开始就没有初始值了,此时会被默认初始化为0。这种不对所有元素初始化的过程就称为不完全初始化。
数组类型
我们先前讲过,数组是大部分c语言学习者最早接触到的自定义类型,也就是说,数组是一种类型。
数组类型由两部分组成,即元素类型+数组长度
比如int score[5]的类型就是int [5]
char arr[10]的类型就是char [10]
一维数组的使用
既然我们存了数据在数组中,我们也应该在需要数据时提取出数据,为此C语言为数组中的元素进行了编号,第一个元素编号为0,第二个元素编号为1,以此类推,这样的编号就叫做下标。
当我们得到一个元素的下标以及此元素所在数组,就可以访问此元素了
比如我们想访问score数组中的第三个元素(下标为2),并存在变量a中。
int a = score[2];
一维数组的本质
以上讲述的都是数组的基本语法,接下来我们从内存出发,理解数组的本质。
当我们使用int score[5]={1,2,3,4,5};,就是创建了一个名为score的变量,变量类型为int [5]。
变量类型会决定这个变量在内存中开辟多少空间,此处变量类型为int[5],顾名思义,就是在内存中开辟5个int类型大小的空间。然后依次在五个空间内存入1.2.3.4.5五个数据。
接下来我们依次打印五个数组元素的地址:
可以发现,每个元素相比于上一个元素地址大四个字节,可是这种变量是在栈区开辟的空间,栈区内存是从高低使用的,难道不应该每个元素的地址相比于上一个元素小四个字节吗?
接下来我带大家进行分析:
我们在创建一维数组时,就已经确定了这个变量需要开辟的总空间的大小,整个数组作为一个变量相较于其它数据而言是在低地址的。而在数组内部,c语言规定下标从小到大,数组元素地址也是从小到大的,为什么要这样设计呢?
因为数组的访问本质上是指针对地址的访问,接下来我们用几个例子来证明:
此案例中,三者地址相同,说明&arr,与arr本身都是首个元素的地址。
此例子说明,arr[4]本质上就是对从arr地址开始,偏移量为4的地址进行访问。
而当偏移量为正数,偏移后地址是变大的,也就是说在内存中要保证后面的元素地址比前面的元素大,才能用指针偏移量来访问,否则用负数的指针偏移量访问不是很别扭吗?
在此还能解释一个问题,为什么数组下标要从0开始?
当我们访问第一个元素,arr[0]本质上来说就是*(arr+0),也就是访问arr地址本身,即第一个元素的地址。如果下标从1开始,那在那么在利用指针访问时就要对所有数据-1,反而麻烦,于是一开始就规定下标从0开始,后续直接利用下标作为偏移量就行了。
二维数组
若把一维数组比作一根数轴,那么二维数组就可以视为一个平面直角坐标系,二维数组有“行”与“列”的概念。
二维数组的创建
type arr_name[常量值1][常量值2];
与一维数组相比,二维数组的常量多了一组,即用来定义二维数组的行数。常量值1用于定义行数,常量值2用于定义列数。
如int arr[3][5]就是创建一个名为arr的数组,用于存放int类型数据,此数组有三行五列。由于此语句没有对数据初始化,默认初始化为0,如下图所示:
二维数组的初始化
完全初始化
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 1 3,4,5,6,7};
与一维数组类似,将二维数组的每一个元素一一赋值,就是完全初始化。
按照行初始化
int arr[3][5] = {{1,2,3,4,5} ,{2,3,4,5,6},{1 3,4,5,6,7}};
此初始化,将每一行的元素用大括号括起来,称为按照行初始化,此初始化可以让代码的每一行看起来更分明,提高代码的可读性。
不完全初始化
int arr[3][5] = {{1,2,3} ,{5,6},{1}};
不完全初始化基于按照行初始化,指定每一行的前几个元素,赋予特定的值,未被初始化的元素默认为0。比如上述代码创造的数组如下:
二维数组的下标
一维数组的下标是从0开始一次递增对于二维数组,行与列各有一组下标,分别从0开始。在平面直角坐标系中,只要确定了x和y就能确定一个点,相同的,二维数组中,只要确定了行和列的下标,就可以确定一个元素。
那么如何通过两个下标访问一个二维数组的元素呢?在此我们需要用到两个下标访问操作符[]
如在arr数组中找到2行3列的元素:
arr[1][2]
二维数组的本质
二维数组的结构
不妨回忆一下,一维数组的本质是通过首元素地址与偏移量来访问一个地址。二维数组也是通过首元素的地址与偏移量来访问的,但是一个数组元素只有一个地址,相比于首元素的偏移量也是确定的,为何需要两个偏移量?
我们尝试输出一个二维数组的所有元素地址:
可以发现,每两个元素之间的差值都是4字节,也就是一个int的内存大小。说明二维数组并非我们想象中那样是一个平面,而是一个连续的地址:
其实二维数组分为两层数组,外层数组用于存放一维数组;内层数组用于存放元素。
也许有点难以理解这句话,那我们拿例子来分析:
在此处,我们有三个基本的数组,每个数组存放5个元素。而在下方有一个存放了三个数组的数组。可以发现,通过利用外层数组名arr来访问,可以正常得到每个元素。那么这样的数组形式是如何访问到每个元素的呢?
首先和大家理清几个概念:
外层数组也是数组,但是外层数组的元素是数组
外层数组的数组名arr本质上也是一个指针,与一维数组相同
对(arr+i)指针解引用,得到的是外层数组的第i-1个元素,但是此元素是一个数组,数组名本质上是指针,所以*(arr + i)得到的是一个指针
如果你理解了以上三点,说明你对数组和指针掌握的还不错。我们是用arr[][]来访问二维数组的,我们在讲解一维数组本质的时候提到过:
arr[i] == *(arr+i)
那么在此连续使用两个[],其实就是解引用了两次。
arr[i][j] == *(*(arr+i)+j)
那么为什么一维数组解引用一次就得到了目标值,二维数组要解引用两次?我们在理清概念时提起过,外层数组的变量是指针,*(arr + i)得到的是一个指针。对于这样一个指针,我们任然可以使用偏移量j与解引用操作符去访问,*(p
+ j),将此处的p指针替换为*(arr + i), 那么我们最后的表达式就是刚才的表达式了。
再用代码证明一次:
可以看到,我们确实通过这样的一个表达式访问到了这个二维数组的元素。
进一步对ij两个偏移量解析:
我们知道,指针是有步长的,int类型的指针的步长是4字节,char类型指针步长为1字节。此数组指针的步长是多少呢?答案是不确定的,这个数组占用的空间是多大,这个类型的数组指针步长就是多大。
在一维数组中,数组名的本质就是首个元素的指针,此指针的步长是一个元素占用的内存;
在二维数组中,数组名的本质也是首个元素的指针,但是此处首元素是一个数组,故此指针的步长是此数组的所有元素占用的内存;
我利用以下代码证明:
在一开始创建了一个行为3,列为5的数组,可以将此二维数组拆成三个一维数组,每个数组有5个元素,5个元素的大小是20字节。
在上述指针的加减法中,arr+1偏移了20个字节的地址,arr+2偏移了40个字节的地址。可以发现,刚好分别就是1个数组的大小与两个数组的大小。这就可以说明,此处的arr是一个指针且指针类型是数组指针。
arr[i][j] == *(*(arr+i)+j)
那么我们来解析以下二维数组访问表达式中i与j造成了怎样的偏移:
我们刚刚辨析过,外层数组的元素类型是数组指针,指针在偏移时偏移量是:这个指针指向的元素的大小的字节个数
对于外层数组,i面对的是一个步长为20字节(5 * int)的指针,所以i变动会跳过20*i个字节。从上图中也可以看出,i每自增1,指针就跳过了五个元素。
对于内层数组,此时一个元素就是一个int的大小,作为内层数组的偏移量,j每次自增造成的偏移量也就是1个元素了。
二维数组通过这样的对指针步长的运用,一个用于跳过数组,一个用于跳过元素,造成了一个“二维”的假象。相信看完这部分内容,你对arr[i][j]这样的操作也会有不一样的认知。
变长数组
在C99标准之前,C语言在创建数组的时候,数组大小的指定只能使用常量、常量表达式,或者如果我们初始化数据的话,可以省略数组大小。比如以下三种方式,得到的数组都是固定的大小。
int arr1[10];
int arr2[3+5];
int arr3[] = {1,2,3};
这样的语法限制,让我们创建数组就不够灵活,有时候数组大了浪费空间,有时候数组又小了不够用
的。
C99中给一个变长数组(variable-length array,简称 VLA)的新特性,允许我们可以使用变量指定数组大小。
以代码为例:
int n = a+b;
int arr[n];
上面示例中,数组arr 就是变长数组,因为它的长度取决于变量n 的值,编译器没法事先确定,只有运行时才能知道n 是多少。
变长数组的根本特征,就是数组长度只有运行时才能确定,所以变长数组不能初始化。它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度。
变长数组的意思是数组的大小是可以使用变量来指定的,在程序运行的时候,根据变量的大小来指定数组的元素个数,而不是说数组的大小是可变的。数组的大小一旦确定就不能再变化了。
柔性数组
注意:此内容需要结构体与动态内存知识
柔性数组的定义
柔性数组也是在C99标准后才出现的,它是指:对于动态内存中的结构体的最后一个成员,可以放一个长度可以改变的数组。
柔性数组的创建与使用
//方法1 struct st{ int a; int arr[];//柔性数组成员 } //方法2 struct st{ int a; int arr[0];//柔性数组成员 }
根据定义可知,柔性数组必须是结构体的最后一个成员。在上述开辟过程中,我们都在结构体末尾放了一个数组,此数组的[]内部没有值或者值为0。这就是柔性数组的基本语法,若数组不是最后一个成员,或者数组[]内有0以外的值,最后创建的都不是柔性数组。注意:部分编译器只支持其中一种写法。
柔性数组有以下特性:
1.柔性数组成员前至少有一个其它成员
2.sizeof计算结构体的大小时,不计入柔性数组成员
3.柔性数组的长度变化由malloc与realloc决定,在第一次使用malloc开辟内存时,必须大于结构体其它成员占用内总和,多出来的内存分配给柔性数组。
为了理解这些特性以及柔性数组的长度变化,我们来分析一串代码:
struct st { int a; int arr[];//柔性数组成员 }; int main() { struct st* p = (struct st*)malloc(sizeof(struct st) + 10 * sizeof(int)); //为结构体开辟空间,并为柔性数组开辟空间存放10个元素 if (p == NULL)//检查开辟空间是否成功 { perror("malloc"); return 1; } //为柔性数组的元素赋值 int i = 0; p->a = 100; for (i = 0; i < 10; i++) { p->arr[i] = i; } //感觉数组长度不够,增长数组: struct st* ptr = realloc(p, sizeof(struct st) + 15 * sizeof(int)); //增加5个元素的空间 if (ptr == NULL)//检查开辟空间是否成功 { perror("realloc"); return 1; } else { p = ptr; ptr = NULL; } //对后续开辟的5个空间赋值 for (i = 10; i < 15; i++) { p->arr[i] = i; } //释放空间 free(p); p = NULL; return 0; }
柔性数组开辟:
struct st* p = (struct st*)malloc(sizeof(struct st) + 10 * sizeof(int));
一开始我们创建了一个结构体,在利用动态内存开辟了属于结构体本身的空间后,追加了10个int类型的大小,用于存放柔性数组的元素。
此处也利用了sizeof不计算柔性数组的特性,避免程序员自己计算结构体的大小,追加的空间也更加直观。
柔性数组增长:
struct st* ptr = realloc(p, sizeof(struct st) + 15 * sizeof(int));
上述代码在开辟了10个元素的空间后,仍需要空间放其它元素,于是使用realloc开辟了额外的五个空间,这就是柔性数组的长度变化。
可以发现,柔性数组的本质就是动态内存管理,利用malloc与realloc来操作内存,变化数组长度。
相比于变长数组,通过变量赋值以后就变成了定长数组,柔性数组其实才是真正意义上的“变长”。