《C Prime Plus》14.8学习笔记

139 阅读4分钟

结构与文件

结构变量是可以储存不同类型的信息的变量,基于这个特点,它是数据库构建过程中非常重要的工具。 前面的学习中,讲到存储信息到文件的方法有fprintf()等。最好理解的就是fprintf(),因为它可以转换格式。然而,fprintf()是效率最低的一种方法,一旦面对大量数据,比如现在学习的结构变量,就很困难。

因此,书中建议使用fread()fwrite()来读写结构进文件。 fwrite()的使用范例:

fwrite(&primer,sizeof(struct book),1,pbooks);

有一个名为primer的结构变量,首先fwrite()接受了它的地址,定位到它开始的位置,然后把它所有的字节拷贝进文件指针pbooks所指对象中去。中间的两个参数是确定一次本次读取的字节大小,要确定与结构的字节数保持一致。

保存结构的示例程序

书中提供了一套很有代表性的代码,来展示结构与文件的互动:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXBOOKS 10
#define MAXTITL 40
#define MACAUTH 40

typedef struct book {
    char title[MAXTITL];
    char author[MACAUTH];
    float value;
} Book;

char *s_gets(char *, int);

int main(void) {
    Book library[MAXBOOKS]; //library是结构数组
    int count = 0;
    int index, filecount;
    FILE *pbooks;
    int size = sizeof(Book);
    if ((pbooks = fopen("book.dat", "a+b")) == NULL) exit(1);
    rewind(pbooks);//定位到文件开始处
    while (count < MAXBOOKS && //fread()函数是将输入流pbooks中的信息传递入library中,以library的形式读入于程序中
           fread(&library[count], //size是一个Book结构的字节大小,也告诉了fread要读入的字节数给library的元素
                 size, //细节来说,fread依次按title,author,value读入数据,先读入一个字符串给title,然后读入一个字符串给author,最后读入一个浮点数给value
                 1, pbooks) == 1) {//在bat文件中,对应的浮点数数据并非字面上的数字,而是一种特殊的有前缀的信息,被fread读入后会被转换
        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 == MAXBOOKS) {
        fputs("The book.dat is full.", stderr);
        exit(2);
    }
    puts("Please add new book titles.");
    puts("Press [enter] at the start of a line to stop.");
    while (count < MAXBOOKS
           && s_gets(library[count].title, MAXTITL) != NULL
           && library[count].title[0] != '\0') {
        puts("Now enter the author:");
        s_gets(library[count].author, MAXTITL);
        puts("Now enter the value:");
        scanf("%f", &library[count++].value);
        while (getchar() != '\n')continue;
        if (count < MAXBOOKS) 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? Fuck you!");
    fclose(pbooks);
    return 0;
}

char *s_gets(char *str, int n) {
    char *ret_val; //ret_val的作用是验证输入函数是否运行成功
    int i = 0;
    ret_val = fgets(str, n, stdin);
    if (ret_val) {
        while (str[i] != '\n' && str[i] != '\0') ++i; //搜索输入内容中的换行符和空字符
        if (str[i] == '\n') str[i] = '\0';
        else
            while (getchar() != '\n') continue;
    }
    return ret_val;
}

程序构成

程序主要分为结构声明、函数原型、主函数、函数定义四块。输入函数s_gets()是前面学习字符串时自定义的输入函数,可以安全地接收用户的输入并得到一个完整的不以换行符结尾的字符串。

程序分析及思路

  1. 声明Book结构模板,由标题、作者、价格组成,两个字符串,一个浮点数。
typedef struct book {
    char title[MAXTITL];
    char author[MACAUTH];
    float value;
} Book;
  1. 进入主函数内,先声明一个最大元素数量为10的结构数组。然后声明一个承接文件数据流的文件指针pbooks
Book library[MAXBOOKS];
FILE * pbooks;
  1. fopen()读取文件,用if语句包括这个操作,假如fopen()返回空指针,说明读取错误,程序出错,退出。
if ((pbooks=fopen("book.dat","a+b"))==NULL) exit(1);
  1. rewind()定位到文件开始处,如果文件有内容,就先展示文件已经储存的内容,这也是为什么要用rewind()回到开头的原因,如果一开始book.dat存在且已经有内容,程序会从文件内容末尾开始。整形变量count用来记录已经读取到文件中第几个Book数据了。如果文件不存在内容,那么就不会进入这个循环,直接转到下面的添加内容环节。
while (count < MAXBOOKS && 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 == MAXBOOKS) {
        fputs("The book.dat is full.", stderr);
        exit(2);
    }

这里的fread()至关重要,一定要理解它干了什么。fread()将作为输入流位置的参数pbooks中的信息以Books的形式要求一个元素一个元素传递给了library数组(因为library是Book结构),细节上来说,是依次按照读取一个字符串给title成员,读取一个字符串给author成员,读取一个浮点数给value成员。

展示内容的循环结束后,用filecount变量存储文件原本就有的内容条目数,这是为了后面用户输入后,让程序展示用户输入的新内容,而不是把所有内容全展示。

最后,如果count到了10,说明文件里预存的内容已经达到10本书了,不能继续输入了。

  1. 检查完文件的内容后,就来到了重要的输入过程了。while检查count不要达到最大数,并把输入函数放在条件中,用户需要依次输入标题作者价格,所以先把标题放在条件中让用户先输入;最后的title首字符不是空字符代表着用户如果直接输入一个换行符(直接回车),sgets()直接把这个仅存的换行符换成了空字符,然后循环条件从左往右扫描,来到这里,扫描到了用户刚输入的结果是一个空字符,因此循环结束。
while (count < MAXBOOKS && s_gets(library[count].title, MAXTITL) != NULL
           && library[count].title[0] != '\0')

用户输入了正常的title后,进入循环内,接着输入作者和价格,因为价格是浮点数而非字符串,所以要用scanf(),但是scanf()在输入非字符串时,会给缓冲区留下换行符,要及时清理。最后,只要count还没达到最大值,就要求用户继续输入,循环继续。

while (count < MAXBOOKS && s_gets(library[count].title, MAXTITL) != NULL
&& library[count].title[0] != '\0')
        puts("Now enter the author:");
        s_gets(library[count].author, MAXTITL);
        puts("Now enter the value:");
        scanf("%f", &library[count++].value);
        while (getchar() != '\n')continue;
        if (count < MAXBOOKS) puts("Enter the next title.");

用户输入完(或者已经满了),就来到最后的阶段,展示用户输入的内容。但要注意一点,此时用户输入的内容均还在library数组里,还没有存储到文件中。展示完后,就要好好地把library数组的内容注意放进文件中,以前可能会用fprintf()来,但是fwrite()是最好的选择。

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? Fuck you!");
    fclose(pbooks);

fwrite()library[filecount]开始,filecount是用户输入前的条目数,这意味着fwrite()要从用户输入的第一个新内容开始写入,字节大小依然为一个Book结构变量的字节大小,不过数目就要以用户输入的条目数来计算了,所以要进行count - filecount的表达式计算,接收这些写入数据的依然是pbooks。

最后,不要忘了用fclose()关掉文件模式。