C 提供了矩形的多维数组,虽然实际上它们用得比指针数组少得多。本节我们将展示多维数组的一些特性。
考虑下日期转换的问题:把某月的第几天转换为当年的第几天,以及反向转换。例如,3月1日是非闰年的第60天,是闰年的第61天。我们定义两个函数来做这个转换:day_of_year 把月和日转换为年的第几天,month_day 把年的第几天转换成月和日。因为第二个函数要计算两个值,所以月和日这两个参数会是指针:
month_day(1988, 60, &m, &d)
会将 m 设为 2,d 设为 29(2月29日)。
这两个函数需要相同的信息,每个月有多少天的表格(“七月大、八月大、九月小...”)。由于闰年和平年月份的天数不一样,把这些数据分开放到二维数组的两行,会比计算时去判断二月份的天数要简单。数组和执行转换的函数如下:
static char daytab[2][13] = {
{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
};
int day_of_year(int year, int month, int day)
{
int i, leap;
leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
for (i = 0; i < month; i++)
day += daytab[leap][i];
return day;
}
void month_day(int year, int yearday, int *pmonth, int *pday)
{
int i, leap;
leap = year%4 == 0 && year%100 != 0 || year%400 == 0;
for (i = 1; yearday > daytab[leap][i])
yearday -= daytab[leap][i];
*pmonth = i;
*pday = yearday;
}
回忆一下,逻辑表达式的算术值,不是0(假)就是1(真),所以能赋给 leap,用作数组 daytab 的下标。
对 day_of_year 和 month_day 而言,数组 daytab 只能是外部的,这两个函数才能共用它。我们把它的类型定义为 char, 是为了说明,用 char 来保存非字符的小整数,是一种正当的用法。【21世纪的应用程序员应该不用这么节省内存了】
daytab 是我们处理的第一个二维数组。在 C 中,二维数组实际上是一维数组,其【高维的】每个元素是一个数组。这样下标就能写成
daytab[i][j] /* [列][行] */
而不是
daytab[i, j] /* 错误 */
除了这个表示法上的区别,二维数组的处理方式与其他编程语言差不多是一样的。元素是按行来存的,因此最右边的下标,或者说列,在以存储顺序访问数组元素时,变化最快。
一维数组以大括号内的一个初始化表达式列表来初始化;二维数组的每行以对应的子列表来初始化。我们让 daytab 从第 0 列开始,这样月份就自然地从 1 到 12 而不是 0 到 11。因为这里内存空间并不稀缺,这种写法比调整下标会更清晰。
如果二维数组要传递给函数,函数中的参数声明必须包含列数;行数是无关紧要的,因为依旧传递的是一个指向行的指针,而每行是一个数组。在这个特例中,该指针指向的对象是这些包含 13 个 int 的数组。因此,如果 daytab 要传递给函数 f,f 的声明会是
f(int daytab[2][13]) { ... }
或是
f(int daytab[][13]) { ... }
既然行数无关紧要,也能写成
f(int (*daytab)[13]) { ... }
说明参数是指向有 13 个整数的数组的指针。括号是必须的,因为中括号 [] 的优先级比 * 号高。如果没有括号,如下声明
int *daytab[13]
是一个数组,包含13个整数指针。更通用地说【即针对所有多维数组】,数组的第一维(下标)是随意的,其他维度必须指定。
5.12节有对复杂声明的进一步讨论。
练习5-8、dat_of_year 和 month_day 中没有错误校验。修改这个缺陷。
考虑这个问题:写个函数 month_name(n) ,返回包含第 n 个月名字的字符串指针。这是内部 static 数组的理想应用场景。month_name 包含一个私有的字符串数组,当它被调用时会返回指向正确字符串的指针。本节展示了这个包含月份名的数组是如何初始化的。
语法与之前初始化语法类似:
/* month_name:返回第n个月的名字 */
char *month_name(int n)
{
static char *name[] = {
"Illegal month",
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December"
};
return (n < 1 || n > 12) ? name[0] : name[n];
}
name 是一个字符串指针的数组,它的声明与前面文本行排序例子中 lineptr 的声明一样。初始化表达式是一列字符串;每个字符串都被赋给数组中对应的位置。第 i 个字符串存放在内存的某处,而指向它们的指针存在了 name[i] 中。由于数组名 name 的长度没有指定,编译器会计算初始化表达式,并填充正确的数值。
C 初学者有时会搞不清二维数组与指针数组之间的区别,比如前一个例子中的 name。给出如下定义
int a[10][20];
int *b[10];
则 a[3][4] 和 b[3][4] 在语法上都是合法的,都是对一个 int 的引用。但 a 是一个真正的二维数组:有 200 个 int 大小的内存已经被分配了,而且使用传统的矩形下标计算公式 20*行+列 来找数组元素 a[行][列] 。然而,对 b 来说,这个定义只分配了10个指针的空间,而且没有初始化;必须显式初始化,不管是静态初始化或是通过代码来初始化。 假定 b 的每个元素的确指向了一个有20个元素的数组,则总共需要有200个 int 被分配,再加上 10 个内存单元用来放指针。指针数组的重要优势在于,数组的每行可以是不同的长度。也就是说, b 的每个元素不需要都指向有 20个元素的向量【数组】;某些可以指向有 2个元素的,或者有 50个元素的,甚至不指向任何东西【NULL】。
虽然我们上面是拿整数来讨论,但目前为止最常用的指针数组是用来保存不同长度的字符串,例如函数 month_name 中保存各月份名称的 name 数组。指针数组的声明和图示如下:
char *name = {"Illegal month", "Jan", "Feb", "Mar"};
对比二维数组的声明和图
char aname[][15] = {"Illegal month", "Jan", "Feb", "Mar"};
练习5-9、重写 day_of_year 和 month_day,但使用指针而不是索引。