The C Programming Language——结构

概述

结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下

由于结构将一组相关的变量看作一个单元而不是各自独立的实体,因此结构有助于组织复杂的数据,特别是在大型的程序中

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可以用来强制在下一个字边界上对齐

某些机器上字段的分配时从字的左端至右端进行的,而某些机器上则相反。这意味着,尽管字段对维护内部定义的数据结构很有用,但在选择外部定义的数据的情况下,必须仔细考虑哪端优先的问题。依赖于这些因素的程序是不可移植的(处理外部数据的情况)

字段不是数组,并且没有地址,因此对它们不能使用&运算符

猜你喜欢

转载自blog.csdn.net/weixin_39918693/article/details/80619451