10.2 数据文件概述
10.2.1 ASCII文件与二进制文件
ASCII文件就是“将需要保存到文件的信息使用ASCII字符表示,然后按照顺序将每个字符的ASCII码存储到文件中”。ASCII文件的优点是编码方式公开,可以被其它的文本编辑器打开;其缺点是效率比较低,信息冗余度高。
二进制文件将数据在内存中的二进制形式原样存储到文件中。
10.2.2 缓冲文件系统
所谓缓冲区,是指对于每个正在使用的文件,系统在内存中为其开辟的一段存储区,用于存储从文件输入的内容或者需要输出到文件的内容。像这种通过缓冲区处理文件的系统称为缓冲文件系统。使用缓冲文件系统可以大大提高文件操作的速度。
缓冲区有两种类型,输入缓冲区和输出缓冲区,程序、缓冲区和文件的关系如图13.4。
10.2.3 文件结构与文件类型指针
1.文件结构
为了有效地使用文件缓冲区,系统将缓冲区的细节封装到了一个结构体中,称为文件结构。该结构体的成员包含了文件缓冲区的详细信息,并使用typedef关键字将该结构类型重命名为FILE,该结构定义包含在stdio.h头文件中,因此,使用文件的程序都要包含“#include<stdio.h>”。
2.文件类型指针
有了文件缓冲区的结构定义以后,C语言引进指针来指向该文件结构,通过移动指针实现对文件的操作,该指针就是FILE文件类型指针。定义文件结构指针的格式为:
FILE 文件指针;
10.2.4 标准输入/输出设备文件
在操作系统中,把外部设备也当作一个文件进行管理的。例如,键盘通常作为标准输入设备,显示器及打印机称为标准输出设备。在C语言中,键盘被指定为标准的输入文件,文件指针是stdin,从键盘上输入就意味着从标准输入文件上输入数据,scanf()函数、getchar()函数使用的就是标准输入文件;显示器被指定为标准的输出文件,文件指针是stdout,向屏幕显示信息就是向标准输出文件输出数据,printf()函数、putchar()函数使用的就是标准输出文件。文件指针stdin和stdout不需要用户定义,它们都包含在stdio.h头文件中,由C编译系统自动完成。
10.3 文件的打开与关闭
10.3.1 文件的打开
C语言中,打开文件使用fopen()函数,打开文件的真正含义是请求系统为指定的文件名分配相应的文件缓冲区。打开文件的函数调用格式如下:
fopen("文件名","文件打开方式")
说明:
(1)括号中的“文件名”指出要打开的文件。文件名中一般要包含路径,如果未包含路径,默认与应用程序的当前路径相同;如果包含文件路径,由于“\”为转义字符,所有路径中的“\”都需要使用“\\”表示。
(2)fopen()函数的返回值是一个FILE类型的指针,常称为文件指针。所以,在使用文件之前,先要定义文件类型指针:
FILE *fp;
函数如果执行成功,返回包含文件缓冲区信息的文件结构体的地址,赋给文件指针fp。否则,返回一个NULL(空值)的FILE指针。
(3)为保证文件能正常打开,在调用fopen()函数时一般都要进行判断,例如,例13.1中的打开形式如下:
if((fp=fopen("D:\\file.txt","r"))==NULL) /* 打开文件 */
{ printf("File open error!\n");
exit(1);
}
(4)一旦文件经fopen()函数正常打开,该文件的操作方式即固定下来,直至文件被关闭都不能改变。例如,例13.1中的文件打开以后,只能读出其中的数据,不能进行写操作。
(5)允许同时打开多个文件,每打开一个文件就需要一个FILE指针,若需要同时打开多个文件,需要定义多个FILE指针。不允许同一个文件在关闭前被再次打开。
(6)C语言中,ASCII文件与二进制文件的打开方式可以通用。
10.3.2 文件的关闭
文件操作结束后,要使用fclose()函数关闭文件。关闭文件操作的真正含义是:将文件缓冲区中还未写入文件的数据写入文件,确保数据的完整性,同时释放文件占用的缓冲区单元。fclose()函数的格式为:
fclose(文件指针)
10.4 文件的操作
10.4.1 字符方式文件读写函数fgetc()和fputc()
1.fputc()函数
C语言中,使用fputc()函数向文件写入一个字符,函数调用形式为:
fputc(ch,fp)
2.fgetc()函数
C语言中,从文件中读取一个字符使用fgetc()函数,函数调用形式为:
ch=fgetc(fp);
10.4.2 字符串方式文件读写函数fgets()和fputs()
1.fputs()函数
C语言中,向文件中写入字符串通常使用fputs()函数,一般调用形式为:
fputs(str,fp)
2.fgets()函数
C语言中,从文件中读出字符串可以使用fgets()函数,一般调用形式为:
fgets(str, n, fp);
10.4.3 格式化文件读写函数fscanf()和fprintf()
1.fprintf()函数
fprintf ()函数的一般调用形式为:
fprintf(fp, 格式控制字符串, 输出项列表)
2.fscanf ()函数
fscanf()的一般调用形式为:
fscanf (fp, 格式控制字符串, 输出项列表)
10.4.4 数据块读写函数fread()和fwrite()
1.fwrite ()函数
fwrite()函数的一般调用形式为:
fwrite(buffer, size, count , fp)
2.fread ()函数
fread()函数的一般调用形式为:
fread(buffer, size, count , fp)
10.5 其他文件操作函数
1.feof()函数
feof ()函数的作用是检测文件是否结束,函数格式如下:
feof(fp)
2.rewind()函数
rewind()函数的作用是定位文件指针,使文件指针指向读写文件的首地址,即打开文件时文件指针所指向的位置。函数调用形式为:
rewind(fp);
3.fseek()函数
fseek(fp, offset, fromwhere);
4.ftell()函数
ftell ()函数的作用是获取当前文件指针读写的位置,函数格式如下:
ftell(fp);
11.1 宏定义
11.1.1 无参宏定义
无参数宏是指在宏名的后面没有参数。只是用一个指定的宏名来代表一个字符串,它的一般形式为:
#define 标识符 字符串
在C语言中,以“#”开头的均为预处理命令,“define”则表示是宏定义命令,“标识符”是定义的宏名,“字符串”可以是常数、表达式、类型名、格式串等。
说明:
(l)宏名一般习惯用大写字母表示,以便与变量名相区别,但也允许用小写字母。
(2)使用宏名代替一个字符串,可以减少程序中重复书写某些字符串的工作量。当需要改变某一个常量时,可以只改变#define命令行,改一处则全改。
(3)宏定义是用宏名代替一个字符串,只做简单的替换,编译程序不做语法检查。
(4)宏定义并不是C语句,一般不在行末加分号。如果加上分号,则会连分号一起进行替换。
(5)#define命令必须放在函数定义的外面,习惯上,#define总是写在文件开头处。宏名的作用域为从宏定义命令起到源文件结束,可以用#undef命令终止宏名的作用域,这样可以灵活控制宏名的作用范围。
(6)宏定义中可以引用已定义的宏名,进行层层替换。
(7)在源程序中出现的宏名若带引号时,预处理程序不对其进行宏替换。例如,例11.14的printf()函数格式字符串中的L和S都没有被替换。
(8)宏定义还可以定义运算符、表达式及其他符号。
11.1.2 带参数的宏定义
带参宏是指在宏名的后面带有参数。宏定义中的参数称为形参,宏调用中的参数称为实参。在调用带参数的宏时,不仅要进行宏展开,而且要进行参数替换,其定义的一般形式为:
#define 宏名(形参表) 字符串
其中,“字符串”中包含形参表中的参数。带参宏调用的一般形式为:
宏名(实参表)
说明:
(1)带参宏定义时,宏名与括号之间不应有空格,否则将空格以后的字符都作为字符串的一部分。
(2)宏定义中,字符串中的形参通常要用括号扩起来。
带参的宏调用与函数调用非常相似,但两者的实现过程完全不同。区别主要有:
(l)函数调用时,如果实参是表达式,要先求出表达式的值,再把结果值传递给形参。而使用带参的宏不作计算,直接替换进去。例如,上面的S(a-b),在宏展开时并不计算a-b的值,而只将实参字符“a-b”代替。
(2)函数调用是在程序运行时处理的,而宏调用是在编译时进行的。
(3)函数中的实参和形参都要定义类型,实参和形参是不同的变量,各自有自己的作用域,系统为实参和形参分配内存单元,调用时要把实参值传递给形参,进行“值传递”。而宏不存在类型的问题,宏名无类型,它的形参也无类型,只是一个符号,系统不为形参分配内存单元,不进行值的传递。但宏调用中的实参有具体的值,要用它们去替换形参,若是变量时必须作类型声明。
(4)程序中使用宏定义多时,宏展开以后的源程序长,因为每展开一次都使程序增长,而函数调用不会使源程序变长。
(5)宏替换不占用运行时间,只占用编译时间,而函数调用则占用运行时间(分配单元、保留现场、值传递和返回等)。
11.2 文件包含
文件包含是指一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。C语言提供了#include命令,用来实现文件包含操作,其一般形式为:
#include<文件名>
或
#include "文件名"
以上两种#include的区别是:采用尖括号(如<stdio.h>)时,编译系统从启动C编译系统的文件夹中去寻找所要包含的文件,这称为标准方式。如果用双引号(如"stdio.h")时,编译系统首先从用户保存源文件的文件夹中去寻找所要包含的文件,若找不到,再按标准方式查找。一般来说,如果调用系统的是库函数,用标准方式(尖括号)可节省查找时间,而如果要包含自己编写的文件时,一般用双引号方式。
文件包含命令是编译预处理命令,是在编译前执行的。编译程序在执行#include命令时将被包含文件插入到#include命令所在的位置,合并在一起当作一个文件来处理,并不是作为两个文件连接(用link命令实现连接),而是作为一个源程序编译,得到一个目标(.obj)文件,因此被包含的文件也只能是源文件而不能是目标文件(.obj文件)。
用文件包含的方法可以减少重复代码的输入,减轻劳动强度,提高编程效率。
使用#include命令的注意事项:
(1)一个#include命令只能指定一个被包含文件。如果要包含n个文件,要用n个#include命令。
(2)在一个被包含文件中又可以包含另一个被包含文件,即文件包含是可以嵌套的。但不能循环嵌套。
(3)被包含文件中的全局变量也是包含文件中的全局变量,在包含文件中这些量不必使用extern声明即可直接引用。
(4)C语言为用户提供了标准函数和宏定义。用户要想使用这些库函数,必须用#include命令将有关头文件包含在自己的源程序中。
11.3 条件编译
11.3.1 #ifdef…#else…#endif命令
一般形式如下:
#ifdef 标识符
程序段1
#else
程序段2
#endif
功能:如果标识符已经被#define命令定义过,则在程序编译时对程序段1进行编译;否则,对程序段2进行编译。
11.3.2 #ifndef…#else…#endif命令
一般形式如下:
#ifndef 标识符
程序段1
#else
程序段2
#endif
功能;如果标识符没被#define命令定义过,则对程序段1进行编译;否则,对程序段2进行编译。这与格式1的功能恰好相反。
11.3.3 #if命令
一般形式如下:
#if常量表达式
程序段1
#else
程序段2
#endif
功能:如果常量表达式的值为真(非0),则对程序段1进行编译;否则,对程序段2进行编译。
11.3.4 #undef命令
一般形式如下:
#undef 标识符
功能:将已经定义的标识符变为未定义的。
12.1 逻辑位运算
12.1.1按位“与”运算符&
按位“与”运算的运算符用“&”表示。
按位“与”的运算规则是:将两个操作数二进制位中的对应位相与,当对应位都为1时结果为1,否则结果为0。
12.1.2 按位“或”运算符|
按位“或”运算的运算符用“|”表示。
按位“或”的运算规则是:将两个操作数二进制位中的对应位相或,对应位都为0时结果为0,否则结果为1。
12.1.3 按位“异或”运算符^
按位“异或”运算的运算符用“^”表示。
按位“异或”的运算规则是:将两个操作数二进制位中的对应位相异或,对应位相同时结果为0,不同时结果为1。
12.1.4 按位“取反”运算符~
按位“取反”运算的运算符用“~”表示。
按位“取反”的运算规则是:将操作数的各二进制位按位取反,将0变为1,将1变为0。
12.2 移位运算符
12.2.1 左移运算符<<
左移运算的运算符用“<<”表示。
左移运算的运算规则是:将一个操作数的二进制表示形式按指定的移位次数向左移动,移出的高位丢掉,空出的低位补0。
12.2.2 右移运算符>>
右移运算的运算符用“>>”表示。
右移运算的运算规则是:将一个操作数的二进制表示形式按指定的移位次数向右移动,移出的低位丢掉。空出的高位,根据操作数的类型来定,如果是无符号数,则高位补0。