14.8 把结构内容保存到文件中
由于结构可以存储不同类型的信息,所以它是构建数据库的重要工具。数据库文件可以任意数量的此类结构的数据对象。存储在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)。本节来谈论这个主题。
或许存储记录最没效率的方法使用fprintf()。例如,回忆程序清单14.1中的book结构:
#define MAXTITL 40
#define MAXAUTL 40
struct book {
char title[MAXTITL];
char author[MAXAULT];
float value;
};
如果pbook标识一个文件流,那么通过下面这条语句可以把信息存储于struct book类型的结构变量primer中:
fprintf( pbook, "%s %s %.2f\n", primer.title, primer.author, primer.value );
对于一些结构(如,有30个成员的结构)。这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(例如,"%39s%39s%8.2f"),但是这个方法仍然很笨拙。更好的方案是使用fread()和fwrite()函数读写结构大小的单元。这两个函数使用与程序相同的二进制表示法。例如:
fwrite( &primer, sizeof(struct book), 1, pbooks );
fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。
以二进制表示法存储数据的缺点是,不同的系统可能使用不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一个系统,不同编译器设置也可能导致不同的二进制布局。
14.8.1 保存结构的程序示例
程序清单14.14把程序清单14.2修改为一个新的版本,把书名保存在book.dat文件中。如果该文件存在,程序将显示它当前的内容,然后允许在文件中添加内容(如果你使用的是早期的Borland编译器,请参阅程序14.2后面的“Borland C和浮点数”)。
程序清单14.14 booksave.c程序
/* booksave.c -- saves structure contents in a file */
#include
#include
#include
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 10 /* maximum number of books */
char * s_gets(char * st, int n);
struct book { /* set up book template */
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
int main(void)
{
struct book library[MAXBKS]; /* array of structures */
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); /* go to start of file */
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("Press [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; /* clear input line */
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'); // look for newline
if (find) // if the address is not NULL,
*find = '\0'; // place a null character there
else
while (getchar() != '\n')
continue; // dispose of rest of line
}
return ret_val;
}
/* 首次输出:
再次运行输出:
*/
14.8.2 程序要点
a+部分允许程序读取整个文件并在文件末尾的末尾添加内容。b是ANSI的一种标识方法,表明程序使用二进制文件格式。对于不接受b模式的UNIX系统,可以省略b,因为UNIX只有一种文件形式。对于早期的ANSI实现,要找出和b等价的表示法。
我们选择二进制模式是因为fread()和fwrite()函数要使用二进制文件。虽然结构中有些内容是文本,但是value成员不是文本。如果使用文本编辑器查看book.dat,该结构本文部分的内容显示正常,但是数值部分不可读,甚至会导致文本编辑器出现乱码。
rewind()函数确保文件指针位于文件开始处,为读文件做好准备。
由于表达式&library[filecount]是数组中第1个新结构的地址,所以拷贝就从这里开始。
也许该例是把结构写入文件和检索它们的最简单的方法,但是这种方法浪费存储空间,因为这还保存了结构中未使用的部分。也就是说,它保存了字符数组中未使用的部分,实际上不是每个输入项都需要这么多空间。但是,让每个输入块的大小相同在检索数据时很方便。
另一个方法是使用可变大小的记录。为了方便读取文件中的这种记录,每个记录以数值字段规定记录的大小,这比上一种方法复杂。通常,这种方法涉及接下来要介绍的“链式结构”和第12章的动态内存分配。