听说C语言很难?怎么不来看看我这篇(六)结构

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语言的其他的内容。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值