C语言K&R圣经笔记 6.3结构体数组

6.3 结构体数组

考虑写个程序来计算每个 C 语言关键字的出现次数。我们需要一个字符串数组来保存关键字名称,还需要一个整数数组来保存数量。一种可能的方式是使用两个并行的数组,keyword 和 keycount :

char *keyword[NKEYS];
int keycount[NKEYS];

但两个数组是并行的这个事实,暗示了还有一种不同的组织方式,即结构体数组。每个关键字条目是一对:

char *word;
int count;

且有一个数组由这样的对组成。如下结构声明

struct key {
    char *word;
    int count;
} keytab[NKEYS];

声明了一个结构体类型 key,定义了这种类型的一个数组 keytab,并为它们分配了空间。数组的每个元素都是一个结构体。也可以写成:

struct key {
    char *word;
    int count;
};

struct key keytab[NKEYS];

由于 keytab 这个结构包含了名称的常量集合,最简单的办法是让它成为一个外部变量,并在定义时将它一劳永逸地初始化。结构体的初始化与之前的类似——在定义后面加上由大括号括起来的初始化列表:

struct key {
    char *word;
    int count;
} keytab[] = {
    "auto", 0,
    "break", 0,
    "case", 0,
    "char", 0
    "const", 0,
    "continue", 0,
    "default", 0,
    /* ... */
    "unsigned", 0,
    "void", 0,
    "volatile", 0,
    "while", 0
};

初始化表达式是成对出现的,对应结构体的两个成员。更精确的方式是把“每行”或者说每个结构体都用大括号括起来,例如

{"auto", 0},
{"break", 0},
{"case", 0}
...

但当初始化表达式是简单变量或字符串,而且都存在的情况下,内部的括号是不需要的。与简单类型的数组一样,如果存在初始化表达式,而且 [ ] 内为空时,结构体数组 keytab 中的条目个数将会由编译器自动计算。

关键字计算程序从 keytab 的定义开始。主例程通过反复调用 getword 函数来读取输入,getword 每次获取一个单词。每个单词都会在 keytab 中搜索,用的是我们在第三章写的二分搜索函数的字符串版本。表中的关键字必须要以升序存储。

#include <stdio.h>
#include <ctype.h>
#include <string.h>

#define MAXWORD 100

int getword(char *, int);
int binserarch(char *, struct key *, int);

/* 计算C关键字数量 */
main()
{
    int n;
    char word[MAXWORD];

    while (getword(word, MAXWORD) != EOF)
        if (isalpha(word[0]))
            if ((n = binsearch(word, keytab, NKEYS)) >= 0)
                keytab[n].count++;
    for (n = 0; n < NKEYS; n++)
        if (keytab[n].count > 0)
            printf("%4d %s\n",
                keytab[n].count, keytab[n].count);
    return 0;
}



/* binsearch: 在 tab[0]...tab[n-1]中查找word */
int binsearch(char *word, struct key tab[], int n)
{
    int cond;
    int low, high, mid;

    low = 0;
    high = n - 1;
    while (low <= high) {
        mid = (low + high) / 2;
        if (cond = strcmp(word, tab[mid].word) < 0){
            low = mid + 1;
        else if (cond > 0)
            high = mid -1;
        else
            return mid;
    }
    return -1;
}

我们待会来展示 getword 函数;现在只要知道每次调用 getword 返回一个单词,该单词会被拷贝函数的第一个参数(字符数组)中,这就足够了。

NKEYS 是 keytab 中关键字的数量。尽管我们可以手动计算,但让机器来做会更简单而且更安全,特别是这个列表还可能会变化。一种可能的方法是用一个空指针来结束初始化列表,然后在 keytab 中循环,直到遇到结尾。

但没必要做那么多,因为数组的大小完全是在编译期间确定的。数组的大小是每个元素的大小乘以元素的个数,因此元素的个数就是

 keytab 的大小 / struct key 的大小

C 语言提供了一个编译期的一元操作符 sizeof 用来计算任意对象的大小。表达式

sizeof 对象

sizeof 类型名

会得到一个整数,等于指定对象或类型所占空间的字节数。(严格地说,sizeof 产生的是无符号整数,其类型为 size_t,在 <stddef.h> 头文件中定义。)对象可以是变量或数组或结构体,类型名可以是基本类型如 int 或 double的名字,也可以是派生类型如结构体或指针的名字。

在我们的例子中,关键字的数量是数组的大小除以一个元素的大小。使用一个 #define 语句来计算并设置 NKEYS 的值。

#define NKEYS (sizeof keytab / sizeof (struct key))

另一种方法是用数组的大小除以某个特定元素的大小

#define NKEYS (sizeof keytab / sizeof keytab[0])

后者的优势在于,如果数组类型变化,这个定义也不用修改。

sizeof 不能用在 #if 行中,因为预处理器不会解析类型名。但 #define 中的表达式不是由预处理器求值,所以这里的代码是合法的。

现在来看看 getword 函数。我们写了一个比这个程序所需更为通用的 getword 函数,但并不复杂。getword 从 输入中获取下一个“单词”,其中单词是一个由字母开头的字母或数字的字符串,或者单个非空格字符。函数的值是单词的首字母,或者是代表文件结束的EOF,或者是字符本身,如果该字符不是字母的话。

/* getword:从输入中获取下一个单词或字符 */
int getword(char *word, int limit)
{
    int c, getch(void);
    void ungetch(int);
    char *w = word;

    while (isspace(c = getch()))
        ;
    if (c != EOF)
        *w++ = c;
    if (!isalpha(c)) {
        *w = '\0';
        return c;
    }
    for ( ; --lim > 0; w++) 
        if (!isalphanum(*w = getch())) {
            ungetch(*w);
            break;
        }
    *w = '\0';
    return word[0];
}

getword 使用了我们在第四章写的 getch 和 ungetch 函数。当字母和数字组成的 token 结束时,getword 多读了一个字符。调用 ungetch 把这个字符推回给输入,供下一次调用使用。getword 还使用了 isspace 来跳过空白字符,isalpha 来识别字母,isalphanum来识别字母和数字;这些函数都来自标准头文件 <ctype.h>。

练习6-1、我们这个版本的 getword 没有正确处理下划线,字符串常量,注释,以及预处理控制行。写个更好的版本。