1.写在前面
前面的我们学习了C语言的变量、控制流、函数、指针,这节博客我们来学习C语言的结构的,C语言基础的最后一个模块。那么什么是结构呢?结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些的变量组织在一个名字之下。
2.结构的基本知识
我们知道数学中有一种概念叫做坐标系,坐标系中点是最基本的对象,假定用x与y坐标表示它,且x、y的坐标值都为整数。那么在C语言中怎么表示呢?具体的如下:
struct point {
int x;
int y;
}
关键字struct
引入结构声明。关键字struct
后面的名字是可选的,称为结构标记(point
)。结构标记用于结构命名,在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。
结构中定义的变量称为成员。结构成员、结构标记和普通变量可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。
struct声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其它基本类型的变量声明是相同的。
如果结构声明的后面不带变量表,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓。但是,如果结构声明中带有标记,那么在以后定义结构实例时便可以使用该标记定义。
3.结构与函数
结构的合法操作只有几种:作为一个整体复制和赋值,通过&运算符取地址,访问其成员。
其中复制和赋值包括向函数传递参数以及从函数返回值。主要的方法有:一是分别传递各个结构成员,二是传递整个结构,三是传递指向结构的指针。
首先我们看下如下的函数:
struct point makepoint(int x, int y)
{
struct point temp;
temp.x = x;
temp.y = y;
return temp;
}
现在可以使用makepoint
函数动态地初始化任意结构,也可以向函数提供结构类型的参数。
struct rect screen;
struct point middle;
struct point makepoint(int, int);
screen.pt1 = makepoint(0,0);
screen.pt2 = makepoint(XMAX, YMAX);
middle = makepoint((screen.pt1.x + screen.pt2.x)/2,
(screen.pt1.y + screen.pt2.y)/2);
接下来我们编写一个函数对点执行算术运算。如下:
struct addpoint(struct point p1, struct point p2)
{
p1.x += p2.x;
p1.y += p2.y;
return p1;
}
其中函数的参数和返回值都是结构类型,之所以直接将相加所得的结果所得的结果赋值给p1,而没有使用显示的临时变量存储,是为了强调结构类型的参数和其他类型的参数一样,都是通过值传递的。
如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高。结构指针类似于普通变量指针。
其中结构指针使用频度非常高,为了使用方便,C语言提供了另外一种简写的方式。假定P是一个指向结构的指针,可以用
p -> 结构成员
这种形式引用相应的结构成员。
运算符.和->都是从左至右结合的,所以,对于下面的声明:
struct point {
int x;
int y;
}
struct rect {
struct point pt1;
struct point pt2;
};
struct rect r, *rp = &r;
// 以下4个表达式是等价的
r.pt1.x;
rp->pt1.x;
(r.pt1).x;
(rp->pt1).x;
例如,对于如下的结构声明
struct {
int len;
char *str;
} *p;
*p->str读取的是指针str所指向的对象的值;
*p->str++先读取指针的str指向的对象的值,然后再将str加1;
(*p->str)++将指针str指向的对象值加1;
*p+±>str先去读指针str指向的对象的值,然后再将p加1;
4.结构数组
讲结构数组的时候,我们可以想到以前的一个程序,它用来统计输入中各个C语言关键字的出现的次数。我们可以使用如下的结构数组,具体的如下:
struct key {
char *word;
int count;
} keytab[NKEYS];
它声明了一个结构类型key,并定义了该类型的机构数组keytab,同时为其分配存储空间。数组keytab的每个元素都是一个结构。上述声明也可以写成下列的形式:
struct key {
char *word;
int count;
};
struct key keytab[NKEYS];
我们来看下如下的程序,具体的如下:
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
int getword(char *, int);
int binsearch(char *, struct key *, int);
/* count C keywords */
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].word);
return 0;
}
/* binsearch: find word in tab[0]...tab[n-1] */
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)
high = mid - 1;
else if (cond > 0)
low = mid + 1;
else
return mid;
}
return -1;
}
上面的NKEYS代表keytab中关键字的个数。尽管可以手工计算,但由机器实现会更简单、更安全。数组的长度在编译时已经完全确定,它等于数组项的长度乘以项数,因此,可以得出项数为:
Keytab的长度/struct key的长度
C语言提供了一个编译时一元运算符sizeof,它可用来计算任意对象的长度。表达式
sizeof 对象 以及 sizeof(类型名)
将返回一个整型值,它等于指定对象或类型占用的存储空间字节数。
所以我们可以得出如下计算数组长度的公式,具体的如下:
#define NKEYS (sizeof keytab / sizeof(struct key))
#define NKEYS (sizeof keytab / sizeof(keytab[0]))
这里更偏向去第二种方法,即使类型改变了,也不需要改动程序。
条件编译语句#if中不能使用sizeof,因为预处理不对类型名进行分析。但预处理器并不计算#define语句中的表达式,因此,在#define中使用sizeof是合法的。
5.指向结构的指针
前面的程序我们使用的是结构数组,为此我们修改下程序,修改成指向结构的指针。具体的代码的代码如下:
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
int getword(char *, int);
struct key *binsearch(char *, struct key *, int);
/* count C keywords; pointer version */
main()
{
char word[MAXWORD];
struct key *p;
while (getword(word, MAXWORD) != EOF)
if (isalpha(word[0]))
if ((p=binsearch(word, keytab, NKEYS)) != NULL)
p->count++;
for (p = keytab; p < keytab + NKEYS; p++)
if (p->count > 0)
printf("%4d %s\n", p->count, p->word);
return 0;
}
/* binsearch: find word in tab[0]...tab[n-1] */
struct key *binsearch(char *word, struck key *tab, int n)
{
int cond;
struct key *low = &tab[0];
struct key *high = &tab[n];
struct key *mid;
while (low < high) {
mid = low + (high-low) / 2;
if ((cond = strcmp(word, mid->word)) < 0)
high = mid;
else if (cond > 0)
low = mid + 1;
else
return mid;
}
return NULL;
}
上面的binsearch函数在声明中必须表明:它返回的值类型是一个指向struct key类型的指针,而非整型,这在函数原型及binsearch函数中都要声明。如果binsearch找到与输入单词匹配的数组元素,它将返回一个指向该元素的指针,否则返回NULL。
其中主程序main中有下列的语句:
for (p = keytab; p < keytab + NKEYS; p++)
如果p是指向结构的指针,则对p的算术运算需要考虑结构的长度,所以,表达式p++执行时,将在p的基础上加上一个正确的值,以确保得到结构数组的下一个元素。
但是,千万不要认为数组的长度等于各成员长度的和。因为不同的对象有不同的对齐要求,所以,结构中可能会出现未命名的空穴。
6.自引用结构(二叉树)
还是原来的问题,就是统计输入中所有单词的出现次数。
我们可以读取输入中任意单词的同时,就将它放置到正确的位置,从而始终保证所有单词是按顺序排列的。虽然这可以不用通过在线性数组中移动单词来实现,但它仍然会导致执行的时间过长。我们可以使用一种称为二叉树的数据结构来取而代之。
每个不同的单词在树中都是一个节点。每个节点包含:
- 一个指向该单词内容的指针
- 一个统计出现次数的计数值
- 一个指向左子树的指针
- 一个指向右子树的指针
任何节点最多拥有两个子树,也可能只有一个子树或一个都没有。
有了上面的知识,我们可以得出如下的结构,具体的如下:
struct tnode {
char * word;
int count;
struct tnode *left;
struct tnode *right;
}
最后主函数通过getword
读入单词,并通过addtree
函数将它们插入到树中。具体的代码如下:
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
struct tnode *addtree(struct tnode *, char *);
void treeprint(struct tnode *);
int getword(char *, int);
/* word frequency count */
main()
{
struct tnode *root;
char word[MAXWORD];
root = NULL;
while (getword(word, MAXWORD) != EOF)
if (isalpha(word[0]))
root = addtree(root, word);
treeprint(root);
return 0;
}
主函数main以参数的方式传递给该函数的一个单词将作为树的最顶层。在每一步中,新单词与节点中存储的单词进行比较,随后,通过递归调用addtree而转向左子树或右子树。改单词最终将于树中某节点匹配(这种情况下计数值加1)或遇到一个空指针(表明必须创建一个节点并加入到树中)。若生成了新节点,则address返回一个指向新节点的指针,该指针保存在父节点中。具体的代码如下:
struct tnode *talloc(void);
char *strdup(char *);
/* addtree: add a node with w, at or below p */
struct treenode *addtree(struct tnode *p, char *w)
{
int cond;
if (p == NULL) { /* a new word has arrived */
p = talloc(); /* make a new node */
p->word = strdup(w);
p->count = 1;
p->left = p->right = NULL;
} else if ((cond = strcmp(w, p->word)) == 0)
p->count++; /* repeated word */
else if (cond < 0) /* less than into left subtree */
p->left = addtree(p->left, w);
else /* greater than into right subtree */
p->right = addtree(p->right, w);
return p;
}
新节点的存储空间由子程序talloc获得。talloc函数返回一个指针,指向能容纳一个树节点的空闲空间。函数strdup将新单词复制到某个隐藏位置,计数值将被初始化。
然后我们再来看下treeprint函数按顺序打印树。具体的代码如下:
/* treeprint: in-order print of tree p */
void treeprint(struct tnode *p)
{
if (p != NULL) {
treeprint(p->left);
printf("%4d %s\n", p->count, p->word);
treeprint(p->right);
}
}
最后我们再来看下剩下的两个函数,具体的如下:
#include <stdlib.h>
/* talloc: make a tnode */
struct tnode *talloc(void)
{
return (struct tnode *) malloc(sizeof(struct tnode));
}
char *strdup(char *s) /* make a duplicate of s */
{
char *p;
p = (char *) malloc(strlen(s)+1); /* +1 for '\0' */
if (p != NULL)
strcpy(p, s);
return p;
}
7.表查找
链表中每个块都是一个结构,它包含一个指向名字的指针、一个指向替换文本的指针以及一个指向该链表后继块的指针。如果指向链表后继块的指针为NULL,则表明链表结束。
具体的结构体如下:
struct nlist { /* table entry: */
struct nlist *next; /* next entry in chain */
char *name; /* defined name */
char *defn; /* replacement text */
};
#define HASHSIZE 101
static struct nlist *hashtab[HASHSIZE]
我们可以看下一个简单的散列的函数,具体的如下:
/* hash: form hash value for string s */
unsigned hash(char *s)
{
unsigned hashval;
for (hashval = 0; *s != '\0'; s++)
hashval = *s + 31 * hashval;
return hashval % HASHSIZE;
}
我们再来看下lookup的函数,具体的代码如下:
/* lookup: look for s in hashtab */
struct nlist *lookup(char *s)
{
struct nlist *np;
for (np = hashtab[hash(s)]; np != NULL; np = np->next)
if (strcmp(s, np->name) == 0)
return np; /* found */
return NULL; /* not found */
}
再来看下install的函数,具体的如下:
struct nlist *lookup(char *);
char *strdup(char *);
/* install: put (name, defn) in hashtab */
struct nlist *install(char *name, char *defn)
{
struct nlist *np;
unsigned hashval;
if ((np = lookup(name)) == NULL) { /* not found */
np = (struct nlist *) malloc(sizeof(*np));
if (np == NULL || (np->name = strdup(name)) == NULL)
return NULL;
hashval = hash(name);
np->next = hashtab[hashval];
hashtab[hashval] = np;
} else /* already there */
free((void *) np->defn); /*free previous defn */
if ((np->defn = strdup(defn)) == NULL)
return NULL;
return np;
}
8.类型定义(typedef)
typedef的功能是用来建立新的数据类型名,例如,声明typedef int Length;将Length定义为int具有同等意义的名字。
我们可以看一个复杂的例子,具体的如下:
typedef struct tnode *Treeptr;
typedef struct tnode { /* the tree node: */
char *word; /* points to the text */
int count; /* number of occurrences */
struct tnode *left; /* left child */
struct tnode *right; /* right child */
} Treenode;
从任何意义上讲,typedef声明并没有创建一个新类型,它知识为某个已存在的类型增加了一个新的名称而已。typedef类似于#define语句,但由于typedef是编译器解释的,因此它的文本替换功能要超过预处理器的能力。
9.联合
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。
格式如下:
//定义方式
union u_tag {
int ival;
float fval;
char *sval;
} u;
// 访问方式
if (utype == INT)
printf("%d\n", u.ival);
if (utype == FLOAT)
printf("%f\n", u.fval);
if (utype == STRING)
printf("%s\n", u.sval);
else
printf("bad type %d in utype\n", utype);
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都是0,此结构空间要大到最够容纳最"宽"的成员,并且,其对齐方式要适合于联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址以及访问其中一个成员。
10.位字段
在存储空间很宝贵的情况下,有可能需要将多个对象保存在一个机器字中。一种常用的方法是,使用类似于编译器符号表的单个二进制位标志集合。外部强加的数据格式(如硬件设备接口)也经常需要从字的部分值读取数据。
C语言中提供了一种方法,即直接定义和访问一个字中的位字段的能力,而不需要通过按位逻辑运算符。位字段或简称字段,是字中相邻位的集合。字是单个的存储单元,它同具体的实现有关。我们看到如下的定义,具体的如下:
struct {
unsigned int is_keyword : 1;
unsigned int is_extern : 1;
unsigned int is_static : 1;
} flags;
这里定义了一个变量flags,它包含3个一位的字段。冒号后的数字表示字段的宽度(用二进制位数表示)。字段被声明为unsigned int
类型,以保证它们是无符号量。
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界由具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界上对齐。
某些机器上字段的分配是从字的左端至右端进行的,而某些机器上则相反。这意味着,尽管字段对维护内部订单的数据结构很有用,但在选择外部定义数据的情况下,必须仔细考虑那端优先的问题。依赖于这些因素的程序是不可移植的。字段也可以仅仅声明为int,为了方便移植,需要显示声明该int类型是singned
还是unsigned
类型。字段不是数组,并且没有地址,因此对它们不能使用&运算符。
11.写在最后
本篇博客我们主要介绍下了结构,至此C语言的所有基础的东西我们都学完了。后面我们要看看C语言的其他的内容。