概述
结构是一个或多个变量的集合
,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下
由于结构将一组相关的变量
看作一个单元
而不是各自独立的实体,因此结构有助于组织复杂的数据
,特别是在大型的程序中
ANSI C标准在结构方面最主要的变化是定义了结构的赋值操作
——结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值(编译器早已支持,但标准才完成精确定义)
在ANSI C标准中,自动结构和数组
现在也可以进行初始化
了
1、结构的基本知识
struct point{
int x;
int y;
};
关键字struct
引入结构声明。结构声明
由包含在花括号内的一系列声明组成
struct后面的名字是可选的(不建议省略),称为结构标记
。结构标记用于为结构命名
,在声明之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式
结构中定义的变量称为成员
结构成员、结构标记和普通变量
可以采用相同的名字,它们之间不会冲突,因为通过上下文分析总可以对它们进行区分。不同结构中的成员可以使用相同的名字(通常只有密切相关的结构
才会使用相同的名字)
struct声明定义了一种数据类型
。在标志结构成员表
结束的右花括号之后可以跟一个变量表
(如果跟了变量表,那么编译器就会为这些结构实例
分配存储空间)
如果结构声明的后面不带变量表
,则不需要为它分配存储空间,它仅仅描述了一个结构的模板或轮廓
。如果结构声明中带有结构标记
,那么在以后定义结构实例
时便可以使用该结构标记
来定义。譬如:struct point pt;
结构的初始化
可以在定义的后面使用初值表
进行。初值表中同每个成员对应的初值必须是常量表达式
自动结构
也可以通过赋值初始化,还可以通过调用返回相应类型结构的函数进行初始化
什么是自动结构??????
结构成员运算符.
结构可以嵌套
如果没有使用typedef
重命名结构类型
,那么在定义结构实例的时候struct和结构标记
(结构声明的时候没有省略)必须同时存在(好像enum
也是这样)
2、结构与函数
结构的合法操作
只有几种:作为一个整体复制和赋值,通过&运算符取地址,访问其成员
复制和赋值包括向函数传递参数以及从函数返回值
结构之间不可以进行比较
可以用一个常量成员值列表
初始化结构,自动结构
也可以通过赋值进行初始化
至少可以通过3种可能的方法传递结构:一是分别传递各个结构成员,二是传递整个结构,三是传递指向结构的指针。3种方法各有利弊
//返回一个point类型的结构
struct point makepoint(int x, int y)
{
struct point temp;
temp.x = x;
temp.y = y;
return temp;
}
//参数名和结构成员同名不会引起冲突。事实上,使用重名可以强调两者之间的关系
结构类型的参数和其他类型的参数一样,都是通过值传递
的
矩形的标准表示形式
(点1在左下方,点2在右上方,用对角线上的两点来定义矩形
)
//矩形的规范化
#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))
struct rect{
struct point pt1;
struct point pt2;
};
struct rect canonrect(struct rect r)
{
struct tect 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;
}
如果传递给函数的结构很大,使用指针方式的效率通常比复制整个结构的效率要高
结构指针
的使用频度非常高,为了使用方便,C语言提供了另一种简写方式。假定p是一个指向结构的指针,可以用p->结构成员
来引用相应的结构成员
*p++->str
:先读取指针str指向对象的值,然后再将p加1(注意运算符优先级,也要注意运算符与操作数距离的远近)
3、结构数组
结构数组
的初始化是通过一个用花括号括起来的初值表
(表中的每一项都要为常量表达式
)来完成的
在结构数组的初始化过程中,与结构成员相对应,初值也要按照成对的方式列出。更精确的做法是,将每一行(即每个结构)的初值都括在花括号内
如果初值是简单变量或字符串
,并且其中的任何值都不为空,则内层的花括号可以省略(不建议省略)
结构数组的长度
由机器来计算将会更加的简单、安全
(尤其是初始化列表可能变化的时候)
//用来统计输入中各个C语言关键字出现的次数的程序
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
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},
{"register", 0},
{"while", 0}
};
#define NKEYS (sizeof(keytab) / sizeof(keytab[0]))
int getword(char *, int);
int binsearch(char *, struct key *, int);
int main(int argc, char ** argv)
{
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;
}
int binsearch(char *word, struct key tab[], int n)
{
int cond;
int low, high, mid;
low = 0;
high = n - 1;
while(low <= high)
{
mid = (high + low) / 2;
if((cond = strcmp(word, tab[mid].word)) < 0)
high = mid - 1;
else if(cond > 0)
low = mid + 1;
else
return mid;
}
return -1;
}
//关于getword函数更多的解释见p118
int getword(char *word, int lim)
{
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(!isalnum(*w = getch()))
{
ungetch(*w);
break;
}
*w = '\0';
return word[0];
}
数组的长度
在编译时
已经完全确定了
C语言提供了一个编译时(compile-time)
一元运算符sizeof
,它可用来计算任一对象的长度。(sizeof 对象
或sizeof(类型名)
)将返回一个无符号整型值(size_t
类型的,在<stddef.h>
中定义),它等于指定对象或类型占用的存储空间字节数
。
条件编译语句#if
不能使用sizeof
,因为预处理器不对类型名进行解析
。但预处理器并不计算#define
语句中的表达式,因此,在#define
中使用sizeof
是合法的
预处理器是计算条件编译语句
的,但是不计算宏定义语句
(只是执行替换操作)
4、指向结构的指针
为了进一步说明结构指针和结构数组
,重新编写关键字统计程序,这次采用指针
,而不使用数组下标
//用来统计输入中各个C语言关键字出现的次数的程序
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXWORD 100
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},
{"register", 0},
{"while", 0}
};
#define NKEYS (sizeof(keytab) / sizeof(keytab[0]))
int getword(char *, int);
struct key *binsearch(char *, struct key *, int);
int main(int argc, char ** argv)
{
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;
}
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;
}
//关于getword函数更多的解释见p118
int getword(char *word, int lim)
{
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(!isalnum(*w = getch()))
{
ungetch(*w);
break;
}
*w = '\0';
return word[0];
}
注意数组边界的问题
p86p120,C语言的定义保证数组末尾之后的第一个元素(即&tab[n]
)的指针算术运算可以正确执行(这是实际需要的)
千万不要认为结构的长度
等于各成员长度的和
。因为不同的对象有不同的对齐要求
,所以,结构中可能会出现未命名的“空穴(hole)”。使用sizeof运算符可以返回正确的对象长度
当函数的返回值类型比较复杂时,很难看出函数名,也不太容易使用文本编辑器找到函数名,所以建议将上面的形式写成下面的形式(只是建议而已,保持自己喜欢的风格就好)
struct key *binsearch(char *word, struct key *tab, int n)
struct key *
binsearch(char *word, struct key *tab, int n)
5、自引用结构
二叉树
中的任何节点最多拥有两个子树,也可能只有一个子树或一个都没有
一个包含其自身实例
的结构是非法的(包含自身实例的指针除外)
自引用结构
的一种变体:两个结构相互引用
struct t;
struct s;
struct t{
struct s *p;
};
struct s{
struct t *q;
};
统计输入中所有单词的出现次数的程序
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#define MAXWORD 100
struct tnode{
char *word;
int count;
struct tnode *left;
struct tnode *right;
};
struct tnode *addtree(struct tnode *, char *);
void treeprint(struct tnode *);
int getword(char *, int);
int main(int argc, char ** argv)
{
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;
}
struct tnode *talloc(void);
char *strdup(char *);
struct tnode *addtree(struct tnode *p, char *w)
{
int cond;
if(p == NULL)
{
p = talloc();
p->word = strdup(w);
p->count = 1;
p->left = p->right = NULL;
}
else if((cond = strcmp(w, p->word)) == 0)
p->count++;
else if(cond < 0)
p->left = addtree(p->left, w);
else
p->right = addtree(p->right, w);
return p;
}
void treeprint(struct tnode *p)
{
if(p != NULL)
{
treeprint(p->left);
printf("%4d %s\n", p->count, p->word);
treeprint(p->right);
}
}
struct tnode *talloc(void)
{
return (struct tnode *)malloc(sizeof(struct tnode));
}
char *strdup(char *s)
{
char *p;
p = (char *)malloc(strlen(s) + 1);
if(p != NULL)
strcpy(p, s);
return p;
}
talloc函数
分配新节点需要的存储空间,增加新节点的代码只在树叶部分
执行。strdup函数
将新单词复制到某个安全的位置
如果单词不是按照随机的顺序到达的,树将变得不平衡
,这种情况下,程序的运行时间
将大大增加。最坏的情况下,若单词已经排好序,则程序模拟线性查找的开销将非常大(某些广义二叉树
不受这种最坏情况的影响)。
一般来说,一个程序中只会有一个存储分配程序(即用一个存储分配程序来处理多种类型的请求),这样会出现两个问题:1、它如何在大多数实际机器上满足各种类型对象的对齐要求
(例如,整型通常必须分配在偶数地址上,为什么?????);2、使用什么样的声明能处理分配程序必须能返回不同类型的指针
对齐要求一般比较容易满足,只需要确保分配程序始终返回满足所有对齐限制要求
的指针就可以了,其代价是牺牲一些存储空间(标准库中的malloc函数能够满足对齐要求
)
为什么要有对齐要求?????????
对于任何执行严格类型检查
的语言来说,像malloc这样的函数的类型声明总是很令人头疼的问题。在C语言中,一种合适的方法是将malloc的返回值声明为一个指向void类型的指针,然后再显式地将指针强制转换为所需类型(在调用malloc函数的时候)
调用malloc函数
得到的存储空间可以通过调用free函数
释放以重用
malloc的其中一种实现p162
6、表查找
表查找程序
的核心代码,该算法采用的是hash查找方法
(散列查找方法)
struct nlist{
struct nlist *next;
char *name;
char *defn;
};
#define HASHSIZE 101
static struct nlist *hashtab[HASHSIZE];
unsigned hash(char *s)
{
unsigned hashval;
for(hashval = 0; *s != '\0'; s++)
hashval = *s + 31 * hashval;
return hashval % HASHSIZE;
}
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;
return NULL;
}
char *strdup(char *);
struct nlist *install(char *name, char *defn)
{
struct nlist *np;
unsigned hashval;
if((np = lookup(name)) == NULL)
{
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
free((void *)np->defn);
if((np->defn = strdup(defn)) == NULL)
return NULL;
return np;
}
如何设计hash函数
,这是数学专业的事情
7、类型定义(typedef)
C语言提供了一个称为typedef
的功能,它用来建立新的数据类型名
通过typedef建立的类型与原来的类型完全相同
,用该类型和原来类型定义的变量也具有完全相同的属性
typedef中声明的类型在变量名的位置出现,而不是紧接在关键字typedef之后(采用指针声明方式
的理解方法)
建议以大写字母作为typedef定义的类型名的首字母,以示区别
从任何意义上讲,typedef声明并没有创建一个新类型
,它只是为某个已存在的类型增加了一个新的名称
而已
typedef类似于#define语句,但由于typedef是由编译器解释的
,因此它的文本替换功能要超过预处理器的能力
typedef int (*PFI)(char *, char *);
定义了一个函数指针
除了表达方式更简洁
之外,使用typedef还有另外两个重要原因:1、它可以使程序参数化,以提高程序的可移植性;2、为程序提供了更好的说明性p128
8、联合
联合是可以(在不同时刻
)保存不同类型和长度的对象
的变量,编译器负责跟踪对象的长度和对齐要求
联合提供了一种方式,以在单块存储区域中管理不同类型的数据
,而不需要在程序中嵌入任何同机器有关的信息
联合的目的:一个变量可以合法地保存多种数据类型中任何一种类型的对象
联合的实例必须足够大,以保存其所包含的类型中最大的一种
,具体长度同具体的实现有关
从union读取的类型必须是最近一次存入的类型
。程序员负责跟踪当前保存在联合中的类型
如果保存的类型与读取的类型不一致,其结果取决于具体的实现
联合可以使用在结构和数组中,反之亦可。访问结构中的联合的某一成员的表示法与嵌套结构相同
实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量
都为0,此结构空间要大到足够容纳最“宽”的成员,并且,其对齐方式要适合于联合中所有类型的成员
对联合允许的操作与对结构允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中一个成员
联合只能用其第一个成员类型的值进行初始化
(赋值可以是其中的一种)
可以使用联合来强制一个变量在特定类型的存储边界上对齐(malloc的一种实现)
9、位字段
在存储空间很宝贵的情况下,有可能需要将多个对象保存在一个机器字上
常用的方法:二进制位标志集合、位字段
二进制位标志集合
需要用到移位运算
、屏蔽运算
及补码运算
等简单的位操作
C语言提供了另一种可替代的方法(位字段,或简称为字段):直接定义和访问一个字中的位字段的能力,而不需要通过按位逻辑运算符
位字段是“字”中相邻位的集合
。“字”是单个的存储单元
,它同具体的实现有关
什么是机器字长?????????
struct{
unsigned int is_keyword : 1;
unsigned int is_extern : 1;
unsigned int is_static : 1;
}flags;
这里定义了一个变量flags,它包含3个一位的字段
。冒号后的数字表示字段的宽度
(用二进制位数表示)。字段被声明为unsigned int类型,以保证它们是无符号量
单个字段的引用方式与其他结构成员相同。字段的作用与小整数相似
字段的所有属性几乎都同具体的实现有关。字段是否能覆盖字边界
由具体的实现定义。字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界
上对齐
某些机器上字段的分配时从字的左端至右端进行的,而某些机器上则相反。这意味着,尽管字段对维护内部定义的数据结构
很有用,但在选择外部定义的数据
的情况下,必须仔细考虑哪端优先的问题。依赖于这些因素的程序是不可移植的(处理外部数据的情况)
字段不是数组,并且没有地址
,因此对它们不能使用&运算符