专题七、数组

本专题介绍 C 语言中一维数组和多维数组的声明与使用,并讨论 C99 中的变长数组。

参考资料:《C 语言程序设计 · 现代方法 第 2 2 2 版》

前面专题中所见的变量都只是标量 scalar \text{scalar} scalar),标量具有保存单一数据项的能力。C 语言也支持聚合 aggregate \text{aggregate} aggregate)变量,这类变量可以存储一组一组的数值。在 C 语言中一共有两种聚合类型:数组 array \text{array} array)和结构 structure \text{structure} structure)。

1. 一维数组

数组是含有多个数据值的数据结构,并且每个数据值具有相同的数据类型。这些数据值称为元素 element \text{element} element),可以根据元素在数组中所处的位置把它们一个个地选出来。

最简单的数组类型就是一维数组,一维数组中的元素一个接一个地编排在单独一行内。这里可以假设有一个名为 a 的一维数组:
一维数组

为了声明数组,需要指明数组元素的类型和数量。例如,为了声明数组 a 10 10 10int 类型的元素,可以写成

int a[10];

数组的元素可以是任何类型,数组的长度可以用任何(整数)常量表达式(见专题四)指定。因为程序以后改变时可能需要调整数组的长度,所以较好的方法是用宏来定义数组的长度:

#define N 10
...
int a[N];

1.1 数组下标

为了存取特定的数组元素,可以在写数组名的同时在后边加上一个用方括号围绕的整数值,称这是对数组取下标 subscripting \text{subscripting} subscripting)或进行索引 indexing \text{indexing} indexing)。数组元素始终从 0 0 0 开始,所以长度为 n n n 的数组元素的索引是从 0 0 0 n − 1 n - 1 n1。例如,如果 a 是含有 10 10 10 个元素的数组,那么这些元素可以如下所示依次标记:
一维数组索引
形如 a[i] 的表达式是左值(见专题三),所以数组元素可以像普通变量一样使用。一般说来,如果数组包含 T T T 类型的元素,那么数组中的每个元素均视为 T T T 类型的变量:

a[0] = 1;
printf("%d\n", a[5]);
++a[i];

数组通常和 for 循环结合在一起使用。下面给出了在长度为 N N N 的数组 a 上的一些常见操作示例:

for (i = 0; i < N; i++)
	a[i] = 0;				/* clears a */
	
for (i = 0; i < N; i++)
	scanf("%d", &a[i]);		/* reads data into a */
	
for (i = 0; i < N; i++)
	sum += a[i];			/* sums the elements of a */

C 语言不要求检查下标的范围。当下标超出范围时,程序可能执行不可预知的行为。下标超出范围的原因之一是:忘记了 n n n 元数组的索引是从 0 0 0 n − 1 n - 1 n1,而不是从 1 1 1 n n n。考虑下面这段代码:

int a[10], i;
for (i = 1; i <= 10; i++)
	a[i] = 0;

对于某些编译器来说,这个表面上正确的 for 语句却产生了一个无限循环!当变量 i 的值变为 10 10 10 时,程序将数值 0 0 0 存储在 a[10] 中。但是 a[10] 这个元素并不存在,所以在元素 a[9] 后数值 0 0 0 立刻进入内存。如果内存中变量 i 放置在 a[9] 的后边,那么变量 i 将会被重置为 0 0 0,进而导致循环重新开始。

数组下标可以是任何整数表达式:

a[i+j*10] = 0;

表达式甚至可能会有副作用:

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

当数组下标有副作用时一定要注意。例如,下面这个循环想把数组 b 中的元素复制到数组 a 中,但它可能无法正常工作:

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

表达式 a[i] = b[i++] 会导致未定义的行为。通过从下标中移走自增操作可以很容易避免此类问题的发生:

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

程序 reverse.c:数列反向
要求用户录入一串数,然后按反向顺序输出这些数。

/* Reverses a series of numbers */

#include <stdio.h>

#define N 10

int main(void)
{
	int a[N], i;
	
	printf("Enter %d numbers: ", N);
	for (int i = 0; i < N; i++)
		scanf("%d", &a[i]);
	
	printf("In reverse order:");
	for (int i = N - 1; i >= 0; i--)
		printf(" %d", a[i]);
	printf("\n");
	
	return 0;
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter 10 numbers: 34   82   49   102   7   94   23   11   50   31 ‾ \underline{34\ 82\ 49\ 102\ 7\ 94\ 23\ 11\ 50\ 31} 34 82 49 102 7 94 23 11 50 31

In reverse order: 31 50 11 23 94 7 102 49 82 34

1.2 数组初始化

数组初始化式 array initializer \text{array initializer} array initializer)最常见的格式是用一个大括号括起来的常量表达式列表,常量表达式之间用逗号进行分隔:

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

如果初始化式比数组短,那么数组中剩余的元素赋值为 0 0 0

int a[10] = {1, 2, 3, 4, 5, 6};
/* initial value of a is {1, 2, 3, 4, 5, 6, 0, 0, 0, 0} */

利用这一特性,可以很容易地把数组初始化为全 0 0 0

int a[10] = {0};
/* initial value of a is {0, 0, 0, 0, 0, 0, 0, 0, 0, 0} */

初始化式完全为空是非法的,所以要在大括号内放上一个 0 0 0。初始化式比要初始化的数组长也是非法的。
如果给定了初始化式,可以省略掉数组的长度:

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

编译器利用初始化式的长度来确定数组的大小。数组仍然有固定数量的元素,这跟明确地指定长度效果一样。

1.3 指定初始化式(C99)

经常有这样的情况:数组中只有相对较少的元素需要进行显式的初始化,而其他元素可以进行默认赋值。考虑下面这个例子:

int a[15] = {0, 0, 29, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 48};

我们希望数组元素 2 2 2 29 29 29,元素 9 9 9 7 7 7,元素 14 14 14 48 48 48,而其他元素为 0 0 0。对于大数组,如果使用这种方式赋值,将是冗长和容易出错的。

C99 中的指定初始化式可以用于解决这一问题。上面的例子可以使用指定初始化式写为:

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

括号中的数字称为指示符

除了可以使赋值变得更简短、更易读之外,指定初始化式还有一个优点:赋值的顺序不再是一个问题,我们可以将上面的例子重新写为:

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

指示符必须是整型常量表达式。如果待初始化的数组长度为 n n n,则每个指示符的值都必须在 0 0 0 n − 1 n - 1 n1 之间。但是,如果数组的长度是省略的,指示符可以是任意非负整数,此时编译器将根据最大的指示符推断出数组的长度。

初始化式中可以同时使用老方法(逐个元素初始化)和新方法(指定初始化式):

int c[10] = {5, 1, 9, [4] = 3, 7, 2, [8] = 6};

这个初始化式指定数组的前三个元素值为 5 5 5 1 1 1 9 9 9,元素 4 4 4 的值为 3 3 3,其后两个元素为 7 7 7 2 2 2,最后元素 8 8 8 的值为 6 6 6,而没有指定值的元素均赋予默认值 0 0 0

程序 repdigit.c:检查数中重复出现的数字
该程序用来检查数中是否有出现多于一次的数字。

/* Checks numbers for repeated digits */

#include <stdbool.h>	/* C99 only */
#include <stdio.h>

int main(void)
{
	bool digit_seen[10] = {false};
	int digit;
	long n;

	printf("Enter a number: ");
	scanf("%ld", &n);

	while (n > 0) {
		digit = n % 10;
		if (digit_seen[digit])
			break;
		digit_seen[digit] = true;
		n /= 10;
	}

	if (n > 0)
		printf("Repeated digit\n");
	else
		printf("No repeated digit\n");
	
	return 0;
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter a number: 28212 ‾ \underline{28212} 28212

Repeated digit

1.4 对数组使用 sizeof 运算符

运算符 sizeof 可以确定数组的大小(字节数)。如果数组 a 10 10 10 个整数,那么 sizeof(a) 通常为 40 40 40(假定每个整数占 4 4 4 个字节)。
还可以用 sizeof 来计算数组元素(如 a[0])的大小。用数组的大小除以数组元素的大小可以得到数组的长度:sizeof(a) / sizeof(a[0])。例如,数组 a 的清零操作可以写成如下形式:

for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	a[i] = 0;

有些编译器会对表达式 i < sizeof(a) / sizeof(a[0]) 给出一条警告消息,因为变量 i 的类型可能是 int,而 sizeof 返回的值类型为 size_t(一种无符号类型)。为了避免出现这一警告,可以把 sizeof(a) / sizeof(a[0]) 强制转换为有符号整数:

for (i = 0; i < (int) (sizeof(a) / sizeof(a[0])); i++)
	a[i] = 0;

表达式 (int) (sizeof(a) / sizeof(a[0])) 写起来不太方便,定义一个宏来表示它常常是很有帮助的:

#define SIZE ((int) (sizeof(a) / sizeof(a[0])))

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

程序 interest.c:计算利息
下面这个程序显示一个表格,该表格显示了在几年时间内 100 100 100 美元投资在不同利率下的价值。

/* Prints a table of compound interest */

#include <stdio.h>

#define NUM_RATES ((int) (sizeof(value) / sizeof(value[0])))
#define INITIAL_BALANCE 100.00

int main(void)
{
	int i, low_rate, num_years, year;
	double value[5];

	printf("Enter interest rate: ");
	scanf("%d", &low_rate);
	printf("Enter number of years: ");
	scanf("%d", &num_years);
	
	printf("\nYears");
	for (i = 0; i < NUM_RATES; i++) {
		printf("%6d%%", low_rate + i);
		value[i] = INITIAL_BALANCE;
	}
	printf("\n");

	for (year = 1; year <= num_years; year++) {
		printf("%3d    ", year);
		for (i = 0; i < NUM_RATES; i++) {
			value[i] += (low_rate + i) / 100.0 * value[i];
			printf("%7.2f", value[i]);
		}
		printf("\n");
	}
	
	return 0;
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter interest rate: 6 ‾ \underline{6} 6
Enter number of years: 5 ‾ \underline{5} 5

Years     6%     7%     8%     9%    10%
  1     106.00 107.00 108.00 109.00 110.00
  2     112.36 114.49 116.64 118.81 121.00
  3     119.10 122.50 125.97 129.50 133.10
  4     126.25 131.08 136.05 141.16 146.41
  5     133.82 140.26 146.93 153.86 161.05

2. 多维数组

数组可以有任意维数。下面的声明产生一个二维数组(数学上的术语称为矩阵):

int m[5][9];

数组 m 5 5 5 9 9 9 列。如下所示,数组的行和列下标都从 0 0 0 开始索引:
二维数组
为了访问 ij 列的元素,需要写成 m[i][j] 的形式。表达式 m[i] 指明了数组 m 的第 i 行,而 m[i][j] 则选择了此行中的第 j 个元素。

虽然我们以表格形式显示二维数组,但是实际上它们在计算机的内存中不是这样存储的。C 语言是按照行主序存储数组的,也就是从第 0 0 0 行开始,接着第 1 1 1 行,依次类推。例如,下面显示了数组 m 的存储:
在这里插入图片描述
就像 for 循环和一维数组紧密结合一样,嵌套的 for 循环是处理多维数组的理想选择。例如,初始化用作单位矩阵的数组:

#define N 10

double ident[N][N];
int row, col;

for (row = 0; row < N; row++)
	for (col = 0; col < N; col++)
		if (row == col)
			ident[row][col] = 1.0;
		else
			ident[row][col] = 0.0;

C 语言中的多维数组扮演的角色相对较弱,因为 C 语言为存储多维数组提供了更加灵活的方法:指针数组(见专题十二)。

2.1 多维数组初始化

通过嵌套一维初始化式的方法可以产生二维数组的初始化式:

int m[5][9] = {{1, 1, 1, 1, 1, 0, 1, 1, 1},
			   {0, 1, 0, 1, 0, 1, 0, 1, 0},
			   {0, 1, 0, 1, 1, 0, 0, 1, 0},
			   {1, 1, 0, 1, 0, 0, 0, 1, 0},
			   {1, 1, 0, 1, 0, 0, 1, 1, 1}};

每一个内部初始化式提供了矩阵中一行的值。为高维数组构造初始化式可采用类似的方法。C 语言为多维数组提供了多种方法来缩写初始化式。

  • 如果初始化式没有大到足以填满整个多维数组,那么把数组中剩余的元素赋值为 0 0 0
  • 如果内层的列表没有大到足以填满数组的一行,那么把此行剩余的元素初始化为 0 0 0
  • 甚至可以省略掉内层的花括号,此时编译器会按照行主序对多维数组进行填充。
    在多维数组中省略掉内层的花括号可能是很危险的,因为额外的元素(更糟的情况是丢失的元素)将会影响剩下的初始化式。省略花括号会引起某些编译器产生类似 “ missing braces around initializer \text{missing braces around initializer} missing braces around initializer” 这样的警告消息。

C99 的指定初始化式对多维数组也有效。例如,可以这样创建 2 × 2 2 \times 2 2×2 的单位矩阵:

double ident[2][2] = {[0][0] = 1.0, [1][1] = 1.0};

2.2 常量数组

无论一维数组还是多维数组,都可以通过在声明的最开始处加上单词 const 而成为 “常量”。程序不应该对声明为 const 的数组进行修改,编译器能够检测到直接修改某个元素的意图。

把数组声明为 const 有两个主要的好处。它表明程序不会改变数组,这对于以后阅读程序的人可能是有价值的信息。它还有助于编译器发现错误 —— const 会告诉编译器我们不打算修改数组。

const 类型限定符(见专题十七)不限于数组,它可以和任何变量一起使用。

程序 deal.c:发牌
程序负责从一副标准纸牌中随机抽取用户所需的数量并发放给用户。该程序需要解决三个问题:

  • 如何从一副牌中随机抽取纸牌?
  • 如何避免两次抽到同一张牌?
  • 如何把纸牌的等级和花色翻译成字符格式?

为了随机抽取纸牌,可以采用一些 C 语言的库函数(见专题二五)。time 函数返回当前的时间,用一个数表示。srand 函数初始化 C 语言的随机数生成器。通过把 time 函数的返回值传递给函数 srand 可以避免程序在每次运行时发同样的牌。rand 函数在每次调用时会产生一个看似随机的数。通过采用运算符 %,可以缩放 rand 函数的返回值,使其落在 0 ∼ 3 0 \sim 3 03(用于表示花色)的范围内,或者是落在 0 ∼ 12 0 \sim 12 012(用于表示等级)的范围内。

为了避免两次都拿到同一张牌,需要记录已经选择过的牌。程序将采用一个名为 in_hand 的二维数组,数组有 4 4 4 行(每行表示一种花色)和 13 13 13 列(每列表示一种等级)。换句话说,数组中的每个元素对应着 52 52 52 张纸牌中的一张。在程序开始时,所有数组元素都为假。每次随机抽取一张纸牌时,将检查数组 in_hand 中的对应元素为真还是为假。如果为真,那么就需要抽取其他纸牌;如果为假,则把 true 存储到与这张纸牌相对应的数组元素中,以提醒我们这张纸牌已经抽取过了。

一旦证实纸牌是 “新” 的(还没有选取过),就需要把牌的等级和花色数值翻译成字符,然后显示出来。为了把纸牌的等级和花色翻译成字符格式,程序将设置两个字符数组(一个用于纸牌的等级,另一个用于纸牌的花色),然后用等级和花色对数组取下标。这两个字符数组在程序执行期间不会发生改变,所以也可以把它们声明为 const

/* Deals a random hand of cards */

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUM_SUITS 4
#define NUM_RANKS 13

int main(void)
{
	bool in_hand[NUM_SUITS][NUM_RANKS] = {false};
	int num_cards, rank, suit;
	const char rank_code[] = {'2', '3', '4', '5', '6', '7',
							  '8', '9', 'T', 'J', 'Q', 'K', 'A'};
	const char suit_code[] = {'C', 'D', 'H', 'S'};

	srand((unsigned) time(NULL));

	printf("Enter number of cards in hand: ");
	scanf("%d", &num_cards);

	printf("Your hand:");
	while (num_cards > 0) {
		suit = rand() % NUM_SUITS;		/* picks a random suit */
		rank = rand() % NUM_RANKS;		/* picks a random rank */
		if (!in_hand[suit][rank]) {
			in_hand[suit][rank] = true;
			num_cards--;
			printf(" %c%c", rank_code[rank], suit_code[suit]);
		}
	}
	printf("\n");
	
	return 0;
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter number of cards in hand: 5 ‾ \underline{5} 5

Your hand: 5D TS 2D JC TH

3. C99 中的变长数组

前面说到,数组变量的长度必须用常量表达式进行定义。但是在 C99 中,有时候也可以使用非常量表达式。

程序 reverse2.c:数列反向(改进版)

/* Reverses a series of numbers using a variable - length array */

#include <stdio.h>

int main(void)
{
	int i, n;
	
	printf("How many numbers do you want to reverse? ");
	scanf("%d", &n);
	
	int a[n];
	
	printf("Enter %d numbers: ", n);
	for (int i = 0; i < n; i++)
		scanf("%d", &a[i]);
	
	printf("In reverse order:");
	for (int i = n - 1; i >= 0; i--)
		printf(" %d", a[i]);
	printf("\n");
	
	return 0;
}

上面程序中的数组 a 是一个变长数组 variable-length array \text{variable-length array} variable-length array,简称 VLA \text{VLA} VLA)。变长数组的主要优点是程序员不必在构造数组时随便给定一个长度,程序在执行时可以准确地计算出所需的元素个数。如果让程序员来指定长度,数组可能过长(浪费内存)或过短(导致程序出错)。

变长数组的长度不一定要用变量来指定,任意表达式(可以含有运算符)都可以。例如:

int a[3*i+5];
int b[j+k];

像其他数组一样,变长数组也可以是多维的:

int c[m][n];

变长数组的主要限制是它们没有静态存储期限(见专题十七),另一个限制是变长数组没有初始化式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

朔北之忘 Clancy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值