文章目录
14.1 文件的基本概念
14.1.1 流
程序里面变量中存储的数据在程序运行结束之后就会丢失。只有将变量保存到外部存储器(比如硬盘)中才可以永久保存内容。C语言中有文件读写函数可以进行外部存储的处理。
首先所有文件的内容也可以看成是类似于水流的流,最底层是由一个字节一个字节组成的。当然,借助于函数,我们可以对流进行读写,而且可以从更高的抽象层次来操作,不需要直接面对一个个字节。
14.1.2 文件指针和标准流
C程序中对流的访问是通过文件指针实现的。此指针的类型为FILE *(FILE类型在<stdio.h>中声明)。可以声明文件指针
FILE *fp1, *fp2;
文件指针可以保存打开的流的信息(比如打开硬盘上的文件),然后就可以通过它们来访问流。
<stdio.h>提供了3个标准流,这3个标准流可以直接使用,我们不需要对其它们进行声明,也不用打开或者关闭它们。
文件指针 | 流 | 默认的含义 |
---|---|---|
stdin | 标准输入流 | 键盘 |
stdout | 标准输出流 | 屏幕 |
stderr | 标准错误流 | 屏幕 |
前面章节使用过的函数(printf、scanf、putchar、getchar、puts和gets)都是通过stdin获得输入,用stdout进行输出。
14.1. 文本文件和二进制文件
在C语言的文件处理函数里(具体来说是<stdio.h>头文件里的函数),文件分为两种类型:文本文件和二进制文件。在文本文件中,字节表示字符,这使人们可以检查或者编辑文件。例如,C程序的源代码是存储在文本文件中的。而在二进制文件中,字节不一定表示字符,若干个字节组成的字节组还可以表示其它类型的数据,比如整数和浮点数。比如图片、视频、音频文件存储的内容。在Windows中,用记事本是无法查看这些文件的,需要用专门的软件才可以查看它们。
C语言的文本文件还具有二进制文件没有的特性:
- 文本文件分为若干行。文本文件的每一行通常以一两个特殊字符结尾,特殊字符的选择与操作系统有关。在Windows中,行末的标记为回车符(’\x0d’,ASCII码值是13)与一个紧随其后的回行符(’\x0a’,ASCII码值是10)。在UNIX和Macintosh操作系统(Mac OS)的较新版本中,行末的标记是一个单独的回行符。换句话说,Windows和其它操作系统的文本文件可以是不兼容的,而读写文本文件的函数在不同的操作系统中要做不同的处理。
- 文本文件可以包含一个特殊的“文件末尾”标记。一些操作系统允许在文本文件末尾使用一个特殊的字节作为标记。在Windows中,标记为’\x1a’(Ctrl+Z)。这个标记不是必需的,但如果存在,它就标记着文件的结束,其后的所有字节都会被忽略(有点像字符数组中用空字符表示字符串的结束)。使用Ctrl+Z的这一习惯继承自DOS,而DOS中的这一习惯又是从CP/M(早起用于个人电脑的一种操作系统)来的。大多数其他操作系统(包括UNIX)没有专门的文件末尾字符。
二进制文件不分行,也没有行末标记和文件末尾标记,所有字节都是被平等对待的。
对文件进行读写数据时,需要考虑是按文本格式存储还是按二进制格式进行存储。考虑在文件中存储3276756的情况。一种选择是以文本的形式把该数按字符3、2、7、6、7、5、6写入。假设字符集是ASCII,那么就可以得到下列7个字节:
0011 0011 | 0011 0010 | 0011 0111 | 0011 0110 | 0011 0111 | 0011 0101 | 0011 0110 |
---|---|---|---|---|---|---|
‘3’ | ‘2’ | ‘7’ | ‘6’ | ‘7’ | ‘5’ | ‘6’ |
另一种选择是以二进制的形式存储此数,比如这个数存储在int类型变量中,而且int占4个字节,那么其存储形式为:
00000000 | 00110001 | 11111111 | 11010100 |
---|
一般来说,把数值以二进制的形式存储更加节省空间。
编写用来读写文件的程序时,需要考虑该文件是文本文件还是二进制文件。在屏幕上显示文件内容的程序可能要把文件视为文本文件。但是,文件复制程序需要原样复制文件,就不能认为要复制的文件为文本文件。否则可能会不能原样复制。比如,Windows中的文本文件,包含了文件末尾字符,这样也不能原样复制。在无法确定文件是文本文件还是二进制形式时,安全的做法是把文件假定为二进制文件。
概念上来说,文本文件和二进制文件的说法是脱离字符编码的,文本文件是在字符编码之上的抽象,但是由于本章C的文本文件处理(的函数)讨论和处理文件都是以ASCII编码的,一个字节是一个字符,那么我们的讨论也只限定在以ASCII为编码的文本文件(不处理中文字符)。
14.2 文件基本操作
14.2.1 打开文件
FILE *fopen(const char* filename, const char* mode);
如果要把文件用作流,需要调用fopen函数来打开它。fopen的第一个参数表示要打开的文件名,文件名中包含绝对路径或者相对路径。第二个参数是“模式字符串”,它用来指定打算对文件的解读和执行的操作。比如,字符串"r"表明把文件看成文本文件,而不是二进制文件,将从文件读入数据,但是不会写入数据。
fopen函数返回一个文件指针。通常保存此文件指针到文件指针变量中,稍后需要对文件进行操作时使用它。fopen函数常见的调用形式如下,其中fp是FILE*类型的变量,
fp = fopen("in.txt", "r");
当程序稍后调用输入函数从文件in.txt中读字符时,将会把fp作为一个实际参数。
当无法打开文件时,fopen函数会返回空指针(NULL)。这可能是因为文件不存在,也可能是因为我们没有与“模式字符串”参数中申请的处理文件的相应的权限。
Windows程序员:在fopen函数调用文件名中含有字符\时,一定要小心。因为C语言会把字符\看成是转义字符的开始标志。
fopen("c:\project\test1.dat", "r");
这个调用会失败,因为编译器会把\t看成是转义字符。有两种方法可以避免这个问题。一种方法是用\\替代\:
fopen("c:\\project\\test1.dat", "r");
另一种方法更简单,只要用/代替\就可以了:
fopen("c:/project/test1.dat", "r");
Windows会把/看成是目录分隔符。
14.2.2 模式
模式字符串不但用来告诉fopen函数稍后需要对文件采取的操作,还用来告诉fopen函数文件中的数据是文本形式还是二进制形式。
用于文本文件的模式字符串:
字符串 | 含义 |
---|---|
“r” | 打开文件用于读 |
“w” | 打开文件用于写(文件不需要存在) |
“a” | 打开文件用于追加(文件不需要存在) |
“r+” | 打开文件用于读和写,从文件头开始 |
“w+” | 打开文件用于读和写(如果文件存在就截去) |
“a+” | 打开文件用于读和写(如果文件存在就追加) |
当使用fopen函数打开二进制文件时,需要在模式字符串中包含字母b。
用于二进制文件的模式字符串:
字符串 | 含义 |
---|---|
“rb” | 打开文件用于读 |
“wb” | 打开文件用于写(文件不需要存在) |
“ab” | 打开文件用于追加(文件不需要存在) |
“r+b"或者"rb+” | 打开文件用于读和写,从文件头开始 |
“w+b"或者"wb+” | 打开文件用于读和写(如果文件存在就截去) |
“a+b"或者"ab+” | 打开文件用于读和写(如果文件存在就追加) |
可以看出<stdio.h>对写数据和追加数据进行了区分。当给文件写数据时,通常会删除之前的文件内容,重新进行写。如果需要保留文件原有内容,把写入的内容追加到文件末尾,可以用追加模式。
当打开文件用于读和写时(模式字符串包含字符+时),有一些特殊的规则。这里我们并没有进一步探讨,需要的话请参考相关的文章。
14.2.3 关闭文件
int fclose(FILE *stream);
可以使用fclose关闭不再使用的文件。fclose的参数是文件指针,此指针的值来自fopen函数的调用结果。如果关闭成功,fclose函数返回0,否则它返回错误代码EOF(在<stdio.h>中定义的宏)。
下面的代码演示了常见了文件打开和关闭代码。要打开的文件是一个文本文件,对它的实际读取函数可以参考后续的各种文件文件输入函数。
#include <stdio.h>
#include <stdlib.h>
#define FILE_NAME "example.txt"
int main()
{
FILE *fp;
fp = fopen(FILE_NAME, "r");
if (fp == NULL){
printf("Can't open %s\n", FILE_NAME);
exit(EXIT_FAILURE);
}
... // 读文件中的内容
fclose(fp);
return 0;
}
也可以把fopen函数的调用和fp的声明写在一起,
FILE *fp = fopen(FILE_NAME, "r");
或者,可以把fopen函数的调用和空指针的判断写在一起
if ((fp = fopen(FILE_NAME, "r")) == NULL)...
14.3 格式化的输入/输出
这节介绍的函数可以用格式字符串来指明读/写的内容。这些函数包括已经知道的printf函数和scanf函数,它们可以在输入时把字符格式的数据转换成数值格式的数据,并且可以在输出时把数值格式的数据再转换成字符格式的数据。显然,它们应该的读/写对象应该是文本文件。
14.3.1 格式化的输出
int fprintf ( FILE *fp, const char * format, ... );
int printf(const char * format, ... );
fprintf函数相当于printf函数更通用的版本,相比于printf函数,它提供的第一个参数,可以用来指定写入的流,比如磁盘文件。如果fprintf函数第一个参数指定的写入流是标准输出流stdout,那么fprintf与printf函数就是等价的。fprintf函数第二个参数等同于printf函数的第一个参数,是格式控制字符串。其中的转换说明符描述了输出什么样的值,具体的输出值在后续的参数中提供。
#include <stdio.h>
#include <stdlib.h>
#define NAME_LEN 30
int main(){
const char *file_name = "test.txt";
FILE *fp;
char name[NAME_LEN + 1];
int age;
float height;
if ((fp = fopen(file_name, "w")) == NULL){
printf("Can't open %s\n", file_name);
exit(EXIT_FAILURE);
}
printf("请输入姓名:");
scanf("%s", name);
printf("请输入年龄:");
scanf("%d", &age);
printf("请输入身高:");
scanf("%f", &height);
fprintf(fp,"%s\n%d\n%.2f\n", name, age, height);
fclose(fp);
getchar();
getchar();
return 0;
}
14.3.2 格式化的输入
int fscanf ( FILE *fp, const char * format, ... );
fscanf函数相当于scanf函数更通用的版本,相比于scanf函数,它提供的第一个参数,可以用来指定读入的流,比如磁盘文件。如果fscanf函数第一个参数指定的读入流是标准输入流stdin,那么fscanf与scanf函数就是等价的。fscanf函数的第二个参数等同于scanf函数的第一个参数,是格式控制字符串。其中的转换说明符描述了需要输入什么样的值,输入值具体存储在哪些变量中在后续的参数中提供变量的地址。
接格式化输出中的例子,
#include <stdio.h>
#include <stdlib.h>
#define NAME_LEN 30
int main(){
const char *file_name = "test.txt";
FILE *fp;
char name[NAME_LEN + 1];
int age;
float height;
if ((fp = fopen(file_name, "r")) == NULL){
printf("Can't open %s\n", file_name);
exit(EXIT_FAILURE);
}
fscanf(fp,"%s%d%f", name, &age, &height);
printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("身高:%.2f\n", height);
fclose(fp);
getchar();
getchar();
return 0;
}
14.3.3 检测文件末尾和错误条件
待续,参考课本12.2.5节
14.4 字符的输入/输出
在本节中,我们讨论用于读和写单个字符的库函数。这些函数可以处理文本流和二进制流(单个字符又可以视为二进制文件的单个字节)。
注意,本节中的函数把字符作为int型而非char类型的值来处理。这样做的原因之一就是输入函数是通过返回EOF来说明文件末尾(或错误)的情况的,而EOF又是一个负的整数常量。
14.4.1 输出函数
int fputc(int c, FILE *stream);
int putchar(int c);
putchar函数向标准输出流写一个字符:
putchar(ch);
fputc函数是putchar函数向任意流写字符的更通用版本:
fputc(ch, fp);
如果出现了写错误,那么这两个函数都会为流设置错误指示器并且返回EOF。否则,它们会返回写入的字符。
14.4.2 输入函数
int fgetc(FILE *stream);
int getchar(void);
getchar函数从标准输入流stdin中读入一个字符:
ch = getchar();
fgetc函数从任意流中读入一个字符:
ch = fgetc(fp);
这两个函数都把字符看成unsigned char类型的值(返回之前转换成int类型)。因此,它们不会返回EOF之外的负值。
如果出现问题,那么这两个函数的行为是一样的。如果遇到文件末尾,那么它们都会设置流的文件末尾指示器,并且返回EOF。如果产生了读错误,它们则都会设置流的错误指示器,并且返回EOF。为了区分这两种错误情况,可以调用feof函数或者ferror函数。
这两个函数最常见的用法之一是从文件中逐个读入字符直到遇到文件末尾。一般习惯使用下列while循环来实现此目的:
while((ch = fgetc(fp)) != EOF){
}
在从与fp相关的文件中读入字符并把它存储到变量ch(它必须是int类型的)之中后,判定条件会把ch与EOF进行比较。如果ch不等于EOF,这表示还未到达文件末尾,那么它可以执行循环体。如果ch等于EOF,则循环终止。
注意,使用要把fgetc、getchar函数的返回值保存到int型的变量中,而不是char类型的变量中。把char类型的变量与EOF进行比较可能会得到错误的结果。
有两种情况都可能得出错误的结果,为了使下面的讨论更具体,这里假设使用二进制补码存储方式。
首先,假定char类型是无符号类型。(有些编译器把char当做有符号类型来处理,有些当做无符号类型来处理)现在假设fgetc函数返回EOF,把该返回值存储在名为ch的char类型变量中。如果EOF表示-1(通常如此),那么ch的值将为255。这是因为-1就是0xffffffff,把它赋值给ch,ch为0xff(截取-1的最后1个字节),由于ch是无符号的,故为255。把ch(无符号字符)和EOF(有符号整数)进行比较,就要求把ch转换为有符号整数,在这个例子中是255。因为255不等于-1,所以与EOF的比较失败了。
因为无法控制char是无符号还是有符号,这里用unsigned short int做实验,
unsigned short int ch;
ch = -1; // 0xffffffff
if (ch == -1){
printf("等于-1\n");
}else{
printf("不等于-1\n"); // 输出这个错误的结论
}
反之,现在假设char是有符号类型。如果fgetc函数从二进制流中读取了一个值为255(0xff)的字节,那么会产生什么情况呢?因为ch为有符号字符,把255存储在char类型变量中会将它赋值为-1。如果判断ch是否等于EOF,将会错误地产生真的结果。
同样的,用short int做实验,
short int ch;
ch = 65535; // 0xffff
if (ch == -1){
printf("等于-1\n"); // 输出这个错误的结论
}else{
printf("不等于-1\n");
}
例子. 复制文件
#include <stdio.h>
#include <stdlib.h>
#define FILE_NAME_LEN 30
int main(){
char file_name_src[FILE_NAME_LEN + 1], file_name_dst[FILE_NAME_LEN + 1];
FILE *file_src, *file_dst;
int ch;
printf("input src file name:");
gets(file_name_src);
printf("input dst file name:");
gets(file_name_dst);
if ((file_src = fopen(file_name_src, "rb")) == NULL){
printf("Can't open %s\n", file_name_src);
exit(EXIT_FAILURE);
}
if ((file_dst = fopen(file_name_dst, "wb")) == NULL){
printf("Can't open %s\n", file_name_dst);
fclose(file_src);
exit(EXIT_FAILURE);
}
while((ch = fgetc(file_src)) != EOF){
fputc(ch, file_dst);
}
fclose(file_src);
fclose(file_dst);
getchar();
getchar();
return 0;
}
采用“rb”和"wb"作为文件模式使程序既可以复制文本文件也可以复制二进制文件(逐个读取文件中的各个字节,不做任何转换)。如果用"r"和"w"来代替,那么程序将无法复制二进制文件。
注意,运行程序时,可以直接输入文件名,这就相当于在当前目录下操作文件,VS2010的当前目录就是源文件所在的目录。
14.5 行的输入/输出
下面介绍读和写行的库函数。虽然这些函数也可以有效地用于二进制流,但是它们多用于文本流。
14.5.1 行的输出
int fputs(const char *s, FILE * stream);
int puts(const char *s);
puts函数用于向标准输出流stdout输出字符串:
puts("Hi, nice to meet you");
在输出字符串中的字符后,puts函数还会输出一个换行符。
fputs函数时puts函数的更通用版本。此函数的第二个参数指明了输出要写入的流。
fputs("Hi, nice to meet you", fp);
不同于puts函数,fputs函数不会自己写入换行符,除非字符串本身含有换行符。
当出现写入错误时,它们都会返回EOF。否则,它们都会返回一个非负的数。
14.5.2 行的输入
char *fgets(char *s, int n, FILE *stream);
char *gets(char *s);
gets函数用来从标准输入流中读取一行。
gets(str); // str是字符数组
gets函数逐个读取字符,并且把它们存储在str指向的数组中,直到它读到换行符时停止(丢弃换行符)。并在存储字符串的末尾会添加一个空字符。
fgets函数是gets函数的更通用版本,它可以从任意流中读取信息。fgets函数比gets函数更安全,因为它可以指定要存储的字符的最大数量。
fgets(str, sizeof(str), fp);
此调用将使fgets函数逐个读入字符,直到遇到首个换行符或者已经读入了sizeof(str) - 1个字符。如果是遇到了首个换行符,那么fgets函数也会存储换行符。(gets从来不存储换行符,而fgets有时候会存储换行符。)并在存储字符串的末尾会添加一个空字符。
如果出现了读错误,或者是在存储任何字符之前达到了输入流的末尾,那么gets函数和fgets函数都会返回空指针。(通常,可以使用feof函数或ferror函数来确定出现的是哪种错误。)否则,两个函数都会返回自己的第一个实参,即指向保存字符串的数组的指针。
14.6 二进制文件的输入/输出
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
fread和fwrite函数用于读和写数据块,数据块可能是任意类型的变量,包括基本类型的变量、数组或者结构体,内存中的这些数据被进行原样二进制读和写。如果小心使用,它们可以用于文本流,但是它们主要还是用于二进制的流。
fwrite函数被设计用来把内存中的数组原样写到流中。fwrite的第一个参数是数组的地址(数组首元素的地址),第二个参数是数组元素的大小(以字节为单位),第三个参数是要写的数组元素数量(要写的元素数量不一定等于数组总元素数量,即数组长度),第四个参数是文件指针。例如,假如有数组
int a[10] = {1,2,3,4,5,6,7,8,9,10};
将数组前5个元素写到流fp中,
fwrite(a, sizeof(a[0]), 5, fp);
对任意数组b,将其全部内容写到流fp中,
fwrite(b, sizeof(b[0]), sizeof(b) / sizeof(b[0]), fp);
fwrite也可以用于写任意类型的单个变量(包括结构体变量),
double b = 9.743;
fwrite(&b, sizeof(b), 1, fp);
fwrite函数返回实际写入的元素的数量(不是总字节数)。如果出现写入错误,那么此返回值可能会小于第三个参数。
fread函数从流中读入数据保存到数组中(可以理解为把fwrite写的内容原样读回来)。fread函数的参数类似于fwrite函数的参数:数组的地址、每个元素的大小(以字节为单位)、要读的元素数量以及文件指针。为了从文件中读入m个元素放到数组a中,可以这样使用fread函数,
n = fread(a, sizeof(a[0]), m, fp);
fread会返回实际读的元素数量(不是总字节数)。此返回值应该等于第三个参数。但是如果达到了文件末尾或者出现了错误该值可能小于第三个参数,所以检查这个值很重要。可以用feof函数和ferror函数来确定出问题的原因。
使用fwrite读写指针值要小心,由于每次运行程序变量的地址值很可能不同,故读回这些值很可能无效了。
下面将格式化输入输出的文本文件以二进制形式输入和输出。输出代码,
#include <stdio.h>
#include <stdlib.h>
#define NAME_LEN 30
int main(){
const char *file_name = "test.bin";
FILE *fp;
char name[NAME_LEN + 1];
int age;
float height;
if ((fp = fopen(file_name, "wb")) == NULL){
printf("Can't open %s\n", file_name);
exit(EXIT_FAILURE);
}
printf("请输入姓名:");
scanf("%s", name);
printf("请输入年龄:");
scanf("%d", &age);
printf("请输入身高:");
scanf("%f", &height);
fwrite(name, sizeof(name[0]), sizeof(name) / sizeof(name[0]), fp);
fwrite(&age, sizeof(age), 1, fp);
fwrite(&height, sizeof(height), 1, fp);
fclose(fp);
getchar();
getchar();
return 0;
}
输入代码,
#include <stdio.h>
#include <stdlib.h>
#define NAME_LEN 30
int main(){
const char *file_name = "test.bin";
FILE *fp;
char name[NAME_LEN + 1];
int age;
float height;
int n;
if ((fp = fopen(file_name, "rb")) == NULL){
printf("Can't open %s\n", file_name);
exit(EXIT_FAILURE);
}
n = fread(name, sizeof(name[0]), sizeof(name) / sizeof(name[0]), fp);
printf("%d of char array's element is read\n", n);
n = fread(&age, sizeof(age), 1, fp);
printf("%d of int is read\n", n);
n = fread(&height, sizeof(height), 1, fp);
printf("%d of float is read\n", n);
printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("身高:%.2f\n", height);
fclose(fp);
getchar();
getchar();
return 0;
}
14.7 文件定位
待续,参考文本12.2.4节