# 数组

网道（WangDoc.com），互联网文档计划

## 简介 #

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

    int scores[100];

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

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

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

数组名后面使用方括号指定编号，就可以引用该成员。也可以通过该方式，对该位置进行赋值。

    scores[0] = 13;
    scores[99] = 42;

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

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

    int scores[100];

    scores[100] = 51;

上面示例中，数组`scores`只有100个成员，因此`scores[100]`这个位置是不存在的。但是，引用这个位置并不会报错，会正常运行，使得紧跟在`scores`后面的那块内存区域被赋值，而那实际上是其他变量的区域，因此不知不觉就更改了其他变量的值。这很容易引发错误，而且难以发现。

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

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

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

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

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

报错的原因是，C 语言规定，数组变量一旦声明，就不得修改变量指向的地址，具体会在后文解释。由于同样的原因，数组赋值之后，再用大括号修改值，也是不允许的。

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

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

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

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

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

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

    int a[100] = {0};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

## 数组长度 #

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

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

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

由于数组成员都是同一个类型，每个成员的字节长度都是一样的，所以数组整体的字节长度除以某个数组成员的字节长度，就可以得到数组的成员数量。

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

上面示例中，`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

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

## 多维数组 #

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

    int board[10][10];

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

多维数组可以理解成，上层维度的每个成员本身就是一个数组。比如上例中，第一个维度的每个成员本身就是一个有10个成员的数组，因此整个二维数组共有100个成员（10 x 10 = 100）。

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

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

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

    board[0][0] = 13;
    board[9][9] = 13;

注意，`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}
    };

上面示例中，`a`是一个二维数组，这种赋值写法相当于将第一维的每个成员写成一个数组。这种写法不用为每个成员都赋值，缺少的成员会自动设置为`0`。

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

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

上面示例中，指定了`[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};

## 变长数组 #

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

    int n = x + y;
    int arr[n];

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

变长数组的根本特征，就是数组长度只有运行时才能确定。它的好处是程序员不必在开发时，随意为数组指定一个估计的长度，程序可以在运行时为数组分配精确的长度。

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

    int i = 10;

    int a1[i];
    int a2[i + 5];
    int a3[i + k];

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

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

    int m = 4;
    int n = 5;
    int c[m][n];

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

## 数组的地址 #

数组是一连串连续储存的同类型值，只要获得起始地址（首个成员的内存地址），就能推算出其他成员的地址。请看下面的例子。

    int a[5] = {11, 22, 33, 44, 55};
    int* p;

    p = &a[0];

    printf("%d\n", *p);  // Prints "11"

上面示例中，`&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;

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

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

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

    // 写法一
    int sum(int arr[], int len);
    // 写法二
    int sum(int* arr, int len);

上面示例中，传入一个整数数组，与传入一个整数指针是同一回事，数组符号`[]`与指针符号`*`是可以互换的。下一个例子是通过数组指针对成员求和。

    int sum(int* arr, int len) {
      int i;
      int total = 0;

      // 假定数组有 10 个成员
      for (i = 0; i < len; i++) {
        total += arr[i];
      }
      return total;
    }

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

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

    int a[4][2];

    // 取出 a[0][0] 的值
    *(a[0]);
    // 等同于
    **a

上面示例中，由于`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; // 报错

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

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

    int a[5] = {1, 2, 3, 4, 5};

    // 写法一
    int b[5] = a; // 报错

    // 写法二
    int b[5];
    b = a; // 报错

上面两种写法都会更改数组`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));
    }

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

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

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

上面代码给出了数组成员的两种访问方式，一种是使用方括号`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++;
    }

上面示例中，通过`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));

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

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

    int arr[5] = {20, 10, 5, 39, 88};
    int* p = arr;

    while (*p != 88)
      p++;

    printf("%i\n", p - arr); // 4

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

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

    int arr[4][2];

    // 指针指向 arr[1]
    arr + 1;

    // 指针指向 arr[0][1]
    arr[0] + 1

上面示例中，`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

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

## 数组的复制 #

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

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

    a = b;

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

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

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

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

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

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

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

## 作为函数的参数 #

### 声明参数数组 #

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

    int sum_array(int a[], int n) {
      // ...
    }

    int a[] = {3, 5, 7, 3};
    int sum = sum_array(a, 4);

上面示例中，函数`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);

上面示例中，函数`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);

上面示例中，数组`a[n]`是一个变长数组，它的长度取决于变量`n`的值，只有运行时才能知道。所以，变量`n`作为参数时，顺序一定要在变长数组前面，这样运行时才能确定数组`a[n]`的长度，否则就会报错。

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

    int sum_array(int, int [*]);
    int sum_array(int, int []);

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

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

    // 原来的写法
    int sum_array(int a[][4], int n);

    // 变长数组的写法
    int sum_array(int n, int m, int a[n][m]);

上面示例中，函数`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);

上面示例中，两种写法是等价的。第二种写法省掉了数组变量的声明，直接将数组字面量传入函数。`{2, 3, 4, 5}`是数组值的字面量，`(int [])`类似于强制的类型转换，告诉编译器怎么理解这组值。

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文