C语言:数组详解

简介: C语言:数组详解

博客大纲

一维数组

数组的定义:

数组是存放同一类型数据的集合

可以看出数组有两个基本要求:

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来操作内存,变化数组长度。

相比于变长数组,通过变量赋值以后就变成了定长数组,柔性数组其实才是真正意义上的“变长”。

相关文章
|
20天前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
43 4
|
2月前
|
存储 编译器 C语言
【c语言】数组
本文介绍了数组的基本概念及一维和二维数组的创建、初始化、使用方法及其在内存中的存储形式。一维数组通过下标访问元素,支持初始化和动态输入输出。二维数组则通过行和列的下标访问元素,同样支持初始化和动态输入输出。此外,还简要介绍了C99标准中的变长数组,允许在运行时根据变量创建数组,但不能初始化。
42 6
|
2月前
|
存储 人工智能 BI
C语言:数组的分类
C语言中的数组分为一维数组、多维数组和字符串数组。一维数组是最基本的形式,用于存储一系列相同类型的元素;多维数组则可以看作是一维数组的数组,常用于矩阵运算等场景;字符串数组则是以字符为元素的一维数组,专门用于处理文本数据。
|
2月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
2月前
|
存储 C语言
C语言:一维数组的不初始化、部分初始化、完全初始化的不同点
C语言中一维数组的初始化有三种情况:不初始化时,数组元素的值是随机的;部分初始化时,未指定的元素会被自动赋值为0;完全初始化时,所有元素都被赋予了初始值。
|
2月前
|
存储 数据管理 编译器
揭秘C语言:高效数据管理之数组
揭秘C语言:高效数据管理之数组
|
2月前
|
C语言 C++
保姆式教学C语言——数组
保姆式教学C语言——数组
19 0
保姆式教学C语言——数组
|
2月前
|
C语言
数组栈的实现(C语言描述)
本文介绍了如何在C语言中使用数组来实现栈的数据结构,包括栈的创建、入栈、出栈、获取栈顶元素、检查栈是否为空、获取栈的大小以及销毁栈等操作,并提供了相应的函数实现。
37 1
|
2月前
|
C语言
顺序表数组法构建(C语言描述)
如何使用C语言通过数组方法构建有序顺序表,包括顺序表的创建、插入、删除和打印等。
21 2
|
2月前
|
存储 编译器 C语言
【C语言】数组(一维、二维数组的简单介绍)
【C语言】数组(一维、二维数组的简单介绍)