在 Unit 1 中简单介绍过字符数组和字符串的概念,事实上,在 C 语言中不只能创建 char 类型数组,还可以创建其他类型的数组。

1. 什么是数组?

数组由数据类型相同的一系列元素组成。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型,编译器根据这些信息正确地创建数组。普通变量可以使用的类型,数组元素都可以用。
数组在内存中是连续的存储单元,可以把数组看作是一行连续的多个存储单元。

我们在学习普通变量的时候学习了普通变量的定义、初始化、使用,而数组的知识同样需要学习如何定义一个数组,如何初始化一个数组,以及数组的使用。

2. 数组的定义和初始化

2.1. 数组的定义

在使用一个变量之前需要先定义该变量,同样的,在使用数组之前需要先定义数组。编译器会根据定义的数组在内存中找到一块连续的存储单元来存储数组的数据。
数组定义的格式:

  1. type arrayName [arraySize];

这叫做一维数组,事实上我们平常说的数组都是指一维数组。

需要注意的是:

  1. type 可以是任意有效的 C 数据类型。
  2. arrayName 被称为数组名,其命名规则满足 C 语言的命名标准,它代表着数组的起始地址。
  3. arraySize 必须是一个大于零的整数常量。在 C99 标准之前,arraySize 不允许是变量,C99 标准之后引入了变长数组的概念。

定义数组的演示:

  1. float candy[365]; /* 内含365个float类型元素的数组 */
  2. char code[12]; /* 内含12个char类型元素的数组*/
  3. int states[50]; /* 内含50个int类型元素的数组*/

要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。数组元素的编号从 0 开始,所以 candy[0] 表示 candy 数组的第 1 个元素,candy[364] 表示第 365 个元素,也就是最后一个元素。

2.2. 数组的初始化

数组通常被用来储存程序需要的数据。例如,一个内含 12 个整数元素的数组可以储存 12 个月的天数。在这种情况下,在程序一开始就初始化数组比较好。下面介绍初始化数组的方法。

在学习数组之前,我们接触到的变量都是只能存储单个值的,称为标量变量。我们已经很熟悉如何初始化这类变量:

  1. int a = 10;
  2. double b = 1.9;

下面来了解下数组的初始化,和标量变量的初始化一样,数组初始化指在数组定义时给数组元素赋予初值

2.2.1 通用的初始化

C 语言使用新的语法来初始化数组。

  1. type arrayName [arraySize] = {value1, value2, ..., valueN};

例如:

  1. int powers[8] = {1, 2, 4, 6, 8, 16, 32, 64};

如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格。根据上面的初始化,把 1 赋给数组的首元素(powers[0]),以此类推。

数组初始化的限制:

  1. 只有在定义数组时才可以初始化,此后就不能再用了
  2. 不能直接将一个数组赋值给另一个数组

初始化注意事项:

  1. 使用数组之前必须先初始化数组或者为每个元素赋值,否则编译器使用的值是内存相应位置上的现有值。(PS:事实上,这一条只针对自动存储类别的数组,对于静态存储期的数组,即使没有初始化,编译器会默认将所有元素初始化为 0。这一部分是存储类别的知识,目前不需要记住。)
  2. 初始化列表的项数和数组的大小不一致。
    1. 初始化列表的项数大于数组的大小,编译器报错。
    2. 初始化列表的项数小于数组的大小,编译器将剩余的元素初始化为 0。也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;但是,如果部分初始化数组,剩余的元素就会被初始化为0。
  3. 根据第2点,可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数。但必须保证初始化列表中的项数是正确的,否则即使少了一项或者多了一项,编译器都不会察觉。

    1. int a [10]= {}; // 将 a 数组的十个元素都初始化为 0.
  4. 如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小

    1. int a[] = { 1, 2, 3, 4, 5, 6};// 编译器初始化一个含有6个元素的数组
  5. 使用 const 声明数组。有时需要把数组设置为只读,这样程序只能从数组中检索值,不能把新值写入数组。要创建只读数组,应该用 const 声明和初始化数组。例如:

    1. const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

    这样程序在运行过程中就不能修改该数组中的内容。和普通变量一样,应该使用声明来初始化 const 数据,因为一旦声明为 const,便不能再给它赋值。

    2.2.2 字符串特有的初始化

    还有一种初始化数组的方法,这种方法仅限于初始化字符数组,更确切地说是仅限于字符串初始化 —— 使用字符串常量初始化。

    1. char str1[10] = "hello"; // 数组长度是 10
    2. char str2[10] = {"world"};// 数组长度是 10
    3. char str3[] = "hello"; // 数组长度是 6
    4. char str4[] = {"world"}; // 数组长度是 6

    2.2.3 指定初始化器(C99)

    C99 增加了一个新特性:指定初始化器(designated initializer)。利用该特性可以初始化指定的数组元素。例如,只初始化数组中的最后一个元素。

对于传统的 C 初始化语法,必须初始化最后一个元素之前的所有元素,才能初始化它:

  1. int arr[6] = {0,0,0,0,0,212}; // 传统的语法

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

  1. int arr[6] = {[5] = 212}; // 把arr[5]初始化为212

下面我们来演示一下指定初始化器的用法

  1. int days[12] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };
  2. // days 数组:days[] = { 31, 29, 0, 0, 31, 30, 31, 0, 0, 0, 0, 0};

这里演示了指定初始化器的两个重要特性。

  1. 如果指定初始化器后面有更多的值,如该例中的初始化列表中的片段:[4] = 31,30,31,那么后面这些值将被用于初始化指定元素后面的元素,即,在 days[4] 被初始化为 31 后,days[5] 和 days[6] 将分别被初始化为 30 和 31。
  2. 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。如该例中,初始化列表开始时把days[1] 初始化为 28,但是 days[1] 又被后面的指定初始化 [1] = 29 初始化为 29。

如果未指定数组元素大小会怎样?

  1. int stuff[] = {1, [6] = 23}; //会发生什么?
  2. int staff[] = {1, [6] = 4, 9, 10}; //会发生什么?

编译器会把数组的大小设置为足够装得下初始化的值。所以,stuff 数组有 7 个元素,编号为 0~6;而 staff 数组的元素比 stuff 数组多两个(即有9个元素)。

3. 数组的使用

3.1 访问数组元素

访问数组元素可以使用数组表示法和指针表示法。由于此时还没有学习指针的相关知识,因此这里只介绍数组表示法。
数组表示法使用下标(或者称为索引)来访问数组元素的。使用的格式:数组名[下标]
注意:下标从 0 开始计数。这意味着对于一个数组元素个数为 n 的数组来说,其下标的取值范围为[0, n-1],其中 0 表示数组的第一个元素,1 表示数组的第二个元素,…,n-1 表示数组的最后一个元素。

int m[10] = {1, 2, 3, 4, [8]=9, 33, 2};// 1, 2, 3, 4, 0, 0, 0, 9, 33, 2.
int a = m[7]; // a的值是m数组的第8个元素,即9.
int b = m[4]; // b的值是m数组的第5个元素,即0.

3.2. 数组元素赋值

在数组定义之后就不能再初始化数组了,但我们可以在声明数组后,借助数组下标(或索引)给数组元素赋值。

#include <stdio.h> 
#define SIZE 50 
int main(void) 
{
    int counter, evens[SIZE]; 
    for (counter = 0; counter < SIZE; counter++) 
        evens[counter] = 2 * counter; 
    ... 
}

注意这段代码中使用循环给数组的元素依次赋值。只有初始化才可以对数组整体进行赋值,之后想要修改数组中元素的值只能通过赋值方式依次对需要修改的数组元素赋值。
C 不允许把数组作为一个单元赋给另一个数组除初始化以外也不允许使用花括号列表的形式赋值

int oxen[5] = {5,3,2,8}; /* 初始化没问题 */ 
int yaks[5] = oxen;      /* 不允许 */ 
yaks[5] = oxen[5];       /* 数组下标越界 */
yaks[5] = {5,3,2,8};     /* 不起作用 */

3.3. 数组的边界

在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。例如,假设有下面的声明: int doofi[20];。那么在使用该数组时,要确保程序中使用的数组下标在 0~19的范围内,因为编译器不会检查出这种错误(但是,一些编译器发出警告,然后继续编译程序)。
编译器不会检查数组下标是否使用得当。在 C 标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。

C 语言为何会允许这种麻烦事发生?
这要归功于 C 信任程序员的原则。不检查边界,C 程序可以运行更快。编译器没必要捕获所有的下标错误,因为在程序运行之前,数组的下标值可能尚未确定。因此,为安全起见,编译器必须在运行时添加额外代码检查数组的每个下标值,这会降低程序的运行速度。C 相信程序员能编写正确的代码,这样的程序运行速度更快。但并不是所有的程序员都能做到这一点,所以就出现了下标越界的问题。

3.4. 数组的大小

在 C99 标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。sizeof 表达式被视为整型常量,但是 const 值不是(这与 C++ 不同)。另外,表达式的值必须大于 0。
而 C99 标准允许使用变量作为数组的大小,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA。VLA 有一些限制,例如,声明 VLA 时不能进行初始化