2022-11-23

用二进制I/O进行随机访问

该程序创建了一个数组, 并在该数组中存放了一些值。 然后, 程序以二进制模式创建了一个名为numbers.dat的文件, 并使用fwrite()把数组中的内容拷贝到文件中。 内存中数组的所有double类型值的位组合(每个位组合都是64位) 都被拷贝至文件中。 不能用文本编辑器读取最后的二进制文件, 因为无法把文件中的值转换成字符串。 然而, 储存在文件中的每个值都与储存在内存中的值完全相同, 没有损失任何精确度。 此外, 每个值在文件中也同样占用64位存储空间, 所以可以很容易地计算出每个值的位置。

程序的第 2 部分用于打开待读取的文件, 提示用户输入一个值的索引。程序通过把索引值和 double类型值占用的字节相乘, 即可得出文件中的一个位置。 然后, 程序调用fseek()定位到该位置, 用fread()读取该位置上的数据值。 注意, 这里并未使用转换说明。 fread()从已定位的位置开始, 拷贝8字节到内存中地址为&value的位置。 然后, 使用printf()显示value。

关键概念

C程序把输入看作是字节流, 输入流来源于文件、 输入设备(如键盘) , 或者甚至是另一个程序的输出。 类似地, C程序把输出也看作是字节流, 输出流的目的地可以是文件、 视频显示等。

C 如何解释输入流或输出流取决于所使用的输入/输出函数。 程序可以不做任何改动地读取和存储字节, 或者把字节依次解释成字符, 随后可以把这些字符解释成普通文本以用文本表示数字。 类似地, 对于输出, 所使用的函数决定了二进制值是被原样转移, 还是被转换成文本或以文本表示数字。如果要在不损失精度的前提下保存或恢复数值数据, 请使用二进制模式以及fread()和fwrite()函数。 如果打算保存文本信息并创建能在普通文本编辑器查看的文本, 请使用文本模式和函数(如getc()和fprintf())。

要访问文件, 必须创建文件指针(类型是FILE *) 并把指针与特定文件名相关联。 随后的代码就可以使用这个指针(而不是文件名) 来处理该文件。

要重点理解C如何处理文件结尾。 通常, 用于读取文件的程序使用一个循环读取输入, 直至到达文件结尾。 C 输入函数在读过文件结尾后才会检测到文件结尾, 这意味着应该在尝试读取之后立即判断是否是文件结尾。

本章小结

标准 I/O 包自动创建输入和输出缓冲区以加快数据传输。 fopen()函数为标准 I/O 打开一个文件, 并创建一个用于存储文件和缓冲区信息的结构。fopen()函数返回指向该结构的指针, 其他函数可以使用该指针指定待处理的文件。 feof()和ferror()函数报告I/O操作失败的原因。

C把输入视为字节流。 如果使用fread()函数, C把输入看作是二进制值并将其储存在指定存储位置。 如果使用fscanf()、 getc()、 fgets()或其他相关函数, C则将每个字节看作是字符码。 然后fscanf()和scanf()函数尝试把字符码翻译成转换说明指定的其他类型。 getc()和 fgetc()系列函数把输入作为字符码储存,将其作为单独的字符保存在字符变量中或作为字符串储存在字符数组中。 类似地, fwrite()将二进制数据直接放入输出流, 而其他输出函数把非字符数据转换成用字符表示后才将其放入输出流。

ANSI C提供两种文件打开模式: 二进制和文本。 以二进制模式打开文件时, 可以逐字节读取文件; 以文本模式打开文件时, 会把文件内容从文本的系统表示法映射为C表示法。 对于UNIX和Linux系统, 这两种模式完全相同。

通常, 输入函数getc()、 fgets()、 fscanf()和fread()都从文件开始处按顺序读取文件。 然而, fseek()和ftell()函数让程序可以随机访问文件中的任意位置。 fgetpos()和fsetpos()把类似的功能扩展至更大的文件。 与文本模式相比, 二进制模式更容易进行随机访问。


结构和其他数据形式

示例问题:创建图书目录

程序中创建的结构有3部分, 每个部分都称为成员(member)或字段(field) 。 这3部分中, 一部分储存书名, 一部分储存作者名, 一部分储存价格。是必须掌握的3个技巧:为结构建立一个格式或样式;声明一个适合该样式的变量;访问结构变量的各个部分。

建立结构声明

结构声明(structure declaration) 描述了一个结构的组织布局。

struct book {

char title[MAXTITL];

char author[MAXAUTL];

float value;

};

该声明描述了一个由两个字符数组和一个float类型变量组成的结构。 该声明并未创建实际的数据对象, 只描述了该对象由什么组成。 〔 有时, 我们把结构声明称为模板, 因为它勾勒出结构是如何储存数据的。

title部分是一个内含MAXTITL个元素的char类型数组。 成员可以是任意一种C的数据类型, 甚至可以是其他结构! 右花括号后面的分号是声明所必需的, 表示结构布局定义结束。 可以把这个声明放在所有函数的外部, 也可以放在一个函数定义的内部。 如果把结构声明置于一个函数的内部, 它的标记就只限于该函数内部使用。 如果把结构声明置于函数的外部, 那么该声明之后的所有函数都能使用它的标记。

struct book dickens;

该函数便创建了一个结构变量dickens, 该变量的结构布局是book。结构的标记名是可选的。 但是以程序示例中的方式建立结构时(在一处定义结构布局, 在另一处定义实际的结构变量) , 必须使用标记。

定义结构变量

结构有两层含义。 一层含义是“结构布局”, 刚才已经讨论过了。 结构布局告诉编译器如何表示数据, 但是它并未让编译器为数据分配空间。 下一步是创建一个结构变量, 即是结构的另一层含义。

struct book library;

编译器执行这行代码便创建了一个结构变量library。 编译器使用book模板为该变量分配空间: 一个内含MAXTITL个元素的char数组、 一个内含MAXAUTL个元素的char数组和一个float类型的变量。 这些存储空间都与一个名称library结合在一起

在结构变量的声明中, struct book所起的作用相当于一般声明中的int或float。

struct book doyle, panshin, * ptbook;

结构变量doyle和panshin中都包含title、 author和value部分。 指针ptbook可以指向doyle、 panshin或任何其他book类型的结构变量。 从本质上看,book结构声明创建了一个名为struct book的新类型。

初始化结构

初始化一个结构变量(ANSI之前, 不能用自动变量初始化结构; ANSI之后可以用任意存储类别) 与初始化数组的语法类似

struct book library = {

"The Pious Pirate and the Devious Damsel",

"Renee Vivotte",

1.95

};

使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。 因此, title成员可以被初始化为一个字符串, value成员可以被初始化为一个数字。 为了让初始化项与结构中各成员的关联更加明显, 我们让每个成员的初始化项独占一行。 这样做只是为了提高代码的可读性, 对编译器而言, 只需要用逗号分隔各成员的初始化项即可。

初始化结构和类别储存期

如果初始化静态存储期的变量(如, 静态外部链接、静态内部链接或静态无链接) , 必须使用常量值。 这同样适用于结构。 如果初始化一个静态存储期的结构, 初始化列表中的值必须是常量表达式。 如果是自动存储期, 初始化列表中的值可以不是常量。

访问结构成员

结构类似于一个“超级数组”, 这个超级数组中, 可以是一个元素为char类型, 下一个元素为forat类型, 下一个元素为int数组。

本质上, .title、 .author和.value的作用相当于book结构的下标。虽然library是一个结构, 但是library.value是一个float类型的变量, 可以像使用其他 float 类型变量那样使用它。

如果还有一个相同类型的结构变量, 可以用相同的方法:

struct book bill, newt;

s_gets(bill.title, MAXTITL);

s_gets(newt.title, MAXTITL);

.title 引用 book 结构的第 1 个成员。

结构的初始化器

C99和C11为结构提供了指定初始化器(designated initializer) [1], 其语法与数组的指定初始化器类似。 但是, 结构的指定初始化器使用点运算符和成员名(而不是方括号和下标) 标识特定的元素。

只初始化book结构的value成员

struct book surprise = { .value = 10.99};

可以按照任意顺序使用指定初始化器:

struct book gift = { .value = 25.99,

.author = "James Broadfool",

.title = "Rue for the Toad"};

与数组类似, 在指定初始化器后面的普通初始化器, 为指定成员后面的成员提供初始值。 另外, 对特定成员的最后一次赋值才是它实际获得的值。

结构数组

结构和内存manybook.c程序创建了一个内含100个结构变量的数组。 由于该数组是自动存储类别的对象, 其中的信息被储存在栈(stack) 中。 如此大的数组需要很大一块内存, 这可能会导致一些问题。

如果在运行时出现错误, 可能抱怨栈大小或栈溢出, 你的编译器可能使用了一个默认大小的栈, 这个栈对于该例而言太小。 要修正这个问题, 可以使用编译器选项设置栈大小为10000, 以容纳这个结构数组; 或者可以创建静态或外部数组(这样, 编译器就不会把数组放在栈中) ; 或者可以减小数组大小为16。

Borland C和浮点数

如果程序不使用浮点数, 旧式的Borland C编译器会尝试使用小版本的scanf()来压缩程序。 然而, 如果在一个结构数组中只有一个浮点值 , 那么这种编译器(DOS的Borland C/C++ 3.1之前的版本, 不是Borland C/C++ 4.0) 就无法发现它存在。

声明结构数组

struct book library[MAXBKS];

以上代码把library声明为一个内含MAXBKS个元素的数组。 数组的每个元素都是一个book类型的数组。 因此, library[0]是第1个book类型的结构变量, library[1]是第2个book类型的结构变量, 以此类推。可以帮助读者理解。 数组名library本身不是结构名, 它是一个数组名, 该数组中的每个元素都是struct book类型的结构变量。

表示结构数组的成员

为了标识结构数组中的成员, 可以采用访问单独结构的规则: 在结构名后面加一个点运算符, 再在点运算符后面写上成员名。

library[0].value /* 第1个数组元素与value 相关联 */

library[4].title /* 第5个数组元素与title 相关联 */

注意, 数组下标紧跟在library后面, 不是成员名后面:

library.value[2] // 错误

library[2].value // 正确

使用library[2].value的原因是: library[2]是结构变量名, 正如library[1]是另一个变量名。

library[2].title[4]

这是library数组第3个结构变量(library[2]部分) 中书名的第5个字符(title[4]部分) 。

程序讨论

while (count < MAXBKS && s_gets(library[count].title, MAXTITL) !=NULL

&& library[count].title[0] != '\0')

表达式 s_gets(library[count].title, MAXTITL)读取一个字符串作为书名,如果 s_gets()尝试读到文件结尾后面, 该表达式则返回NULL。 表达式library[count].title[0] != '\0'判断字符串中的首字符是否是空字符(即, 该字符串是否是空字符串) 。 如果在一行开始处用户按下 Enter 键, 相当于输入了一个空字符串, 循环将结束。

12.50[Enter]

其传送的字符序列如下:

12.50\n

scanf()函数接受1、 2、 .、 5和0, 但是把\n留在输入序列中。 如果没有上面两行清理输入行的代码, 就会把留在输入序列中的换行符当作空行读入,程序以为用户发送了停止输入的信号。 我们插入的这两行代码只会在输入序列中查找并删除\n, 不会处理其他字符。 这样s_gets()就可以重新开始下一次输入。

嵌套结构

指向结构的指针

使用指向结构的指针第一, 就像指向数组的指针比数组本身更容易操控(如, 排序问题) 一样, 指向结构的指针通常比结构本身更容易操控。 第二, 在一些早期的C实现中, 结构不能作为参数传递给函数, 但是可以传递指向结构的指针。 第三, 即使能传递一个结构, 传递指针通常更有效率。 第四, 一些用于表示数据的结构中包含指向其他结构的指针。

声明和初始化结构指针

struct guy * him;

首先是关键字 struct, 其次是结构标记 guy, 然后是一个星号(*) , 其后跟着指针名。 这个语法和其他指针声明一样。

该声明并未创建一个新的结构, 但是指针him现在可以指向任意现有的guy类型的结构。

和数组不同的是, 结构名并不是结构的地址, 因此要在结构名前面加上&运算符。

在有些系统中, 一个结构的大小可能大于它各成员大小之和。 这是因为系统对数据进行校准的过程中产生了一些“缝隙”。

用指针访问成员

指针him指向结构变量fellow[0], 如何通过him获得fellow[0]的成员的值?

第1种方法也是最常用的方法: 使用->运算符。 该运算符由一个连接号(-) 后跟一个大于号(>) 组成。换句话说, ->运算符后面的结构指针和.运算符后面的结构名工作方式相同。

him是一个指针, 但是hime->income是该指针所指向结构的一个成员。

第2种方法是, 以这样的顺序指定结构成员的值: 如果him ==&fellow[0], 那么*him == fellow[0], 因为&和*是一对互逆运算符。

fellow[0].income == (*him).income

必须要使用圆括号, 因为.运算符比*运算符的优先级高。

总之, 如果him是指向guy类型结构barney的指针, 下面的关系恒成立:

barney.income == (*him).income == him->income // 假设 him == &barney

你可能感兴趣的:(2022-11-23)