[深入浅出C语言]走进数组(篇二)

202 阅读9分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

前言

        C语言中,数组很重要也比较基础,很多地方都要用到。

        本文就来分享一波作者对数组的学习心得与见解。本篇属于第一篇,主要介绍二维数组、数组传参、变长数组和复合字面量的一些内容。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

二维数组

二维数组的创建

        二维数组可以看成是一维数组的一维数组,如图:

(这里为了方便理解才画成矩阵样式,实际上是一行上连续排列的而非多行的)

image.png

        可以把诸如arr[0],arr[1],arr[2]看成一维数组名,每一行都是一个带有三个元素的一维数组,而每一行同时又作为一个整体的元素共同构成一个一维数组。

        int B[2][3]也就是创建了一个装了两个带有三个int变量的一维数组的数组,如图:

image.png

        实际上更高维度的数组可以以此类推,比如三维数组就是一个装有若干个二维数组的一维数组。如图:

image.png

二维数组的初始化


int arr[3][4] = {1,2,3,4};//未初始化的全部置为0
int arr[3][4] = {{1,2},{4,5}};//里面的一个花括号{ }里的是一个子数组的元素
int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略

        更高维的数组以此类推。

二维数组的使用

        二维数组的使用也是通过下标的方式。

        看代码:

#include <stdio.h>
int main()
{
     int arr[3][4] = {0};
     int i = 0;
     for(i=0; i<3; i++)
     {
         int j = 0;
         for(j=0; j<4; j++)
         {
             arr[i][j] = i*4+j;
         }
     }
     for(i=0; i<3; i++)
     {
         int j = 0;
         for(j=0; j<4; j++)
         {
             printf("%d ", arr[i][j]);
         }
     }
     return 0;
}

        一般来说,二维数组的第一维度看作行,第二维度看作列,整个看成一个矩阵。

        比如int arr[3][3],不过实际上并不是以矩阵样式存放到内存的,只是使用的时候模拟成矩阵。

image.png

二维数组在内存中的存储

        像一维数组一样,这里我们尝试打印二维数组的每个元素的地址看看。

#include <stdio.h>
int main()
{
     int arr[3][4];
     int i = 0;
     for(i=0; i<3; i++)
     {
         int j = 0;
         for(j=0; j<4; j++)
         {
             printf("&arr[%d][%d] = %p\n", i, j,&arr[i][j]);
         }
     }
     return 0;
}

image.png

        说明什么?说明即使是高维数组,在内存里存放时也是连续存放在一整块内存上的。

        实际上在内存空间中是这样存放的:

image.png

      你会发现arr[3][3]和arr[9]在内存的布局基本一致。

拓展——多维数组空间布局

        所有的数组都可以看成“一维数组”。

        数组的定义是:具有相同数据元素类型的集合,特征是数组中可以保存任意类型。

        那么数组中可以保存数组吗?答案是可以!

        在理解上,我们甚至可以理解所有的数组都可以当成"一维数组"!

        就二维数组来说,我们认为二维数组,可以被看做“一维数组”,只不过内部“元素”也是一维数组。

        那么内部一维数组是在内存中布局是“线性连续且递增”的,多个该一维数组构成另一个“一维数组”,那么整体便也是线性连续且递增的。

        所以我们认为:二维数组可以被看作内部元素是一维数组的一维数组。

        那么,三维呢?n维呢?以此类推。

       n维数组可以被看作内部元素是n-1维数组的一维数组。

比如:  

image.png

         三维数组x可以看成是具有两个二维数组元素的“一维数组”,而二维数组c又可以看成是具有四个一维数组元素的“一维数组”。

二维数组的边界

        数组的下标是有范围限制的。

        数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。

        所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。

        C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的,所以程序员写代码时,最好自己做越界的检查。

        (自由的代价是责任)

        不确定数组元素个数时可以sizeof(arr) / sizeof(arr[0][0])

        二维数组的行和列也可能存在越界,可能是在自己的范围内越界。

        我们前面也讲了,二维数组可以看成一维数组的一维数组,那现在就看看arr[0][i]这个一维数组,只有三个元素,要是我们访问arr[0][3]会怎样?会访问到arr[1][0],相当于一维数组arr[0][i]越界了,只不过二维数组在内存中也是连续分布的,所以越界范围还在二维数组之内。

image.png

数组传参与数组名

数组传参

        数组作为函数参数,本质上传递的是首元素地址,是指针,函数形参是一个对应类型的指针变量。

        正是因为数组可以由指针来表示,才有了这么一出。相比于传递一整个数组(形参就要创建相同大小的数组),传递指针显然更加高效和节省空间,所以实参为数组时,只需要写数组名即可,形参可以写成int arr[ ],[ ]里面无须数字,因为不是真的要再创建一个数组,而是接收数组首元素地址,通过指针偏移来访问数组元素,所以写成这样只是能清楚地说明实参是一个数组罢了,还可以直接把形参写成指针如int*arr,都没有问题。 

        基于上述论断,我们可以发现:

        在非main函数内使用sizeof(arr) / sizeof(arr[0])想要求取数组大小就会出现问题,为什么呢?传进函数的arr是指针(在32位平台下是4字节,在64位平台下是8字节)而非数组。

数组名

        数组名一般都被认为是数组首元素的地址,但也有两个例外。

  1. sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。

  2. &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。

        地址就是指针,存放地址(指针)的变量就叫指针变量,而指针有类型,比如&arr[0]和arr都是int型指针,对指针+-整数会使得地址(指针)偏移,注意其单位不是1字节,而是对应类型的字节数,比如&arr[0]+1实际上地址向高位移动了四个字节。         

更多请看指针章节:[深入浅出C语言]深析指针(篇二) - 掘金 (juejin.cn)

&数组名与数组名

image.png

        &arr在数值上与arr和&arr[0]并无差异,但是发生偏移后的结果截然不同,比如&arr+1是跳过一整个数组的空间,而&arr[0]+1和arr+1只跳过了一个整型的空间,其实也跟它们本身的类型有关,arr和&arr[0]是int指针类型,而&arr是int数组指针类型。

         二维数组的数组名也表示首元素地址,但是要弄清楚这里的首元素是什么。

        前面讲过,二维数组是一维数组的一维数组,它的首元素就是第一个一维数组。

image.png

        arr[0]是一个含有三个元素的一维数组,arr[1]和arr[2]也是,同时arr是以arr[0],arr[1]和arr[2]为元素的一维数组。

        所以二维数组名是包含的第一个一维数组的地址,是数组指针。

        如何求取一个二维数组有几行和几列呢?

  sizeof(arr) /sizeof(arr[0]);//行数,也就是二维数组里有几个一维数组
  sizeof(arr[0]) / sizeof(arr[0][0]); //列数,也就是每一个一维数组里有几个元素

      注意:数组名是首元素地址,是指针,但不是变量,不能够自增或自减,有点像标识符常量,不是可修改的值,当然也不可以赋值。

变长数组

        C99新增了变长数组(VLA),允许使用变量表示数组的维度。

        比如:

image.png

        但是,不能在声明中初始化它们。

        注意:变长数组不能改变大小

        变长数组中的“变”并不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度

        然而,目前完全支持这一特性的编译器不多,所以很多时候都不用变长数组。

        C99/C11标准允许在声明变长数组时使用const变量。

复合字面量

        C99新增了针对数组的复合字面量。字面量是除了符号常量外的常量。例如,5是int类型字面量,81.3是double类型字面量,"elephant"是字符串字面量等等。

        对于数组,复合字面量类似于数组初始化列表,前面是用括号括起来的类型名。

例如:

int diva[2] = {10, 20};
(int [2]){10, 20}//复合字面量

        注意,去掉声明中的数组名,留下的int [2]就是复合字面量的类型名。

        因为复合字面量是匿名的常量,所以不能先创建后使用它,必须在创建的同时使用它。

        为什么这么说?

        实际上常量保存在内存里的一个只读区域(有地址),而且字面量没有标识符,也就是没有名字,除了刚创建出来的时候可以直接使用以外,就无法再次找到并使用。

        与变量的区别其实主要是存储位置,读写权限以及有无标识符(名字)的差异。

        使用指针记录地址就是一种用法:

image.png

        复合字面量的类型名也代表首元素地址,所以可以把它赋值给指向int的指针。

        还可以把复合字面量作为实参传递给函数。

image.png

        这种用法的好处是,把信息传入函数前不必先创建数组,这也是复合字面量的典型用法。

冒泡排序

        基本思想:两两比较相邻的元素,如果逆序则交换,直到没有逆序为止。

        顺序一般分为两种,一是升序,指的是从小到大排序;二是降序,指的是从大到小排序。

        所谓逆序就是违反顺序,比如想要升序,而相邻两元素左边的大于右边的。

        两个两个比较,一个循环就是一轮,一轮过后根据升序或降序,排出一个数,如果是升序,则排在右边,如果是降序,则排在左边。

        如图:

#include<stdio.h>
void BubbleSort(int arr[], int sz)
{
    int i = 0;
    int j = 0;
    for (i = 0; i < sz - 1; i++)
    {
        //一轮,每轮排好一个数
        for (j = 0; j < sz - 1 - i; j++)//要减i,因为排好序的不用再排
        {
            if (arr[j] > arr[j + 1])//大于为升序排列,小于的话为降序排列
            {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

int main()
{
    int arr[10] = {0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    printf("请输入十个整数:\n");
    for (int i = 0; i < sz; i++)
    {
        scanf("%d", &arr[i]);
    }
    BubbleSort(arr, sz);
    printf("经过冒泡排序后的数组:\n");
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~

v2-1a2bf23551e8c0438c155f93aab4b495_720w.jpg