一文带你深入浅出C语言数组

目录

前言

1. 一维数组的创建和初始化

1.1 数组的创建

1.2 数组的初始化

举些例子

指定初始化器(C99)

1.3 一维数组的使用

1.4 一维数组的边界

1.5 指定数组的大小

1.6 一维数组在内存中的存储

1.7 数组的内存布局

铺垫——局部变量的内存布局

正题——一维数组的内存布局

2. 二维数组的创建和初始化

2.1 二维数组的创建

2.2 二维数组的初始化

2.3 二维数组的使用

2.4 二维数组在内存中的存储

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

2.6 二维数组的边界

3.数组传参与数组名

3.1 数组传参 

3.2 数组名 

&数组名与数组名

4. 变长数组 

5. 复合字面量

6. 冒泡排序

敬请期待更好的作品吧~


前言

        本文分享一波对C语言数组的学习见解,主要包括一维、二维数组的创建、初始化、存储,数组传参和出数组名,变长数组等等。由于作者水平有限,难免存在纰漏,读者各取所需即可。

一文带你深入浅出C语言数组_第1张图片 给你点赞,加油加油!

1. 一维数组的创建和初始化

1.1 数组的创建

        数组是一组相同类型元素的集合。

        数组的创建方式:

type_t   arr_name   [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小

        就比如int arr[10];告诉编译器要在内存里找一块内存来存放一组数据这组数据都是int整型。

        根据存放元素数据类型的不同,数组类型有所差异:

char arr3[10];
float arr4[1];
double arr5[20];

1.2 数组的初始化

        数组的初始化是指在创建数组的同时给数组的内容一些合理初始值。

举些例子

int arr1[10] = {1,2,3};//前三个元素的位置依次放入1,2,3三个元素,后面的全部默认放入0

int arr2[] = {1,2,3,4};//不指定数组大小,但是通过初始化给的值的数量自动确定合适大小,这里大小就是4

char arr4[3] = {'a',98, 'c'};//字符数组放入三个字符元素

char arr5[] = {'a','b','c'};//不指定数组大小,但是通过初始化给的值的数量自动确定合适大小,这里大小就是3

char arr6[] = "abcdef";//这里放入的就是字符串常量,关于字符串放入字符数组,下面进行解释

        C语言没有专门用于存储字符串的变量类型,字符串被存储在char类型数组中。

        数组末尾位置是字符'\0',这是空字符,C语言中字符串以空字符为结束的标志,它的ASCII码值是0。

        在使用scanf()读入字符串的时候,不用手动加入空字符,scanf()根据转换说明%s在读取输入时就已完成空字符的添加,同时字符串常量也不用手动添加空字符,编译器会根据双引号确定字符串并在其末尾加上空字符。

        所以一般用字符数组存放字符串时要记得最少多留出一个位置来放置'\0',也就是字符数组大小>=字符串字符数+1。

        数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。

        但是对于下面的代码要区分,内存中如何分配。

char arr1[] = "abc";
char arr2[3] = {'a','b','c'};

一文带你深入浅出C语言数组_第2张图片

指定初始化器(C99)

        C99增加了一个新特性:指定初始化器,利用该特性可以初始化指定的数组元素。例如,值初始化数组中的最后一个元素,这对于以往的C语法来说是行不通的,必须初始化最后一个元素之前的所有元素才能初始化它,比如int arr[6] = {0, 0, 0, 0, 0, 129};。

        而C99规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素。

        比如:

一文带你深入浅出C语言数组_第3张图片

        我们再来看看下面这个例子,想想看是怎么回事。

int main()
{
    int days[] = { 31, 28,[4] = 31, 30, 31,[1] = 29 };
    int i = 0;
    for (i = 0; i < 7; i++)
    {
        printf("%d ", days[i]);
    }
    return 0;
}

        首先,如果指定初始化器后面有更多的值,例如[4] = 31, 30, 31  ,那么这些值将被用于初始化指定元素后面的元素,这就是说,在days[4]被初始化为31后,days[5]和days[6]被分别初始化为30和31。

        其次,注意到我在初始化时并没有指定数组大小没?那我为什么在后面的循环又设定i<7呢?咋知道是7个元素呢?当然是算出来的啦。如果未指定数组大小的话,编译器会把数组的大小设置为足够装得下初始化的值,如本例中,初始化列表中出现所初始化的最大下标的元素也就是days[6],那编译器自动设置数组大小为7([0]~[6])。

        还有一件事,注意到days[1]在初始化列表的前面已经被初始化为28没?打印出来为什么是29呢?因为在初始化列表后面又将其初始化的值更改成了29,这也说明如果再次初始化已初始化的指定元素,那么后一次的初始化会取代之前的初始化。

1.3 一维数组的使用

        对于数组的使用我们之前在讲操作符的时候介绍了一个操作符: [] ,下标引用操作符。它其实就是数组访问的操作符。

        我们来看代码:

#include 
int main()
{
     int arr[10] = {0};//数组的不完全初始化
     //计算数组的元素个数
     int sz = sizeof(arr)/sizeof(arr[0]);
     //对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
     int i = 0;//做下标
     for(i=0; i<10; i++)
     {
         arr[i] = i;
     }
     //输出数组的内容
     for(i=0; i<10; ++i)
     {
         printf("%d ", arr[i]);
     }
     return 0;
}

总结:

1. 数组是使用下标来访问的,下标是从0开始。

2. 数组的大小可以通过计算得到。

比如:int sz = sizeof(arr)/sizeof(arr[0]);

1.4 一维数组的边界

        数组是有边界的,比如int arr[10]的边界就为0~9,使用下标超出指定范围的话就叫作数组越界。

        编译器并不会检查数组下标是否使用得当,在C标准中,使用越界下标的结果是未定义的,这意味着即使程序看上去可以运行,但是运行结果会很奇怪或者异常中止。

        我们最好记住:数组元素的编号(下标)是从0开始的,而且最好在声明数组时用符号常量来表示数组大小,既方便修改,又能确保整个程序中数组大小始终一致。

#include
#define SIZE 10
int main()
{
    int arr[SIZE];
    int i = 0;
    for(i = 0; i < SIZE; i++)
    {
        scanf("%d", &arr[i]);
    }
    return 0;
}

1.5 指定数组的大小

  看点例子:

int arr1[10];//可以吗?

int arr2[4+6];//可以吗?

int count = 10;
int arr3[count];//可以吗?

        数组的声明,在C99标准之前, [ ] 中要给一个整型常量表达式才可以,不能使用变量。sizeof表达式被视为整型常量,但是const值不是(与C++不同)。另外,表达式的值必须大于0。

        相信看完这张图你就全部清楚了:

一文带你深入浅出C语言数组_第4张图片

        在C99标准支持变长数组的概念,也就是创建了一种新型数组,称为变长数组简称VLA(而C11标准把VLA设定为可选而非语言必备特性)。

        后面会具体讲到变长数组。

1.6 一维数组在内存中的存储

        接下来我们探讨数组在内存中的存储。

        看代码:

#include 
int main()
{
     int arr[10] = {0};
     int i = 0;
     int sz = sizeof(arr)/sizeof(arr[0]);
     for(i=0; i

一文带你深入浅出C语言数组_第5张图片

        仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。

        由此可以得出结论:数组在内存中是连续存放的。

        如图(图中地址随便给的):

一文带你深入浅出C语言数组_第6张图片

1.7 数组的内存布局

铺垫——局部变量的内存布局

例1:

int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    printf("%p\n", &a);
    printf("%p\n", &b);
    printf("%p\n", &c);
    return 0;
}

一文带你深入浅出C语言数组_第7张图片

        我们发现,先定义的变量,地址是比较大的,后续依次减小

        这是为什么呢?

        a,b,c都在main函数中定义,也就是在栈上开辟的临时变量。而a先定义意味着,a先开辟空间,那么a就先入栈,所以a的地址最高,其他类似。而栈中先使用的是高地址的空间。

图解:

一文带你深入浅出C语言数组_第8张图片

正题——一维数组的内存布局

例2:

#define N 10
int main()
{
    int a[N] = { 0 };
    for (int i = 0; i < N; i++)
    {
        printf("&a[%d]: %p\n",i, &a[i]);
    }
    return 0;
}

一文带你深入浅出C语言数组_第9张图片

        我们发现,数组的地址排布是:&a[0] < &a[1] < &a[2] < ... < &a[9]。

        该数组在main函数中定义,那么也同样在栈上开辟空间。

        数组有多个元素,那么肯定是a[0]先被开辟空间啊,那么应该&a[0]地址最大呀,可是事实并非如此。

        因为数组是整体申请空间的,然后将地址最低的空间,作为a[0]元素,以此类推。

图解:

一文带你深入浅出C语言数组_第10张图片

2. 二维数组的创建和初始化

2.1 二维数组的创建

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

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

一文带你深入浅出C语言数组_第11张图片

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

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

一文带你深入浅出C语言数组_第12张图片

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

一文带你深入浅出C语言数组_第13张图片

2.2 二维数组的初始化


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

        初始化二维数组的两种方法:

一文带你深入浅出C语言数组_第14张图片

        更高维的数组以此类推。

2.3 二维数组的使用

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

        看代码:

#include 
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],不过实际上并不是以矩阵样式存放到内存的,只是使用的时候模拟成矩阵。

一文带你深入浅出C语言数组_第15张图片

2.4 二维数组在内存中的存储

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

#include 
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;
}

一文带你深入浅出C语言数组_第16张图片

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

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

一文带你深入浅出C语言数组_第17张图片

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

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

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

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

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

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

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

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

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

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

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

比如:  

一文带你深入浅出C语言数组_第18张图片

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

2.6 二维数组的边界

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

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

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

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

(自由的代价是责任)

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

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

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

一文带你深入浅出C语言数组_第19张图片

3.数组传参与数组名

3.1 数组传参 

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

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

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

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

3.2 数组名 

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

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

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

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

        更多请看指针章节:

一文带你深入浅出C指针(初阶)icon-default.png?t=M5H6http://t.csdn.cn/VWka9

&数组名与数组名

一文带你深入浅出C语言数组_第20张图片

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

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

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

一文带你深入浅出C语言数组_第21张图片

        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]); //列数,也就是每一个一维数组里有几个元素

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

4. 变长数组 

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

        比如:

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

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

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

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

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

一文带你深入浅出C语言数组_第22张图片

5. 复合字面量

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

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

例如:

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

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

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

        为什么这么说?

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

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

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

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

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

一文带你深入浅出C语言数组_第23张图片

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

6. 冒泡排序

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

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

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

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

        如图:

#include
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;
}


敬请期待更好的作品吧~

一文带你深入浅出C语言数组_第24张图片你的支持就是对我对我最大的鼓励,阁下何不成人之美,点赞收藏关注走一波~

一文带你深入浅出C语言数组_第25张图片

你可能感兴趣的:(一文深入浅出C语言,c语言)