• C语言 深度探究C语言中的数组


    数组

    数组是一组相同类型的值,按照顺序储存在一起。数组通过变量名后加方括号表示,方括号里面是数组的成员数量

    int scores[100];
    
    • 1

    上面示例声明了一个数组scores,里面包含100个成员,每个成员都是int类型

    注意,声明数组时,必须给出数组的大小

    数组的成员从0开始编号,所以数组scores[100]就是从第0号成员一直到第99号成员,最后一个成员的编号会比数组长度小1

    数组名后面使用方括号指定编号,就可以引用该成员

    也可以通过该方式,对该位置进行赋值

    scores[0] = 13;
    scores[99] = 42;
    
    • 1
    • 2

    上面示例对数组scores的第一个位置和最后一个位置,进行了赋值。

    注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,所以必须非常小心。

    int scores[100];
    
    scores[100] = 51;
    
    • 1
    • 2
    • 3

    上面示例中,数组scores只有100个成员,因此scores[100]这个位置是不存在的

    但是,引用这个位置并不会报错,会正常运行,使得紧跟在scores后面的那块内存区域被赋值,而那实际上是其他变量的区域

    因此不知不觉就更改了其他变量的值。这很容易引发错误,而且难以发现

    数组也可以在声明时,使用大括号,同时对每一个成员赋值

    int a[5] = {22, 37, 3490, 18, 95};
    
    • 1

    注意,使用大括号赋值时,必须在数组声明时赋值,否则编译时会报错

    int a[5];
    a = {22, 37, 3490, 18, 95}; // 报错
    
    • 1
    • 2

    上面代码中,数组a声明之后再进行大括号赋值,导致报错。

    报错的原因是,C 语言规定,数组变量一旦声明,就不得修改变量指向的地址,具体会在后文解释

    由于同样的原因,数组赋值之后,再用大括号修改值,也是不允许的。

    int a[5] = {1, 2, 3, 4, 5};
    a = {22, 37, 3490, 18, 95}; // 报错
    
    • 1
    • 2

    上面代码中,数组a赋值后,再用大括号重新赋值也是不允许的

    使用大括号赋值时,大括号里面的值不能多于数组的长度,否则编译时会报错

    如果大括号里面的值,少于数组的成员数量,那么未赋值的成员自动初始化为0

    int a[5] = {22, 37, 3490};
    // 等同于
    int a[5] = {22, 37, 3490, 0, 0};
    
    • 1
    • 2
    • 3

    如果要将整个数组的每一个成员都设置为零,最简单的写法就是下面这样。

    int a[100] = {0};
    
    • 1

    数组初始化时,可以指定为哪些位置的成员赋值。

    int a[15] = {[2] = 29, [9] = 7, [14] = 48};
    
    • 1

    上面示例中,数组的2号、9号、14号位置被赋值,其他位置的值都自动设为0。

    指定位置的赋值可以不按照顺序,下面的写法与上面的例子是等价的。

    int a[15] = {[9] = 7, [14] = 48, [2] = 29};
    
    • 1

    指定位置的赋值与顺序赋值,可以结合使用。

    int a[15] = {1, [5] = 10, 11, [10] = 20, 21}
    
    • 1

    上面示例中,0号、5号、6号、10号、11号被赋值。

    C 语言允许省略方括号里面的数组成员数量,这时将根据大括号里面的值的数量,自动确定数组的长度。

    int a[] = {22, 37, 3490};
    // 等同于
    int a[3] = {22, 37, 3490};
    
    • 1
    • 2
    • 3

    上面示例中,数组a的长度,将根据大括号里面的值的数量,确定为3

    省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。

    int a[] = {[2] = 6, [9] = 12};
    
    • 1

    上面示例中,数组a的最大指定位置是9,所以数组的长度是10。


    数组长度

    sizeof运算符会返回整个数组的字节长度

    int a[] = {22, 37, 3490};
    int arrLen = sizeof(a); // 12
    
    • 1
    • 2

    上面示例中,sizeof返回数组a的字节长度是12

    由于数组成员都是同一个类型,每个成员的字节长度都是一样的

    所以数组整体的字节长度除以某个数组成员的字节长度,就可以得到数组的成员数量

    sizeof(a) / sizeof(a[0])
    
    • 1

    上面示例中,sizeof(a)是整个数组的字节长度,sizeof(a[0])是数组成员的字节长度,相除就是数组的成员数量

    注意,sizeof返回值的数据类型是size_t,所以sizeof(a) / sizeof(a[0])的数据类型也是size_t。在printf()里面的占位符,要用%zd%zu

    int x[12];
    
    printf("%zu\n", sizeof(x));     // 48
    printf("%zu\n", sizeof(int));  // 4
    printf("%zu\n", sizeof(x) / sizeof(int)); // 12
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,sizeof(x) / sizeof(int)就可以得到数组成员数量12


    多维数组

    C 语言允许声明多个维度的数组,有多少个维度,就用多少个方括号,比如二维数组就使用两个方括号

    int board[10][10];
    
    • 1

    上面示例声明了一个二维数组,第一个维度有10个成员,第二个维度也有10个成员

    多维数组可以理解成,上层维度的每个成员本身就是一个数组

    比如上例中,第一个维度的每个成员本身就是一个有10个成员的数组,因此整个二维数组共有100个成员(10 x 10 = 100)

    三维数组就使用三个方括号声明,以此类推

    int c[4][5][6];
    
    • 1

    引用二维数组的每个成员时,需要使用两个方括号,同时指定两个维度

    board[0][0] = 13;
    board[9][9] = 13;
    
    • 1
    • 2

    注意,board[0][0]不能写成board[0, 0],因为0, 0是一个逗号表达式,返回第二个值,所以board[0, 0]等同于board[0]

    跟一维数组一样,多维数组每个维度的第一个成员也是从0开始编号

    多维数组也可以使用大括号,一次性对所有成员赋值

    int a[2][5] = {
      {0, 1, 2, 3, 4},
      {5, 6, 7, 8, 9}
    };
    
    • 1
    • 2
    • 3
    • 4

    上面示例中,a是一个二维数组,这种赋值写法相当于将第一维的每个成员写成一个数组

    这种写法不用为每个成员都赋值,缺少的成员会自动设置为0

    多维数组也可以指定位置,进行初始化赋值

    int a[2][2] = {[0][0] = 1, [1][1] = 2};
    
    • 1

    上面示例中,指定了[0][0][1][1]位置的值,其他位置就自动设为0

    不管数组有多少维度,在内存里面都是线性存储,a[0][0]的后面是a[0][1]a[0][1]的后面是a[1][0],以此类推

    因此,多维数组也可以使用单层大括号赋值,下面的语句与上面的赋值语句是完全等同的

    int a[2][2] = {1, 0, 0, 2};
    
    • 1

    变长数组

    数组声明的时候,数组长度除了使用常量,也可以使用变量。这叫做变长数组(variable-length array,简称 VLA)

    int n = x + y;
    int arr[n];
    
    • 1
    • 2

    上面示例中,数组arr就是变长数组,因为它的长度取决于变量n的值,编译器没法事先确定,只有运行时才能知道n是多少

    变长数组的根本特征,就是数组长度只有运行时才能确定

    它的好处是程序员不必在开发时,随意为数组指定一个估计的长度,程序可以在运行时为数组分配精确的长度

    任何长度需要运行时才能确定的数组,都是变长数组

    int i = 10;
    
    int a1[i];
    int a2[i + 5];
    int a3[i + k];
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,三个数组的长度都需要运行代码才能知道,编译器并不知道它们的长度,所以它们都是变长数组

    变长数组也可以用于多维数组

    int m = 4;
    int n = 5;
    int c[m][n];
    
    • 1
    • 2
    • 3

    上面示例中,c[m][n]就是二维变长数组


    数组的地址

    数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址

    int a[5] = {11, 22, 33, 44, 55};
    int* p;
    
    p = &a[0];
    
    printf("%d\n", *p);  // Prints "11"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面示例中,&a[0]就是数组a的首个成员11的内存地址,也是整个数组的起始地址

    反过来,从这个地址(*p),可以获得首个成员的值11

    由于数组的起始地址是常用操作,&array[0]的写法有点麻烦,C 语言提供了便利写法,数组名等同于起始地址

    也就是说,数组名就是指向第一个成员(array[0])的指针

    int a[5] = {11, 22, 33, 44, 55};
    
    int* p = &a[0];
    // 等同于
    int* p = a;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,&a[0]和数组名a是等价的

    这样的话,如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组

    函数接受数组作为参数,函数原型可以写成下面这样

    // 写法一
    int sum(int arr[], int len);
    // 写法二
    int sum(int* arr, int len);
    
    • 1
    • 2
    • 3
    • 4

    上面示例中,传入一个整数数组,与传入一个整数指针是同一回事,数组符号[]与指针符号*是可以互换的

    下一个例子是通过数组指针对成员求和

    int sum(int* arr, int len) {
      int i;
      int total = 0;
    
      // 假定数组有 10 个成员
      for (i = 0; i < len; i++) {
        total += arr[i];
      }
      return total;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上面示例中,传入函数的是一个指针arr(也是数组名)和数组长度,通过指针获取数组的每个成员,从而求和

    *&运算符也可以用于多维数组

    int a[4][2];
    
    // 取出 a[0][0] 的值
    *(a[0]);
    // 等同于
    **a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面示例中,由于a[0]本身是一个指针,指向第二维数组的第一个成员a[0][0]

    所以,*(a[0])取出的是a[0][0]的值。至于**a,就是对a进行两次*运算,第一次取出的是a[0],第二次取出的是a[0][0]

    同理,二维数组的&a[0][0]等同于*a

    注意,数组名指向的地址是不能更改的

    声明数组时,编译器自动为数组分配了内存地址,这个地址与数组名是绑定的,不可更改,下面的代码会报错

    int ints[100];
    ints = NULL; // 报错
    
    • 1
    • 2

    上面示例中,重新为数组名赋值,改变原来的内存地址,就会报错

    这也导致不能将一个数组名赋值给另外一个数组名

    int a[5] = {1, 2, 3, 4, 5};
    
    // 写法一
    int b[5] = a; // 报错
    
    // 写法二
    int b[5];
    b = a; // 报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面两种写法都会更改数组b的地址,导致报错


    数组指针的加减法

    C 语言里面,数组名可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址。比如,a + 1返回下一个成员的地址,a - 1返回上一个成员的地址。

    int a[5] = {11, 22, 33, 44, 55};
    
    for (int i = 0; i < 5; i++) {
      printf("%d\n", *(a + i));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,通过指针的移动遍历数组,a + i的每轮循环每次都会指向下一个成员的地址,*(a + i)取出该地址的值,等同于a[i]

    对于数组的第一个成员,*(a + 0)(即*a)等同于a[0]

    由于数组名与指针是等价的,所以下面的等式总是成立

    a[b] == *(a + b)
    
    • 1

    上面代码给出了数组成员的两种访问方式,一种是使用方括号a[b],另一种是使用指针*(a + b)

    如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组

    int a[] = {11, 22, 33, 44, 55, 999};
    
    int* p = a;
    
    while (*p != 999) {
      printf("%d\n", *p);
      p++;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面示例中,通过p++让变量p指向下一个成员

    注意,数组名指向的地址是不能变的

    所以上例中,不能直接对a进行自增,即a++的写法是错的,必须将a的地址赋值给指针变量p,然后对p进行自增

    遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现

    int sum(int* start, int* end) {
      int total = 0;
    
      while (start < end) {
        total += *start;
        start++;
      }
    
      return total;
    }
    
    int arr[5] = {20, 10, 5, 39, 4};
    printf("%i\n", sum(arr, arr + 5));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面示例中,arr是数组的起始地址,arr + 5是结束地址。只要起始地址小于结束地址,就表示还没有到达数组尾部

    反过来,通过数组的减法,可以知道两个地址之间有多少个数组成员,请看下面的例子,自己实现一个计算数组长度的函数

    int arr[5] = {20, 10, 5, 39, 88};
    int* p = arr;
    
    while (*p != 88)
      p++;
    
    printf("%i\n", p - arr); // 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面示例中,将某个数组成员的地址,减去数组起始地址,就可以知道,当前成员与起始地址之间有多少个成员

    对于多维数组,数组指针的加减法对于不同维度,含义是不一样的

    int arr[4][2];
    
    // 指针指向 arr[1]
    arr + 1;
    
    // 指针指向 arr[0][1]
    arr[0] + 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面示例中,arr是一个二维数组,arr + 1是将指针移动到第一维数组的下一个成员,即arr[1]

    由于每个第一维的成员,本身都包含另一个数组,即arr[0]是一个指向第二维数组的指针

    所以arr[0] + 1的含义是将指针移动到第二维数组的下一个成员,即arr[0][1]

    同一个数组的两个成员的指针相减时,返回它们之间的距离

    int* p = &a[5];
    int* q = &a[1];
    
    printf("%d\n", p - q); // 4
    printf("%d\n", q - p); // -4
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,变量pq分别是数组5号位置和1号位置的指针,它们相减等于4或-4


    数组的复制

    由于数组名是指针,所以复制数组不能简单地复制数组名

    int* a;
    int b[3] = {1, 2, 3};
    
    a = b;
    
    • 1
    • 2
    • 3
    • 4

    上面的写法,结果不是将数组b复制给数组a,而是让ab指向同一个数组

    复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制

    for (i = 0; i < N; i++)
      a[i] = b[i];
    
    • 1
    • 2

    上面示例中,通过将数组b的成员逐个复制给数组a,从而实现数组的赋值

    另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份

    memcpy(a, b, sizeof(b));
    
    • 1

    上面示例中,将数组b所在的那段内存,复制给数组a。这种方法要比循环复制数组成员要快


    作为函数的参数

    声明参数数组

    数组作为函数的参数,一般会同时传入数组名和数组长度。

    int sum_array(int a[], int n) {
      // ...
    }
    
    int a[] = {3, 5, 7, 3};
    int sum = sum_array(a, 4);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面示例中,函数sum_array()的第一个参数是数组本身,也就是数组名,第二个参数是数组长度

    由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入

    如果函数的参数是多维数组,那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义

    int sum_array(int a[][4], int n) {
      // ...
    }
    
    int a[2][4] = {
      {1, 2, 3, 4},
      {8, 9, 10, 11}
    };
    int sum = sum_array(a, 2);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面示例中,函数sum_array()的参数是一个二维数组

    第一个参数是数组本身(a[][4]),这时可以不写第一维的长度,因为它作为第二个参数,会传入函数,但是一定要写第二维的长度4

    这是因为函数内部拿到的,只是数组的起始地址a,以及第一维的成员数量2

    如果要正确计算数组的结束地址,还必须知道第一维每个成员的字节长度

    写成int a[][4],编译器就知道了,第一维每个成员本身也是一个数组,里面包含了4个整数,所以每个成员的字节长度就是4 * sizeof(int)


    变长数组作为参数

    变长数组作为函数参数时,写法略有不同

    int sum_array(int n, int a[n]) {
      // ...
    }
    
    int a[] = {3, 5, 7, 3};
    int sum = sum_array(4, a);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面示例中,数组a[n]是一个变长数组,它的长度取决于变量n的值,只有运行时才能知道

    所以,变量n作为参数时,顺序一定要在变长数组前面,这样运行时才能确定数组a[n]的长度,否则就会报错

    因为函数原型可以省略参数名,所以变长数组的原型中,可以使用*代替变量名,也可以省略变量名

    int sum_array(int, int [*]);
    int sum_array(int, int []);
    
    • 1
    • 2

    上面两种变长函数的原型写法,都是合法的

    变长数组作为函数参数有一个好处,就是多维数组的参数声明,可以把后面的维度省掉了

    // 原来的写法
    int sum_array(int a[][4], int n);
    
    // 变长数组的写法
    int sum_array(int n, int m, int a[n][m]);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上面示例中,函数sum_array()的参数是一个多维数组,按照原来的写法,一定要声明第二维的长度

    但是使用变长数组的写法,就不用声明第二维长度了,因为它可以作为参数传入函数


    数组字面量作为参数

    C 语言允许将数组字面量作为参数,传入函数

    // 数组变量作为参数
    int a[] = {2, 3, 4, 5};
    int sum = sum_array(a, 4);
    
    // 数组字面量作为参数
    int sum = sum_array((int []){2, 3, 4, 5}, 4);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上面示例中,两种写法是等价的

    第二种写法省掉了数组变量的声明,直接将数组字面量传入函数

    {2, 3, 4, 5}是数组值的字面量,(int [])类似于强制的类型转换,告诉编译器怎么理解这组值


  • 相关阅读:
    从零开始搞基建(5)——代码质量
    前端项目中使用插件prettier/jscodeshift/json-stringify-pretty-compact格式化代码或json数据
    python中常用的属性访问机制:__getattribute__、__setattr__、__delattr__和__getattr__
    【ARM Cortex-M 系列 4 番外篇 -- 常用 benchmark 介绍】
    Git多人协作场景的使用
    单元测试框架-pytest
    简单工厂模式概述和使用
    fastAdmin表格列表的功能
    部署了HTTPS以后重新验证证书如何取消301跳转
    KYOCERA Programming Contest 2022(AtCoder Beginner Contest 271)
  • 原文地址:https://blog.csdn.net/qq_38973721/article/details/128089601