Warning:
为了避免非零基础人群感到身体不适、头晕恶心、易怒及粗口,请不要查看以下内容。
本章我们将继续学习C语言关于结构的相关知识。
第6章 结构
结构是一个或多个变量的集合,这些变量可能为不同类型,为了处理的方便而将这些变量组织在一个名字之下。(某些语言将结构成为“记录”,比如Pascal语言。)由于结构将一组相关的变量看作一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中。
工资记录是用来描述结构的一个传统例子。每个雇员有一组属性描述,如姓名、地址、社会保险号、工资等。其中的某些属性也可以是结构,例如姓名可以分为几部分,地址甚至工资也可能出现类似的情况。C语言中更典型的一个例子来自于图形领域:点由一对坐标定义,矩形由两个点定义,等等。
ANSI标准在结构方面最主要的变化是定义了结构的赋值操作——结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。多年以前,这一操作就已经被大多数的编译器所支持,但是,直到这一标准才对其属性进行了精确定义。在ANSI标准中,自动结构和数组现在也可以进行初始化。
6.1 结构的基本知识
我们首先建立一些适用于图形领域的结构。点是最基本的对象,我们建立2个点(0,0)和(4,3):
struct point{
int x;
int y;
};
关键字struct引入结构声明。point称为结构标记,用于为结构命名。
结构中定义的变量成为成员。结构成员、结构标记和普通变量(即非成员)可以采用相同的名字,它们之间不会冲突。不同结构中的成员也可以使用相同的名字。
struct声明定义了一种数据类型。在标志结构成员表结束的右花括号之后可以跟一个变量表,这与其它基本类型的变量声明是相同的,例如:
struct { ... } x, y, z;
如果结构后面不带变量表,则不需要为他分配存储空间,它仅仅描述了一个结构的模板或轮廓。但是,如果结构声明中带有标记,那么在以后定义结构实例时便可以使用该标记定义。例如:
struct point maxpt = { 320, 200 };
自动结构也可以通过赋值初始化,还可以通过调用返回相应类型结构的函数进行初始化。
在表达式中,可以通过下列形式引用某个特定结构中的成员:
结构名.成员
其中结构成员运算符“.”将结构名与成员名连接起来。例如,可用下列语句打印点pt的坐标:
printf("%d,%d", pt.x, pt.y);
结构也可以嵌套。我们可以用对角线上的两个点来定义矩形,相应的结构定义如下:
struct rect{
struct point pt1;
struct point pt2;
};
结构rect包含两个point类型的成员。如果按照下列方式声明screen变量:
struct rect screen;
则可以使用语句
screen.pt1.x
引用screen的成员pt1的x坐标。
6.2 结构与函数
首先我们看一下函数makepoint,它带有两个整型参数,并返回一个point类型的结构:
struct point makepoint(int x, int y)
{
struct point temp;
temp.x = x;
temp.y = y;
return point;
}
当结构作为函数的参数时,如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高。结构指针类似于普通变量指针。声明:
struct point *pp;
将pp定义为一个指向struct point类型对象的指针。如果pp指向一个point结构,那么*pp即为该结构。而(*pp).x和(*pp).y则是结构成员。
结构指针的使用频度非常高,为了使用方便,C语言提供了另一种简写方式。假定p是一个指向结构的指针,可以用:
p->结构成员
这种形式引用相应的结构成员。这样,就可以用下面的形式改写上面的一行代码:
printf(“origin is (%d,%d)\n”, pp->x, pp->y);
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,同时为其分配存储空间。数组keytab的每个元素都是一个结构。
因为结构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中的项数。
C语言提供了一个编译时一元运算符sizeof,它可以用来计算任一对象的长度。表达式:
sizeof 对象
以及
sizeof (类型名)
将返回一个整型数,它等于指定对象或类型占用的存储空间字节数。(严格的说,sizeof的返回值是无符号整型值,其类型为size_t,该类型在头文件<stddef.h>中定义。)其中,对象可以是变量、数组或结构;类型可以是基本类型,也可以是派生类型,如:结构类型或指针类型。
6.4 指向结构的指针
为了进一步的说明指向结构的指针和结构数组,我们重新编写关键字统计程序。修改后的程序如下:
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
int getwrod(char *, int)
struct key *binsearch(char *, struct key *, int);
/* 统计关键字的出现次数;采用指针方式实现的版本 */
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函数:在tab[0]...tab[n-1]中查找与读入单词匹配的元素 */
struct key *binsearch(char *word, struct 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。
其次,keytab的元素在这里是通过指针访问的。这就需要对binsearch做较大的修改。
6.5 自引用结构
假定我们需要处理一个更一般化的问题,统计输入中所有的单词出现的次数。为了提高效率,我们采用二叉树的数据结构进行程序设计。
每个不同的单词都是一个节点,每个节点包含:
- 一个指向该单词内容的指针
- 一个统计出现次数的计数值
- 一个指向左子树的指针
- 一个指向右子树的指针
对节点的所有操作要保证,任何节点的左子树只包含按字典序小于该节点中单词的那些单词,右子树只包含按字典序排列大于该节点中单词的那些单词。
要查找一个新单词是否已在树中,可以从根节点开始,比较新单词与该节点中的单词。若匹配,则得到肯定的答案。若新单词小于该节点中的单词,则在左子树中继续查找,否则就在右子树中进行查找。
我们再来看节点描述问题。最方便的表示方式是表示为包括4个成员的结构:
struct tonde { /* 树的节点 */
char *word; /* 指向单词的指针 */
int count; /* 单词时出现的次数 */
struct tonde *left; /* 左子节点 */
struct tonde *right; /* 右子节点 */
};
这种对节点的递归的声明方式看上去好像是不确定的,但他的确是正确的。一个包含其自身实例的结构是非法的,但是,下列声明是合法的。
struct tonde *left;
它将left声明为指向tonde的指针,而不是tonde实例本身。
我们偶尔也会使用自引用的变体,即两个结构相互引用。例如:
struct t {
...
struct s *p; /* p指向一个s结构 */
};
struct s {
...
struct t *q; /* q指向一个t结构 */
};
这里看不懂了。。。暂时略过。。
6.6 表查找
初学阶段,暂时不深入研究。
6.7 类型定义(typedef)
C语言提供了一个称为typedef的功能,它用来建立新的数据类型名,例如,声明:
typedef int Length;
将Length定义为与int具有同等意义的名字。类型Length可用于类型声明、类型转换等,它和int类型完全相同。
注意,typedef中声明的类型在变量名的位置出现,而不是紧接在关键字typedef之后。typedef在语法上类似于存储类extern、static等。我们在这里以大写字母作为typedef定义的类型名的首字母,以示区别。
6.8 联合
联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息。它类似于Pascal语言中的变体记录。
我们来看一个例子(可以在编译器的符号表管理程序中找到该例子)。假设一个常量可能是int、float或字符指针。特定类型的常量值必须保存在合适类型的变量中,然而,如果该常量的不同类型占据大小相同的存储空间,且保存在同一个地方的话,表管理将最方便。这就是联合的目的——一个变量可以合法的保存多种数据类型中任何一种类型的对象。其语法基于进本结构,如下:
union u_tag{
int ival;
float fval;
char *sval;
}u;
变量u必须足够大,以保存这3种类型中最大的一种,具体长度同具体的实现有关。这些类型中的任何一种类型的对象都可赋值给u,且可使用在随后的表达式中,但必须保证是一致的:读取的类型必须是最近一次存入的类型。程序员负责跟踪当前保存在联合中的类型。如果保存的类型于读取的类型不一致,其结果取决于具体的实现。
可以通过下列语法访问联合中的成员:
联合名.成员
或
联合指针->成员
它与访问结构的方式相同,如果用变量utype跟踪保存在u中的当前数据类型,则可以像下面这样使用联合:
if (utype == INT )
printf("%d\n", u.ival);
else if (utype == FLOAT)
printf("%f\n", u.fval);
else if (utype == STRING)
printf("%s\n", u.sval);
else
printf("bad type %d in utype\n", utype);
联合可以使用在结构和数组中,反之亦可。访问结构中的联合(或反之)的某一成员的表示法与嵌套结构相同。
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足以容纳最"宽"的成员,并且,其对齐方式要适合于联合中所有类型的成员。对联合允许的操作与对结构允许的操作相同。
联合只能用其第一个成员类型的值进行初始化。
6.9 位字段
初学阶段,暂时不做讲解。
总结
本章我们学习了结构的相关知识。在C语言中,结构也是非常重要的一部分。接下来我们将学习最后一章“输入与输出”(由于暂时没接触linux相关知识,暂时不学习最后一章)。继续坚持,学完最后一章就对C语言的整体知识框架有了一定的了解。后续会对单独的知识点进行深入研究和灵活运用。