《C程序设计语言(第2版·新版)》第6章 结构
结构:若干个可能是不同类型的变量的集合,为方便将它们组织在一个名字下;这有助于在大型程序中组织复杂的结构;例子:工资记录(包含姓名、地址、社会保险号、工资等属性,每个属性也可以是结构);点(由一对坐标定义);矩形(由两个点定义)
ANSI 精确定义了结构的赋值操作:结构可以拷贝、赋值、传递给函数,函数也可返回结构类型的值;自动结构和数组也可以初始化;
6.1 结构的基本知识
结构声明的例子:
struct point{
int x;
int y;
};
结构声明由包含在花括号内的一系列声明组成;struct后的名字可选,称作结构标记,用来为结构命名,定义之后它就代表花括号内的声明,可用它作为该声明的简写形式;
结构中定义的变量成为成员;结构成员、结构标记和普通变量(即非成员)可采用相同名字而不会冲突(通过上下文总可以区分它们);不同结构中的成员可以使用相同名字;
struct声明定义了一种数据类型,其后可以带一个变量表:
struct {...} x, y, z;//语法上类似于int x, y, z; 可无结构标记;不带变量则只是描述一个结构模板,带变量则会给变量分配存储空间
有标记之后就可以用它来定义结构,例如:
struct point pt; // 定义了一个struct point类型的变量pt
结构定义后面可以使用常量表达式组成的初值表来初始化:
struct point maxpt={200,350};
??自动结构也可通过赋值初始化,还可通过调用相应类型结构的函数来初始化;
表达式中可以通过以下形式引用某个特定结构中的成员:
结构名.成员 // "."称作结构成员运算符;
结构可以嵌套,例如
struct rect{
struct point pt1;
struct point pt2;
} screen;
引用:
screen.pt1.x
6.2 结构与函数
结构只有3种合法操作:
A. 作为一个整体复制和赋值(包括向函数传递参数以及从函数返回值);
B. 通过&运算符取地址;
C. 访问其成员
结构之间不可以进行比较;
至少可以通过3种各有利弊的方法来传递结构:
A. 分别传递各个结构成员;
B. 传递整个结构
C. 传递指向结构的指针
例子1(传递整个结构,一个将矩形坐标规范化(pt1坐标小于pt2坐标)的函数):
#define max(a, b) ((a)>(b)?(a):(b))
#define min(a, b) ((a)<(b)?(a):(b))
struct rect canonrect(struct rect r){
struct rect temp;
temp.pt1.x=min(r.pt1.x, r.pt2.x);
temp.pt1.y=min(r.pt1.y, r.pt2.y);
temp.pt2.x=max(r.pt1.x, r.pt2.x);
temp.pt2.y=max(r.pt1.y, r.pt2.y);
return temp;
}
如果传给函数的结构很大,使用指针通常可以提高效率;
struct point *pp; //定义了一个指向struct point类型的指针
通过(*pp).x可以引用结构成员;结构指针使用频度非常高,所以C提供了一种简写形式:
p->结构成员
对于声明:struct rect r, *rp=&r; 以下四个表达式等价:
r.pt1.x
rp->pt1.x
(r.pt1).x
(rp->pt1).x
所有运算符中这4个优先级最高,和操作数的结合最紧密:"."、"->"、"()"、"[]";
*p++->str先读取指针str指向的对象的值,然后再将p加1;
6.3 结构数组
例子:统计各个关键字出现的次数。声明结构类型key并定义该类型结构数组:
struct key{
char *word;
int count;
} keytab[]={
"auto", 0,
"break", 0,
"while", 0
};
更精确的做法是将每一行即每个结构的初值放在花括号里,如{{"auto", 0},{},...};
初值是简单变量或字符串且任何值都不为空,则内层花括号可省略;
通常如果初值存在并且[]内没有数值,编译时会计算数组keytab的项数;
数组长度在编译时已经完全确定(项长度乘以项数),则结构keytab的项数=keytab长度/struct key的长度;C语言提供了一个“编译时一元运算符”sizeof:
sizeof 对象 //对象可以是变量、数组、结构
sizeof(类型名) //类型可以是基本或派生(如结构、指针)类型
都将返回一个整型值(无符号的size_t型,定义在<stddef.h>中),等于指定对象或者类型占用的存储空间字节数;所以,keytab的项数NKEYS等于sizeof keytab/sizeof(struct key) 或者sizeof keytab/sizeof keytab[0];
#if不能使用sizeof,因为预处理器不分析类型名;#define可以使用sizeof,因为预处理器并不计算#define中的表达式;
6.4 指向结构的指针
指针之间的加法运算是非法的,例如mid=(low+high)/2; 这可借助减法来实现:mid=low+(high-low)/2; (It:很显然这两个式子在数学上等价,但是在编程中不等价)
使用指针实现算法,要确保不出现非法指针或试图访问数组范围外的元素;例如&tab[-1]和&tab[n]都超出了tab的范围,前者绝对非法,后者间接引用是非法的但是其指针算术运算可以正确执行(C语言定义保证了这一点);
千万不要认为结构的长度等于各成员长度的和,因为不同对象有不同的对齐要求,所以结构中可能出现未命名的空穴(hole);例如假设char占用1个字节,int占用4个字节,那么以下结构:
struct{
char c;
int i;
}
可能需要8个字节的存储空间而非5个;使用sizeof运算符可以返回正确的对象长度;
程序的格式问题:为了容易看出和找到函数名,对返回值类型比较复杂(如结构指针)的函数,例如:
struct key *binsearch(char *word, struct key *tab, int n)
可以写成另一种格式:
struct key *
binsearch(char *word, struct key *tab, int n)
(??有空搞清楚a[]、a、&a的区别)
6.5 自引用结构
例子:统计输入中所有单词出现的次数。因为预先不知道出现的单词列表所以无法方便地排序并且折半查找;如果对输入得每个单词都执行线性查找(在线性数组中移动单词,看它是否已出现过),那会花费太多时间(正比于输入单词数的平方);
另一种方法:输入任意单词,就按照顺序,将它放到正确位置上,但这仍会花费过长的执行时间;改进:采用二叉树这种数据结构来取代线性数组;
每个单词在树中都是一个节点,每个节点包括:
· 一个指向该单词内容的指针
· 一个统计出现次数的计数值
· 一个指向左子树的指针
· 一个指向右子树的指针
其中任何节点可能拥有2或1或0个子树;对节点的所有操作要保证:其左子树只包含(按字典)小于它的单词,右子树类似;
要查找一个新单词是否在树中,与根节点比较(匹配:找到;小于,去左子树找;大于,去右子树找;该方向无子树:放入此空位置);从任意节点出发都是这样的方式,所以该过程是递归的;在在插入和打印过程使用递归也是很自然的;
用结构来进行节点的递归声明:
struct tnode{
char *word;
int count;
struct tnode *left;
struct tnode *right;
};
包含其自身实例的结构是非法的,但是像上面一样包含指向自身结构类型的指针是合法的;
偶尔会使用自引用结构的一种变体:
struct t{
...
struct s *p;
};
struct s{
...
struct t *q
};
p122此例中main调用的函数addtree和treeprint都是递归的;addtree先判断节点是否空,空则创建(需申请存储空间)并初始化,非空则比较(等于则操作count,不等于则递归到某子树);
上述初始化中需使用复制单词到某个隐藏位置的strdup函数,它需要调用malloc函数;
单词若非随机到达,树会变得不平衡,运行时间大大增加;最坏情况是单词已排好序,程序模拟线性查找的开销会非常大;(某些广义二叉树不受这种最坏情况的影响)
存储分配程序malloc:需满足各种类型对象的对齐要求(通过牺牲一些存储空间,确保程序总返回满足所有对齐要求的指针);需返回不同类型的指针(一种合适方法是将返回值声明为指向void的指针,在使用时写一个talloc(本例)函数将void *类型强制转换为所需struct tnode *类型);
malloc函数在<stdlib.h>中;若无可用空间,malloc将返回NULL,调用它的函数需负责出错处理;调用malloc得到的空间可通过free函数释放;
6.6 表查找
例子:编写一个表查找程序的核心部分代码(很典型,在宏处理器或编译器符号表管理例程中可找到)
散列查找方法:输入的名字被转换为一个小的非负整数,作为一个指针数组的下标;数组每个元素指向某个链表的表头,链表中的每个块用于描述具有该散列值的名字;每个块都是一个结构,包含指向名字的指针、指向替换文本(本例是#define替换)的指针、指向后继块的指针(它为空则表明链表结束):
struct nlist{
struct nlist *next;
char *name;
char *defn;
};
相应的指针数组定义:
#define HASHSIZE 101
static struct nlist *hashtab[HASHSIZE];
字符串(名字)生成散列值需要hash函数,散列值作为在数组hashtab中执行查找的起始下标;具体查找过程由函数lookup(在表中查找名字)实现,其中的for循环:
for(ptr=head; ptr!=NULL; ptr=ptr->next)
...
是遍历一个链表的标准方法;
install函数将名字和替换文本记录到表中,其中要借助lookup函数来判断;
6.7 类型定义
typedef int Length; //将Length定义为与int 具有同等意义的名字,可用于类型声明或类型转换
typedef char *String; //String定义为与char *即字符指针同义
typedef struct tnode{...} Treenode;//定义了一个新结构类型名
typedef struct tnode *Treeptr;//定义了一个指向该结构的指针
其在语法上类似于存储类extern、static等,(IT)新类型名出现在变量名处;可以首字母大写来区分typedef定义的类型;typedef声明并未创建任何新类型,只是为已存在类型增加一个新名称;它类似于#define,但它由编译器解释,所以她的文本替换能力超过后者,例如:
typedef int (*PFI)(char *, char *);//定义FEI是一个指向函数的指针,函数带两参数返回值为int
除简洁外,使用typedef的两个重要原因:
1)使程序参数化以提高可移植性,若它声明的类型与机器有关,移植时只需改变它即可;比如经常用到:对各种不同大小的整型值,都使用通过typedef定义的类型名,然后分别为各个宿主机选择一组合适的short、int、long类型大小即可;标准库中有size_t和ptrdiff_t等例子;
2)为程序提供更好说明,比如Treeptr类型显然比普通声明更易理解;
6.8 联合
联合是可以在不同时刻保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求;联合提供了无需机器信息下、在单块存储区管理不同类型数据的方式;
特定类型常量值必须保存在合适类型的变量里,但是如果该常量的不同类型占据相同大小的存储空间,且保存在同一个地方,表管理将最方便,这就是联合的目的:一个变量可以保存多种数据类型中任何一种类型的对象;
union u_tag{
int ival;
float fval;
char *sval
} u;
变量u的长度必须足够保存这3中类型中最大的一种(具体长度与实现有关);其中任何一个类型的对象都可赋给u;随后读取u的时候必须是最近一次存入的类型(程序员自己负责,可以用一个变量(如utype)来保存联合中的当前数据类型;),读取类型与保存类型若不一致,结果取决于具体的实现;
访问联合与访问结构的方式相同:
联合名.成员
联合指针->成员
联合可以使用在结构和数组中,反之亦可。访问方法与嵌套结构相同;例如以下的结构数组定义:
struct{
char *name;
int flags;
int utype;
union{
int ival;
float fval;
char *sval;
} u;
}symtab[NSYM];
则引用成员ival:
symtab[i].u.ival
引用sval的第一个字符:
*symtab[i].u.sval
symtab[i].u.sval[0]
联合实际上就是一个结构,它的所有成员相对于基地址的偏移量都为0,此结构空间要大到足够容纳最“宽”的成员,并且其对齐方式要适合于联合中所有的成员;
联合与结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中一个成员;
联合只能用其第一个成员类型的值进行初始化,例如上述只能用整数值初始化;
第8章的存储分配程序会说明如何使用联合来强制一个变量在特定存储边界上对齐;
6.9 位字段
存储空间很宝贵时,可能需要在一个机器字里保存多个对象。常用方法:使用类似于编译器符号表的单个二进制位标志集合;外部强加的数据格式(如硬件设备接口)也经常需要从字的部分位中读取数据;
编译器符号表的操作细节:对标识符信息(如是否关键字、外部的、静态的)进行编码,最简洁方法就是使用一个char或int对象中的位标志集合;通常,定义一个与位的位置对应的“屏蔽码”集合,例如:
#define KEYWORD 01
#define EXTERNAL 02
#define STATIC 04
或
enum{KEYWORD=01, EXTERNAL=02, STATIC=04};
这些数字必须是2的幂,这样访问它们就变成了第2章中描述的移位、屏蔽、补码等运算,例如:
flags |=EXTERNAL | STATIC;//将flags中这两位置1;
flags &=~(EXTERNAL | STATIC);//将flags中这两位置0;
if((flags & (EXTERNAL | STATIC))==0)... //测试这两个位置是否都为0
尽管这些方法很容易掌握,但C仍提供了另一种替代方法,可以直接访问一个字中的位字段而无需通过按位逻辑;位字段(bit-field),简称字段,是“字”中相邻位的集合;(“字”是单个存储单元,同具体实现有关)上述符号表的多个#define语句可用下列三个字段的定义来代替:(It: 位字段可用结构来实现)
struct{
unsigned int is_keyword:1; //保证它们是无符号量,冒号后面数字表示字段长度(二进制位数)
unsigned int is_extern:1;
unsigned int is_static:1;
} flags;
这样就定义了一个包含3个1位字段的变量flag;单个字段引用方式与其他结构成员相同;字段作用与小整数相似,也可出现在算术表达式中;所以前述例子可以表述为:
flags.is_extern=flags.is_static=1;//将这两个位置置为1
flags.is_extern=flags.is_static=0;
if(flags.is_extern==0 && flags.is_static==0)...
字段的所有属性几乎都与具体实现有关;字段能否覆盖字边界由具体实现定义;字段可不命名,无名字段(只有一个冒号和宽度)起填充作用;特殊宽度0可以用来强制在下一个字边界上对齐;
??尽管字段对维护内部定义的数据结构很有用,但在选择外部定义数据的情况下,必须仔细考虑哪端优先的问题(因为某些机器上字段分配从字的左端到右端,某些相反),依赖于此因素的程序不可移植;??字段也可仅仅声明为int(It:即struct和int都可实现位字段),需要显式声明该int型是signed还是unsigned;字段不是数组,并且没有地址,因此对它不能使用&运算符;