第14章 结构和其他数据形式(3)

发布于 2021-10-19  50 次阅读


14.7.10 匿名结构(C11)

匿名结构是一个没有名称的结构成员,为了理解它的工作原理,线考虑如何创建嵌套结构:
struct names{
    char first[20];
    char last[20];
};
struct person{
    int id;
    struct names name;  //嵌套结构成员
};
struct person ted = {8483,{"Ted","Grass"}};
//这里,name成员是一个嵌套结构,可以通过类似ted.name.first的表达式访问”ted“:
puts(ted.name.first);
//在C11中,可以用嵌套的匿名函数成员结构定义person:
struct person
{
    int id;
    struct {char first[20];char last[20];}; //匿名结构
}
//初始化ted的方式相同:
struct person ted = {8483,{"Ted","Grass"}};
//但是,在访问ted时简化了步骤,只需把first看作person的成员那样使用它:
puts(ted.first);
//当然也可以把first和last直接化为person的成员,删除嵌套循环。匿名特性在嵌套联合中更加有用。

14.7.11 使用结构数组的函数

假设一个函数要处理一个结构数组。由于数组名就是该数组的地址,所以可以把它传递给函数。
另外,该函数还需访问结构模板,为了理解该函数的工作原理,下面程序把前面的金融扩展为两人,所以需要一个内含两个funds结构的数组。
//funds4.c          --把结构数组传递给函数
#include<stdio.h>
#define FUNDLEN 50
#define N 2

struct funds{
    char bank[FUNDLEN];
    double bankfund;
    char save[FUNDLEN];
    double savefund;
}; 

double sum(const struct funds money[], int n);

int main(void)
{
    struct funds jones[N] ={
        {"Garlic-Melon Bank",
          4032.27,
         "Lucky's Savings and Loan",
         8543.94    
        },
        {"Honest Jack's Bank'",
          3620.88,
         "Party Time Savings",
         3802.91
        }
    };
    printf("The Joneses have a total of $%.2f.\n",sum(jones,N));

    return 0;
}
double sum(const struct funds money[],int n)
{
    double total;
    int i;

    for(i=0,total = 0;i<n;i++)
        total+=money[i].bankfund + money[i].savefund;
    return (total);
}

file

数组名jones是该数组的地址,即该数组的首元素(jones[0])的地址。因此,指针money的初始值相当于通过下面的表达式获得:
money = &jones[0];
因为monmey指向jones数组首元素,所以money[0]是该数组的另一个名称。与此类似,money[1]是第2个元素。每个元素都是一个funds类型的结构,所以都是使用点运算(.)来访问funds类型结构的成员。
下面是几个要点:
- 可以把数组名作为数组中第1个结构的地址传递给函数
- 然后可以用数组表述法访问数组中的其他结构。注意下面的函数调用于使用数组名的效果相同;
sum(&jones[0],N)
因为jones和&jones[0]的地址相同,使用数组名是传递结构地址的一种间接方法。
- 由于sum()函数不能改变原始数据,所以该函数使用了ANSI C 的限定符const。

14.8 把结构内容保存到文件中

由于结构可以存储不同类型的信息,所以它是构建数据库的重要工具。
例如,可以用一个结构储存雇员或汽车零件的相关信息,最终,我们要把这些信息存储在文件中,并且能再此检索。数据库文件可以包含任意数量的此类数据对象。储存在一个结构中的嵌套信息被称为记录(record),单独的项被称为字段(field)。
或许存储记录最没效率的方法是用fprintf()。
#define MAXTITL 40
#define MAXAUTL 40
struct book{
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
};
如果pbook表示一个文件流,那么通过下面这条语句可以把信息储存在struct book类型的结构变量primer中:
fprintf(pbooks,"%s %s %.2f\n",primer.title,primer.author,primer.value);
对于一些结构(如有多个成员的结构),这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(%39s%39s%8.2f),但是这个办法仍然很笨拙。
更好的方案是使用fread()和fwrite()函数读写结构大小的单元。这两个函数使用与程序相同的二进制表示法:例如:
fwrite(&primer,sizeof(struct book),1,pbook);
//定位到primer结构变量开始的位置,并把结构中所有的字节都拷贝到与pbooks相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小。1表明一次拷贝一块数据。带相同参数的fread()函数从文件中拷贝一块结构大小的数据&primer指向的位置。简而言之这两个函数一次读写整个记录,而不是一个字段。
//以二进制表示法存储数据的缺点是,不同系统可能使用不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一个系统,不同编译器设置也可能导致不同的二进制布局。

14.8.1 保存结构的程序示例

为了演示如何在程序中使用这些函数,我们把下述程序修改称为一个新的版本,把书名豹祠内在book.dat文件中。如果该文件已经存在,该程序将显示它当前的内容,然后允许在文件中添加内容。
//booksave.c        ---在文件中保存结构中的内容
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS  10  //最大书籍量
char *s_gets(char *st,int n);
struct book{
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
}; 
int main(void)
{
    struct book library[MAXBKS];    //结构数组
    int count = 0;
    int index,filecount;
    FILE *pbooks;
    int size = sizeof(struct book);

    if((pbooks = fopen("book.dat","a+b")) == NULL)
    {
        fputs("Can't open book.dat file\n",stderr);
        exit(1);
    } 
    rewind(pbooks);     //定位到文件开始处
    while(count < MAXBKS && fread(&library[count],size,1,pbooks) ==1)
    {
        if(count == 0)
            puts("Current contents of book.dat:");
        printf("%s by %s:$%.2f\n",library[count].title,library[count].author,library[count].value);
        count ++;       
    } 
    filecount = count;
    if(count == MAXBKS)
    {
        fputs("The book.dat file is full.",stderr);
        exit(2);
    }
    puts("Please add new book titles.");
    puts("Please [enter] at the start of a line to stop");
    while(count<MAXBKS && s_gets(library[count].title,MAXTITL)!=NULL && library[count].title[0]!='\0')
    {
        puts("Now enter the author:");
        s_gets(library[count].author,MAXAUTL);
        puts("Now enter the value.");
        scanf("%f",&library[count ++].value);
        while(getchar()!='\n')
            continue;
        if(count < MAXBKS)
            puts("Enter the next title.");
    }
    if(count > 0)
    {
        puts("Here is the list of your books:");
        for(index = 0;index < count;index++)
        {
            printf("%s by %s:$%.2f\n",library[index].title,library[index].author,library[index].value);
        }
        fwrite(&library[filecount],size,count-filecount,pbooks);
    }
    else
        puts("No books?Too bad.\n");
    puts("Bye!\n");
    fclose(pbooks);

    return 0;
}
char *s_gets(char *st,int n)
{
    char *ret_val;
    char *find;

    ret_val = fgets(st,n,stdin);
    if(ret_val)
    {
        find = strchr(st,'\n');
        if(find)
            *find = '\0';
        else
            while(getchar() != '\n')
                continue;
    }
    return ret_val;
}

file

14.8.2 程序要点

首先,以“a+b”模式打开文件,a+部分允许程序读取整个文件的末尾添加内容。
b是ANSI  的一种标识方法,表明程序将使用二进制文件格式。对于不接受b模式的UNIX系统,可以省略b,这是因为UNIX只有一种文件形式,对于早期的ANSI实现,要找出和b等价的表示法。
我们选择二进模式是因为fread()和fwrite()函数要使用二进制文件。虽然结构中有些内容是文本,但是value成员不是文本。如果使用文本编辑器查看book.dat,该结构本文部分的内容显示正常,但是数值部分的内容不可读,甚至可能会导致文本编辑器出现乱码。
rewind()函数确保文件指针位于文件开始处,为读文件做好准备。
第1个while循环每次把一个结构督导结构数组中,当数组已满或读完文件时停止。变量filecount统计已读结构的数量。
第2个while按下循环提示用户进行输入,并接受用户的输入。和程序清单x一样,当数组已满或用户在一行的开始处按下Enter键时,循环结束,注意,该循环开始时的count变量的值是第1个循环结束后的值。该循环把新输入项添加到数组的末尾。
然后for循环打印文件和用户输入的数据,因为该文件是以附加模式打开,所以新写入的内容添加到文件现有内容的末尾。
本可以用一个循环在文件末尾添加一个结构,但还是决定用fwrit()一次写入一块数据。对表达式count-filecount求值的带需兵团变化的书籍数量,然后调用fwrite()把结构大小的块写入文件,由于表达式&library[filecount]是数组中第1个新结构的地址,所以拷贝就从这里开始。
这种方法浪费及存储空间。
还保存了结构中未使用的部分,该结构的大小是2×40×sizeof(float)
解决办法:使用链表。

14.9 链式结构

结构的多种用途之一:创建新的数据形式。计算机用户已经开发出的一些数据形式比我们提到过的数组和简单结构更有效地解决特定的问题。
这种形式包括队列、二叉树、堆、哈希表和图表。这样的形式都由链式结构(linked structure)组成。通常,每个结构都包含一两个数据项和一两个指向其他同类结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。例如,

file

上图显示的分级或树状的结构是否比数组高效?考虑一个有10个节点的树的情况。它有2^10-1个节点,可以存储1023个单词。如果这些单词以某种规律排列,那么可以从最顶层开始,逐级向下移动查找单词,最多只需移动9次便可找到任意单词。如果把这些单词都放在一个数组中,最到要查找1023个元素才能找出所需的单词。

14.10 联合简介

联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合大小都相等,每个联合可以储存各种数据类型。
创建联合和创建结构的方式相同,需要一个联合模板和联合变量。可以用一个步骤定义联合,也可以用联合标记分两步定义。下面是一个带标记的联合模板:
union hold{
    int digit;
    double bigfl;
    char letter;
};
根据以上形式声明的结构可以存储一个int类型、一个double类型和char类型的值,然而,声明的联和只能储存一个int类型的值或者一个double类型的值或者char类型的值。
下面定义了3个与hold类型相关的变量:
union hold fit; //hold类型的联合变量
union hold save[10];    //内含10个联合变量的数组
union hold *pu; //指向hold类型联合变量的指针
第一个声明创建了一个单独的联合变量fit。编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型。在本例中,占用空间最大的是double类型的数据,在我们的系统中,double类型占64位,即8字节。第2个声明创建了一个数组save,内含10个元素,每个元素都是8个字节。第3个声明创建了一个指针,该指针变量储存hold类型联合变量的地址。
可以初始化联合。需要注意的是。联和只能储存一个值,这与结构不同。有3中初始化方法;把一个联合初始化为另一个同类型的联合;初始化联合的第1个元素;或者根据C99标准,使用指定初始化器:
union hold valA;
valA.letter = 'R';
union hold valB = valA; //用另一个联合来初始化
union hold valC = {88}; //初始化联合的dight成员
union hold valD = {.bigfl = 118.2}; //指定初始化器

14.10.1 使用联合

下面是联合的一些用法:
fit.dight = 23;         //把23储存在fit中,占2个字节
fit.bigfl = 2.0;        //清除23,储存2.0,占用8个字节
fit.letter = 'h'      //清除2.0,储存h,占1字节
点运算符表示正在使用哪种数据类型。在联合中,一次只存储一个值。即使有足够的空间,也不能同时存储一个char类型的值和一个int类型的值。编写代码时要注意当前储存在联合中的数据类型。
利用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:
pu = &fit;
x = pu ->dight;      //相当于x = fit.digit
不要像下面的语句序列这样:
fit.letter = 'A';
flnum = 3.02*fit.bigfl; //错误
以上语句序列是错误的,因为储存在fit中的是char类型,但是下一行确假定fit中的内容是double类型。
不过,用一个成员把值储存在一个联合中,然后用另外一个成员查看内容,这种做法有时很有用:
联合的另一种用法是,在结构中储存与其成员有错从属关系的信息。例如,假设用一个结构表示一辆汽车。如果汽车属于驾驶者,就要用一个结构来描述这个所有者。如果汽车被租凭,那么需要一个成员描述其租凭公司。可以用如下代码完成:
struct ower{
    char socsecurity[12];
    ...
}
struct leasecompany{
    char name[40];
    char headquarters[40];
    ...
};
union data{
    struct ower owncar;
    struct leasecompany leasecar;
};
struct car_data{
    char make[15];
    int status;     //私有为0,租凭为1
    union data ownerinfo;
    ...
};
假设flits是car_data类型的结构变量,如果flits.status为0,程序将使用flits.ownerinfo.owncar.socsecurity,如果flits.status为1,程序则使用flits.ownerinfo.leasecar.name。

14.10.2 匿名联合(C11)

匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。例如,我们重新定义car_data结构如下:
struct owner{
    char socsecurity[12];
    ...
};
struct leasecompany{
    char name[40];
    char headquarters[40];
    ...
};
struct car_data{
    char make[15];
    int status; //私有为0,租凭为1
    union{
        struct owner owncar;
        struct leasecompany leasecar;
    };
    ...
};
现在,如果flits是car_data类型的结构变量,可以用flits.owncar.socsecurity代替flits.ownerinfo.owncar.socsecurity。
总结:结构和联合运算符
    成员运算符:.
    一般注释:
    该运算符与结构变量或联合变量名一起使用,指定结构变量或联合变量的一个成员。如果name是一个结构变量的名称,member是改结构的模板指定的一个成员名,下面标识了改结构变量的这个成员:
    name.member
    name.member的类型就是member的类型。联合使用成员运算符的方式与结构相同。
    示例:
    struct{
        int code;
        float cost;
    }item;
    item.code = 1265;
间接成员运算符:->
一般注释:
该运算符和指向结构或联合的指针一起使用,标识结构变量或联合变量的一个成员。假设ptrstr是指向结构的指针,member是该结构模板指定的一个成员,那么:
 ptrstr ->member
标识了指向结构的成员。联合使用间接成员运算符与结构相同。
示例:
struct {
    int code;
    float cost;
}item,*ptrst;
    ptrst = &item;
    ptrst ->code = 3451;
最后一条语句把一个int类型的值赋给item的code成员,如下3个表达式是等价的:
ptrst -> code
item.code
(*ptrst).code

14.11 枚举类型

可以用枚举类型(enumerated type)声明符号名称来标识整型常量。使用enum关键字,可以创建一个新“类型”并指定它可具有的值(实际上,enum常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同,例如,可以这样声明:
enum spectrum(red,orange,yellow,green,blue,violet);
enum spectrum color;
第1个声明创建了spetrum作为标记名,允许把enum spetrum作为一个类型名使用。第2个声明使color作为该类型的变量。第1个声明中花括号内的标识枚举了spectrum变量可能有的值。
因此,color可能的值是red、orange、yellow等。这些符号常量被称为枚举符(enumerator)。然后,便这样子使用:
    int c;
    color = blue;
    if(color == yellow)
        ...;
    for(color =red; color<= violet;color++)
        ...;
虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如,spectrum的枚举符范围是0~5,所以编译器可以用unsigned char 来表示color变量。
顺带一提,C枚举的一些特征并不适用于C++,例如,C允许枚举变量使用++运算符,但是C++标准不允许。所以,如果编写的代码将来会并入C++程序,那么必须把上面例子中的color声明为int类型,才能C和C++都兼容。

14.11.1 enum常量

blue和red到底是什么?
从技术层面上看,它们是int类型的常量。例如,假定有前面的枚举声明,可以这样写:
printf("red = %d,orange = %d\n",red,orange);
其输出如下:
red = 0,orange = 1
red作为一个有名称的常量,代表整数0,类似的,其他标识符都是有名称的常量,分别代表1~5.
只要是能使用整型常量的地方就可以使用枚举常量。例如,在声明数组时,可以枚举常量表示数组的大小,在switch语句中,可以把枚举常量作为标签。

14.11.2 默认值

默认情况下,枚举列表中的常量都被赋予0、1、2等。因此,下面的声明中nina的值是3:
enum kids(nippy,slats,skippy,nina,liz);

14.11.3 赋值

在枚举声明中,可以为枚举常量指定整数值:
enum levels {low = 100,medium = 500,high = 2000};
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。例如:
enum feline {cat,lynx =10,puma,tiger};
那么,cat的值是0(默认),lynx、puma和tiger的值分别是10、11、12.

14.11.4 enum的用法

枚举类型的目的是为了提高程序的可读性和可维护性。如果要处理颜色,使用red和blue比使用0和1更直观。注意,枚举类型只能在内部使用。如果要输入color和orange的值,只能输入1,而不是单词orange。或者,让程序先读入字符串“orange”,再将其转换为orange代表的值。
因为枚举类型是整数类型,所以可以再表达式中以使用整数变量的方式使用enum变量。它们用在case语句中很方便。
下述程序演示了一个使用enmu的小程序。该程序示例使用默认值的方案,把red的值设置为0,使之成为指向字符串“red”的指针的索引。
//enum.c    --使用枚举类型的值
#include<stdio.h>
#include<string.h>    //strcmp(),strchr()
#include<stdbool.h>   //C99特性
char *s_gets(char *st,int n);

enum spectrum{
    red,orange,yellow,green,blue,violet
}; 
const char *colors[] = {"red","orange","yellow","green","blue","violet"};
#define LEN 30

int main(void)
{
    char choice[LEN];
    enum spectrum color;
    bool color_is_found = false;

    puts("Enter a color(empty line to quit):");
    while(s_gets(choice,LEN)!= NULL &&choice[0]!='\0')
    {
        for(color = red;color<=violet;color++)
        {
            if(strcmp(choice,colors[color]) == 0)
            {
                color_is_found = true;
                break;
            }
        }
        if(color_is_found)
            switch(color)
            {
                case red: puts("Roses are red.");
                    break;
                case orange: puts("Poppies are orange.");
                    break;
                case yellow: puts("Sunflowers are yellow.");
                    break;
                case green: puts("Grass is green.");
                    break;
                case blue:puts("Sky is blue.");
                    break;
                case violet:puts("Violets are violet.");
                    break;

            }
            else
                printf("I don't know about the color %s.\n",choice);
            color_is_found = false;
            puts("Next color,please(empty line to quit):"); 
    }
    puts("Goodbye!");

    return 0;

}
char *s_gets(char *st,int n)
{
    char *ret_val;
    char *find;

    ret_val = fgets(st,n,stdin);
    if(ret_val)
    {
        find = strchr(st,'\n');
        if(find)
            *find = '\0';
        else
            while(getchar()!='\n')
                continue;
    }
    return ret_val;
}

file

当输入的字符串与color数组的成员指向的字符串相匹配时,for循环结束。如果循环找到匹配的颜色,程序就用枚举变量的值与作为case标签的枚举常量匹配。

14.11.5 共享名称空间

C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别。作用域是名称空间概念的一部分:两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突,名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同名称空间,该名称空间与普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同。不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。
例如,在C中,下面的代码不会产生冲突:
struct rect {double x;double y;};
int rect;   //在C中不会产生冲突。
尽管如此,以两种不同方式使用相同的标识符会造成混乱。另外,C++不允许这样做。因为它把标记名和变量名放在相同的名称空间中。

14.12 typedef 简介

typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:
- 与#define不同,typedef创建的符号名只受限于类型,不能用于值。
- typedef由编译器解释,不是预处理器
- 在其受限范围内,typedef比#define更灵活
typedef的工作原理:
假设要用BYTE表示1字节的数组。只需像定义个char类型变量一样定义BYTE,然后在定义前面加上关键字typedef即可。
typedef unsigned char BYTE;
随后,便可以使用BYTE来定义变量:
BYTE x,y[10],*z;
该定义的作用域取决于typedef定义所在的位置。如果定义在函数中,就具有局部作用域,受限于定义所在的函数。如果定义在函数外面,就具有文件作用域。
通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号的缩写。当然也可以用小写。
typedef unsigned char byte;
typedef中使用的名称遵循变量的命名规则。
为现有的类型创建一个名称,看上去真是多次一举,但是它有时候的确很有用。在前面的示例中,用BYTE代替unsigned char 表明你打算用BYTE类型的变量表示数字,而不是字符码。使用typedef还能提高程序的可移植性。
例如:我们之前提到的sizeof运算符的返回类型:size_t类型。以及time()函数的返回类型:time_t类型。
C标准规定siezof和time()返回整数类型。但是让实现来决定具体是什么整数类型,其原因是,C标准委员会认为没有哪个类型对于所有的计算机平台都是最优选择。所以,标准委员会决定简历一个心的类型名,如(time_t),并让实现使用typedef来设置它的具体类型。以这样的方式,C标准提供以下通用原型:
time_t time(time_t *);
time_t 在一个系统中是unsigned long,在另外一个系统中可以是unsigned long long。只要包含time.h头文件,程序就能访问合适的定义,你也可以在代码中声明time_t类型的变量。
typedef的一些特征与#define的某些功能重合。例如:
#define BYTE unsigned char
这使预处理器BYTE代替unsigned char,但是也有#define没有的功能:
typedef char *STRING;
没有typedef关键字,编译器将把STRING识别为一个指向char的指针变量。有了typedef关键字,编译器则把STRING解释成一个类型的标识符,该指针实是指向char的指针。
因此:
STRING name,sign;
相当于:
char *name,*sign;
但是,如果这样子假设:
#define STRING char *
然后,下面的声明:
STRING name,sign;
被翻译成
char *name,sign;
这导致只要name才是指针。
还可以把typedef用于结构:
typedef struct complex{
    float real;
    float imag;
}COMPLEX;
然后便可使用COMPLEX类型代替complex结构来表示复数。使用typedef的第1个原因是:为经常出现的类型创建一个方便、易识别的类型名,例如:前面的例子中,许多人更倾向于使用STRING或于其等价的标记。
用typedef来命名一个结构类型时,可以省略该结构的标签:
typedef struct{
    double x;
    double y;
}rect;
假设这样使用typedef定义的类型名:
rect r1 = {3.0,6.0};
rect r2;
以上代码将被翻译成:
struct {
    double x;
    double y;
}r1 = {3.0,6.0};
struct {
    double x;
    double y;
}r2;
r2 = r1;
这两个结构在声明时都没有标记,但它们的成员完全相同(成员名及其类型都匹配),C认为这两个结构的类型相同,所以r1和r2间的赋值是有效操作。
使用typedef的第二个原因是:typedef常用于给复杂的类型命名。例如,下面的声明:
typedef char (*FRPTC())[5];
把FRPTC声明成一个函数类型,该函数返回一个指针,该指针内含5个char类型的数组。
使用typedef时要记住,typedef并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签,以前面的STRING为例,这意味着我们创建的STRING类型的变量可以作为实参传递给以指向char指针作为形参的函数。
通过结构、联合和typedef,C提供了有效的处理工具和处理可以之数据的工具。

14.13 其他复杂的声明

C允许用户定义数据形式。虽然我们常用的是一些简单的形式,但是根据需要又是还会使用到一些复杂的形式。在一些复杂的声明中,常包含下面的符号:
符号      含义
*       表示一个指针
()      表示一个函数
[]      表示一个数组
下面是一些复杂的声明示例:
int board[8][8];    //声明一个内含int数组的数组
int **ptr;          //声明一个指向指针的指针,被指向的指针指向int
int *risk[10]       //声明一个内含10个元素的数组。每个元素都是一个指向int的指针
int (*rusks)[10]    //声明一个指向数组的指针,该数组内含10个int类型的值
int *off[3][4]; //声明一个3×4的二维数组,每个元素都是指向int的指针
int (*uuf)[3][4];   //声明一个指向3×4二维数组的指针,该数组内含int类型值
int (*uof[3])[4];   //声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
要看懂以上声明,关键要理解*、()和[]的优先级。记住下面几条规则。
1. 数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高。因此下面声明的risk是一个指针数组,不是指向数组的指针:
int *risks[10];
2. []和()的优先级相同,由于都是从做往右结合,所以下面的声明中,在应用方括号之前,*先于rusks结合。因此rusks是一个指向数组的指针,该数组内含10个int类型的元素。
int (*rusls)[10];
3. []和()都是从左向右结合。因此下面声明的goods是一个由12个内含50个int类型值的数组组成的二维数组,不是一个有50个内含12个int类型值的数组组成的二维数组:
int goods[12][50];
把以上规则应用于下面的声明:
int *off[3][4];
[3]比*的优先级高,由于从左往右结合,所以[3]先与off结合。因此,oof首先是一个内含3个元素的数组然后再与[4]结合,所以oof的每个元素都是内含4个元素的数组。*说明这些元素都是指针。最后,int表明了这4个元素都是指向int类型的指针,因此,这条表明要表达的是:foo是一个内含3个元素的数组,其中每个元素都是由4个指向int类型的指针组成的数组。简而言之,oof是一个3×4的二维数组,每个元素都是指向int的指针,编译器要为12个指针预留存储空间。
来看下面的声明:
int (* uuf)[3][4];
圆括号使得*先与uuf结合,说明uuf是一个指针,所以uuf是一个指向3×4的int类型二维数组的指针。编译器要为一个指针预留存储空间。
圆括号使得*与uuf结合,说明uuf是一个指针,所以uuf是一个指向3×4的int类型的二维数组的指针,编译器要为一个指针预留存储空间。
根据这些规则,还可以声明:
char *fump(int);    //返回字符串指针的函数
char (*frump)(int); //指向函数的指针,该函数的返回类型为char
char (*flump[3])(int);  //内含3个指针数组,每个指针都指向返回类型为char的函数
这3个函数都接受int类型的参数。
可以使用typedef建立一系列相关类型:
typedef int arr5[5];
typedef arr5 *p_arr5;
typedef p_arr5 arrp10[10];
arr5 togs;      //togs是一个内含5个int类型值的数组
p_arr5 p2;      //p2是一个指向数组的指针,该数组内含5个int类型的值
arrp10 ap;      //ap是一个内含10个指针的数组,每个指针都指向一个内含5个int类型值的数组
如果把这些放入结构中,声明会更复杂。

14.14 函数和指针

可以声明一个指向函数的指针。
通常,函数指针常常用作另一个函数的参数,告诉该函数要使用哪一个函数,例如,排序数组涉及比较两个元素,以确定先后。如果元素是数字,可以使用>运算符;如果元素是字符串或结构,就要调用函数进行比较。
C库中的qsort()函数可以处理任何类型的数组,但是要告诉qsort()使用哪个函数来比较元素。为此,qsort()函数的参数列表中,有一个参数接受指向函数的指针。然后,qsort()函数使用该函数提供的方案进行排序。无论这个数组中的元素是整数、字符串还是结构。
函数指针?
假设有一个指向int类型变量的指针,该指针储存着这个int类型变量存储在内存位置的地址。同样,函数也有地址,因为函数的及其语言实现由载入内存的代码组成,指向函数的指针中储存着函数代码的起始处的地址。
其次,声明一个数据指针时,必须声明指针所指向的数据类型。声明一个函数指针时,必须声明指针指向的函数的类型。为了指明函数类型,要指明函数签名,即函数的返回值和参数类型。例如:
void ToUpper(char *);       //把字符串中的字符转换成大写字符
ToUpper()函数的类型是"带char *类型参数、返回类型void的函数。"下面声明了一个指针pf指向该函数类型:
void (*pf)(char *); //pf是一个指向函数的指针
从该声明可以看出,第1对圆括号把*和pf括起来,表明pf是一个指向函数的指针。因此,(*pf)是一个参数列表为(char *)、返回类型为void的函数。注意,把函数名ToUpper替换为表达式(*pf)是创建指向函数指针最简单的方式。所以,如果想要声明一个指向某类型函数的指针,可以写出该函数原型后,把函数名替换成(*pf)形式的表达式,创建函数指针声明,由于运算符优先级的规则,在声明函数指针时必须把*和指针名括起来。
void *pf(char *);   //pf是一个返回字符指针的函数
注意:
要表明一个指向特定类型函数的指针、可以先声明一个该类型的函数,然后把函数名替换成(*pf)形式的表达式,然后,pf就成为指向该类型函数的指针。
声明了函数指针后,可以把类型匹配的函数地址赋给它。在这种上下文中,函数名可以用于表达函数的地址:
void ToUpper(char *);
void ToLower(char *);
int round(double);
void (*pf)(char *);
pf = ToUpper;       //有效,ToUpper是该类型函数的地址
pf = ToLower;       //有效,ToUpper是该类型函数的地址
pf = round;         //无效,round与指针类型不匹配
pf = ToLower();     //无效,ToLower()不是地址
最后一条语句是无效的,不仅是因为ToLower()不是地址、而且ToLower()返回类型是void,它没有返回值,不能再赋值语句中进行赋值。注意,指针pf可以指向其他带char *类型的参数、返回类型是void的函数。不能指向其他类型的函数。
既然可以用数据指针访问数据,也可以用函数指针访问函数。奇怪的是,有两种逻辑上不一致的语法void的函数,不能指向其他类型的函数。
void ToUpper(char *);
void ToLower(char *);
void (*pf)(char *);
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis);     //把ToUpper作用于(语法1)
pf = ToLower;
pf(mis);        //把ToLower作用于 (语法2)
这两种方法看上去都合情合理。
先分析第一种语法:
由于pf指向ToUpper函数,那么*pf就相当于ToUpper函数,所以表达式(*pf)ToUpper(mis)和ToUpper(mis)相同。从ToUpper函数和pf的声明就能看出,ToUpper和(*pf)是等价的。
第2种方法:由于函数名是组指针,那么指针和函数名可以互换使用,所以pf(mis)和ToUpper(mis)相同。从pf的赋值表达式语句就能看出ToUpper和pf是等价的。
作为函数的参数是数据指针最常见的用法之一,函数指针亦是如此。例如,考虑下面的函数原型:
void show(void (*fp)(char *),char *str);
这看上去很绕,它声明了两个形参:fp和str。fp形参是一个函数指针,str是一个数据指针,更具体的说,fp指向的函数接受char *类型的参数,其返回类型是void;str指向一个char类型的值,因此,假设有上面的声明,可以这样调用函数:
show(ToLower,mis);      // show()使用ToLower()函数:fp = ToLower;
show(pf,mis);       //show()使用pf指向的函数:fp = pf
show()如何使用传入的函数指针?是用fp()语法还是(*fp)()语法调用函数:
void show(void (*fp)(char *),char *str)
{
    (*fp)(str);     //把所选函数作用于str
    puts(str);      //显示结果
}
例如,这里的show()首先用fp指向的函数转换成 str,然后显示转换后的字符串。
顺带一提,把带返回值的函数作为参数传递给另一个函数有两种不同的方法,例如,考虑下面的语句:
function1(sqrt);    //传递sqrt()函数的地址
funcition2(sqrt(4.0));      //传递sqrt()函数的返回值。
第1条语句传递的是sqrt()函数的地址,假设function1()在其代码种会使用该函数,第2条语句种先调用sqrt()函数,然后求值,并把返回值传递给function2().
//通过show()函数来演示这些要点,该函数以各种转换函数作为参数。该程序也演示了一些处理菜单的有用技巧。
//func_ptr.c    --使用函数指针
#include<stdio.h>
#include<string.h>
#include<ctype.h>
#define LEN 81
char *s_gets(char *st,int n);
char showmenu(void);
void eatline(void);     //读取至行末尾
void show(void(*fp)(char *),char *str); 
void ToUpper(char *);   //把字符串转换成大写
void ToLower(char *);   //把字符串转换成小写
void Transpose(char *); //大小写转置
void Dummy(char *);     //不更改字符串

int main(void)
{
    char line[LEN];
    char copy[LEN];
    char choice;
    void (*pfun)(char *);       //声明一个函数指针,被指向的函数接受char *类型的参数,无返回值

    puts("Enter a string (empty line to quit):");
    while(s_gets(line,LEN) != NULL && line[0]!='\0')
    {
        while((choice = showmenu())!='n')
        {
            switch(choice)
            {
                case 'u':pfun = ToUpper;
                            break;
                case 'l':pfun = ToLower;
                            break;
                case 't':pfun = Transpose;
                            break;
                case 'o':pfun = Dummy;
                            break;
            }
            strcpy(copy,line);  //为show()函数拷贝一份
            show(pfun,copy); 
        }
        puts("Enter a string(empty line to quit):");
     } 
     puts("Bye!");

     return 0;
}
char showmenu(void)
{
    char ans;
    puts("Enter menu choice:");
    puts("u) uppercase     1) lowercase");
    puts("t) transposed        o) original case");
    puts("n) next string");
    ans = getchar();        //获取用户的输入
    ans = tolower(ans);     //转换为小写
    eatline();              //清理输入行
    while(strchr("ulton",ans)==NULL)
    {
        puts("Please enter a u,    l,  t,  o,  or n:");
        ans = tolower(getchar());
        eatline();
    } 
    return ans;
 } 
 void eatline(void)
 {
    while(getchar()!= '\n')
        continue;
 }
 void ToUpper(char *str)
 {
    while(*str)
    {
        *str = toupper(*str);
        str++;
     }
 }
 void ToLower(char *str)
 {
    while(*str)
    {
        *str = tolower(*str);
        str++;
     }
 }
 void Transpose(char *str)
 {
    while(*str)
    {
        if(islower(*str))
            *str = toupper(*str);
        else if(isupper(*str))
            *str = tolower(*str);
        str ++;
     }
 }
 void Dummy(char *str)
 {
    //不改变字符 
 }
 void show(void (*fp)(char *),char *str)
 {
    (*fp)(str);     //把用户设定的函数作用于str
    puts(str);      //显示结果 
 }
 char *s_gets(char *st,int n)
 {
    char *ret_val;
    char *find;

    ret_val = fgets(st,n,stdin);
    if(ret_val)
    {
        find = strchr(st,'\n');
        if(find)
            *find = '\0';
        else
            while(getchar()!='\n')
                continue;
     }
     return ret_val;
 }

file

ToUpper(),ToLower(),Transpose(),和Dummy()函数的类型都相同,所以这4个函数都可以给赋值给pfun指针,该程序把pfun作为show()的参数,但是也可以直接把4个函数种的任一个函数名作为参数,如,show(Transpose,copy). 
这种情况下,可以使用typedef。例如:该程序中可以这样写:
typedef void (*V_FP_CHARP)(char *);
void show (V_FP_CHARP fp,char *);
V_FP_CHARP pfun;
如果还想复杂一些,可以声明并初始化一个函数指针的数组:
V_FP_CHARP arpf[4] = {ToUpper,ToLower,Transpose,Dummy};
然后把showmenu()函数的返回类型改为int,如果用户输入u,则返回0,如果用户输入l,则返回1,如果用户输入t,则返回2,以此类推,可以把程序中switch语句替换成下面的while循环:
index = showmenu();
while(index >=0 ** index<=3)
{
    strcpy(copy,line);
    show(arpf[index],copy);
    index = showmenu();
}
虽然没有函数数组,但是可以有函数指针数组。
以上介绍了使用函数名的4种方法:定义函数、声明函数、调用函数和作为指针。
至于如何处理菜单:
ans = getchar();    //获取用户输入
ans = tolower(ans); //转换成小写
和
ans = tolower(getchar());
演示了转换用户输入的两种办法,这两种办法都可以把用户输入的字符转换成一种大小写形式,这样就不用检测用户输入的是'u'还是'U',等等。
eatline()函数丢弃输入行中剩余字符,在处理这两种情况时很有用。第一,用户为了输入一个选择,输入一个字符,然后按下Enter键,将产生一个换行符。如果不处理这个换行符,它将会成为下一次读取的第1个字符。第二,假设用户输入的是整个单词uppercase,而不是一个字母u。如果没有eatline()函数,程序将会把uppercase种的字符作为用户响应依次读取。有了eatline(),程序会读取u字符并且丢弃输入行种剩余的字符。
其次,showemenu()函数的设计意图是,只给程序返回正确的选项。为了完成这项任务,程序使用了string.h头文件种的标准函数strchr():
while(strchr("ulton",ans)==NULL)
该函数在字符串“ulton”中查找字符ans首次出现的位置,并返回一个指向字符的指针,如果没有找到改字符,则返回空指针,因此,上面的while循环头可以用下面的while循环头代替,但是上面的用起来更方便:
while(ans != 'u'&&ans!=.....);

擦肩而过的概率