物联网工程师技术之c语言文件操作
本章重点
• 文件流的定义
• 文件基本操作
• 随机读写文件
大家学到现在,对调试C语言程序一定有一个最深的感触:“为什么每次调试程序都要输入那么多重复的数据呢?既然写程序就是要把人类从重复的劳动中解放出来,能不能让程序在开始运行时自动填入那些值呢?”答案是,当然可以。为实现这个功能,需要将每次运行程序时输入的那些数据以文件的形式存储到磁盘上。
本章主要讨论如何用C语言程序以文件的形式来存储各种数据,并根据需要进行各种操作和使用。
11.1 文件概述
11.1.1 文件
文件是指存放在存储设备中的一种数据集合形式,这里的存储设备可以是硬盘、CD、DVD、U盘等。绝大部分计算机系统都以文件为单位管理数据。大家平时创建的C语言源码文件就是文件,编译出来的可执行文件也是文件。如果打开Windows上的资源管理器(Windows 8上被称为“文件资源管理器”),进入任意一个文件夹,就能看到文件了。如图11-1所示:
图 111 Windows 系统文件夹下的部分文件
一个文件由它的路径和文件名称唯一确定。在Windows上,文件名是不区分大小写的。例如在同一个文件夹下,文件“Notepad.exe”和文件“notepad.EXE”以及文件“notepad.exe”都是指同一个文件。不过在某些系统(例如Unix和Linux)中,文件名是区分大小写的,上面的三个名称会分别指代不同的文件。为了方便阅读,本章的例程中统一使用小写文件名。
一般而言,用户只能通过各种应用程序对文件进行操作,包括但不限于对文件内容的增删改查,以及修改文件名、复制或移动文件等等文件系统操作。应用程序根据用户的指令(或程序员的指令)通过操作系统提供的接口来操作文件。不过幸运的是,C语言开发者并不需要根据操作系统的不同而学习不同的接口(那实在是太麻烦了,而且不同操作系统之间对文件的支持还有一定差异)——C语言为开发者准备了一个标准的文件流模型和一系列操作文件的标准函数,这些函数在不同的操作系统上都有标准的支持,因此只要学习这些函数就可以满足文件操作的需要了。
11.1.2 文本文件与二进制文件
C语言为用户提供了两种不同的文件视图,分别是文本文件视图与二进制文件视图。所谓“视图”是指对于同一份文件,不同的视图下会输出不同的内容。大家肯定会问:“为什么会出现两种不同的视图呢?难道不是忠于原始文件内容就可以了吗?”这是因为在不同的操作系统下,部分文件处理相关的功能可能是有区别的。一个常见的例子是换行符:Windows下,换行符是“\r\n”(十六进制的0xd与0xa),而在Linux和OS X下则只有一个字节“\n”(十六进制0xd),之前的MacOS(MacOS 9及之前的版本)使用的是“\r”(十六进制0xd)。这意味着“换行”在不同的操作系统下有不同的含义。为了统一起见,C语言规定了文本视图下,所有的换行符都应该采用“\n”来实现,而当C语言程序在不同平台下以文本视图读取和写入文件时,C运行时库会自动进行相应的转换。例如在Windows上,所有文件中的换行符“\r\n”都会被转换为“\n”;而在写回文件时,C运行时库再将“\n”转换为“\r\n”。这样就做到了跨平台的统一。
使用文本视图处理文件时,程序读取到的数据与文件的原始数据是有差别的。假如开发者在Windows上写了一个压缩程序用来对文件进行压缩和解压缩,但是使用了文本视图来操作文件,就会发现在某些情况下解压缩出来的文件和原来的文件不同——这是由于C语言文本视图对文件内容进行处理造成的。这时就要使用二进制文件视图了。与文本文件视图不同,二进制文件视图完全忠实于文件的原有内容,程序在读取数据时将看到完整的换行符(因操作系统而异),写入数据时也要由开发者保证换行符与当前操作系统的规定一致。
当然在某些系统(例如Linux/OS X)中,C语言的文本视图和二进制视图在实现上没有任何区别,这时开发者无论是使用文本视图还是二进制视图进行文件操作,得到的结果都是完全相同的。
11.1.3 流
为了标准化文件处理的过程,C语言为开发者提供了一个理想的流模型。流是输入设备或输出设备与缓冲区沟通的道路,在流的支持下,大家只要关注如何操作流既可高性能地读写文件,而无须关注其具体实现。
多学一招:什么是输入/输出设备?什么是缓冲区?
首先先介绍什么是输入和输出。对程序而言,从某些途径接收新数据叫做输入,而将数据传输到除内存以外的某些地方就叫做输出。输入的来源设备和输出的目标设备被统称为输入/输出设备(input/output devices)。常见的输入设备有键盘、鼠标、扫描仪、触摸屏等,常见的输出设备包括显示器、打印机等等。本章中,大家最常接触到的输入/输出设备是硬盘——因为大部分文件数据都要存到硬盘上,再从硬盘读取到程序里。因此硬盘既是输入设备,又是输出设备。
那缓冲区又是什么呢?系统为了避免频繁地从输入/输出设备存取数据,在内存中预留了一块内存区域作为缓冲使用,这块区域就被称作缓冲区(buffer)。 有了缓冲区之后,向输出设备输出的数据不会直接进入输出设备,而是在缓冲区中累积到一定量之后再写入输出设备;而从输入设备读取数据时,则是一次性读取一整块数据放到缓冲区中,然后再将程序真正请求的那一小部分数据从缓冲区中取出来交给程序使用。由于缓冲区是一块内存区域,因此对缓冲区的操作速度远远大于对输入/输出设备的访问,从而起到加速的作用。以操作系统为磁盘准备的缓冲区为例:某应用程序向操作系统发送五次请求,分别向硬盘写入10个字节。假如该操作系统不提供硬盘缓冲区的支持,那么每当操作系统接收到程序的请求时就会直接将数据写入硬盘,所以前后就会发生五次硬盘写入的操作;如果操作系统支持硬盘缓冲区,而且缓冲区的大小大于50字节,那么数据就会被放入缓冲区,操作系统随后选一个合适的时机将数据写入硬盘(例如缓冲区已填满的时候),这样只需一次硬盘写入的操作即可,相当于节省了4/5的时间。下图解释了缓冲区的工作原理。
图 112 缓冲区对写入操作的影响
通常缓冲区分为输入和输出两部分,因此不会出现新的输入数据将缓冲区中的输出数据覆盖的情况,请大家放心。
流是一个抽象的概念,通常代表一个连续的字节序列。与输入/输出设备类似,流入程序的字节序列称作输入流,而流出程序的字节序列被称作输出流。如果存在缓冲区,那么流会进入缓冲区,然后由缓冲区在合适的时机将流数据传递给相应的设备或程序;如果不存在缓冲区,那么流将直接进入相关设备或程序。一般而言,程序员无须特意关注缓冲区的实现,只有在特殊情况下才需要强制将缓冲区中的内容交给输入/输出设备,该操作叫做刷新缓冲区。
C语言程序会默认打开三个流,分别是标准输入流(stdin,全称standard input)、标准输出流(stdout,全称standard output)和标准错误输出流(stderr)。这三个标准流都是文本流。这三个流都是在stdio.h头文件中定义的,因此在使用输出/输出时必须包含头文件stdio.h。
一些流函数只能通过标准流进行操作,例如printf只能用来输出到stdout,perror只能用来输出到stderr,scanf只能从stdin读入数据,等等。还有一些流函数可以由开发者指定一个流作为数据来源或输出目标,例如fprintf、fputs等等,如果指定一个文件流作为这些函数的参数,那么它们就可以操作文件了。下文中会详细介绍这些函数的用法。
例程11-1演示了如何使用fprintf实现与printf相同的效果。
例程 111 使用fprintf输出到标准输出流stdout
1 #include <stdio.h>
1 int main()
2 {
3 fprintf(stdout, "Hello, world!\n");
4 }
程序的运行结果如图11-3所示:
图 113 使用fprintf输出到标准输出流stdout
fprintf使用流作为第一个参数,后面的参数和printf完全相同。稍后会在“文件基本操作”一节中详细介绍该函数的使用方法。
11.1.4 重定向
在开始使用C语言程序操作文件之前,先来接触一下重定向这个概念。
上一小节中讲到,在默认情况下,程序从stdin(标准输入流)中获取输入,向stdout(标准输出流)中进行输出。“重定向”就是指将stdin或stdou更改为其它文件或设备。与直接操作文件相比,重定向的功能比较有限(例如数据一旦输出就无法更改了),但是用起来十分简单,一般不需要对直接操作stdin和stdout的程序做任何修改即可通过重定向符来使用文件。
1、输入重定向
例程11-2可以完成循环读入字符并将其输出的功能,在VS中新建一个名为Echo的工程,输入上述代码,编译为Echo.exe。
例程 112 输出读入的文本
1 #include <stdio.h>
5 int main()
6 {
7 int ch;
8 ch = getchar();
9 while(ch != EOF)
10 {
11 putchar(ch);
12 ch = getchar();
13 }
14 }
程序的运行结果如图11-4所示:
图 114 Echo程序正在等待输入
通过输入重定向符“<”可以将指定文件中的文本通过stdin输入到Echo程序中,这样putchar获取的字符就是文件的内容了。请在Echo.exe所在的文件夹下建立一个名为data.txt的文本文件,并输入以下内容:
图 115 data.txt中的内容
通过下列语句可以将data.txt中的内容重定向给Echo.exe的stdin:
Echo.exe < data.txt
程序的运行结果如图11-6所示:
图 116 使用重定向符进行输入
2、输出重定向
也可以使用输出重定向符“>”将Echo.exe打印到屏幕上的内容输出到另一个文件中。使用下列语句可以将Echo.exe的stdout重定向到文件data.txt:
Echo.exe > data.txt
运行后输入内容并按回车键,会发现屏幕上没有回显。按Ctrl + Z键输入EOF并回车,程序终止,打开data.txt,会发现刚才输入的所有内容都在文件里,如图11-7所示:
图 117 使用重定向符进行输出
3、混合重定向
在命令行中输入下列语句即可实现将in.txt重定向为Echo.exe的stdin,并将out.txt重定向为Echo.exe的stdout。由于Echo.exe原原本本地将输入的内容打印出来,这条语句实现的功能是将文件in.txt复制为out.txt。
Echo.exe < in.txt > out.txt
11.2 文件的常用操作
11.2.1 使用文件指针
文件指针FILE *是一个指向结构体FILE的指针。结构体FILE是头文件stdio.h中定义的一个类型,存储了和这个结构体所对应的文件的相关信息,例如文件描述符、输入/输出缓冲区的指针、大小和当前位置等等。这个结构体的内部成员是对开发者隐藏的,一般而言,开发者不应该直接修改这个结构体内部成员的值。
脚下留心:文件指针不是指向文件内容的指针
请注意,文件指针不是指向文件内容的指针。对于指针FILE *p,p指向了的是文件信息结构体,这个结构体对应的文件有可能是打开状态,也有可能是关闭状态,但无论如何,要想访问文件内容,必须通过使用文件操作函数从对应的文件中取得数据,而不能使用p作为文件内容的指针,试图从p中读取文件数据。下一小节将介绍如何使用文件操作函数读取文件数据。
11.2.2 文件的基本操作步骤
在C语言中,文件操作的基本步骤如图11-8所示:
图 118 文件操作的基本流程
在开发者计划处理文件之前,首先通过指定文件路径和文件名打开那个文件。在打开文件之前,程序是无法对这个文件进行任何操作的,无法向该文件中写入内容,也无法从该文件里读取数据。使用fopen函数打开文件后,程序会得到一个FILE *指针,后面的所有文件操作都要用到这个指针。之后程序应当使用fread、fwrite、fgets、fputs等文件操作函数从文件中把数据读出或将所需数据写入文件。完成所有操作后,应及时使用fclose函数将打开的文件关闭。关闭文件时,如果其相应的缓冲区中有数据的话会自动清空,并写入文件中。一般而言,一个程序能够同时打开的文件数量是有限的(虽然有至少上千个,但也是有限制的),使用文件后及时关闭文件既是良好的编程习惯,也是节约系统资源的做法。
下面将演示如何以文本视图写入模式打开一个文件,写入“Hello, world!”,再关闭文件的过程,如例程11-3所示:
例程 113 简单的文件操作例程
1 #include <stdio.h>
15 int main()
16 {
17 FILE* fp;
18 fp = fopen("hello.txt", "w");
19 if(fp == NULL)
20 {
21 printf("无法打开 hello.txt。\n");
22 return -1;
23 }
24 fputs("Hello, world!\n", fp);
25 fclose(fp);
26 printf("文件写入结束。\n");
27 return 0;
28 }
程序的运行结果如图11-9所示:
图 119 简单文件操作例程
请编译并运行该程序,如果一切正常的话(例如当前hello.txt没有被另一个程序占用),可以看到如上11-9的输出结果。
这时在工程文件夹下应当可以看到hello.txt文件,打开后其内容如图11-10所示:
图 1110 简单文件操作例程的运行结果
这样就完成了第一次操作文件的过程,一定很有成就感吧?下一节中将介绍更多有关文件基本操作的函数和方法。
动手体验:在D盘根目录下创建文件
请试着修改上述示例程序,实现在D盘根目录下创建名为world.txt的文件,并写入一些字符串。
为了在不同的文件夹下创建文件,需要修改第七行中的文件名部分。D盘根目录下的world.txt文件的路径是“D:\world.txt”。同样地,盘符“D”的大小写也是无所谓的。注意使用转义符对路径中相应的符号进行转义!
11.2.3 打开文件
C语言程序使用fopen函数打开文件,并得到相应的文件指针。该函数的原型如下:
FILE* fopen(char* filename, char* mode);
fopen函数的第一个参数filename是文件的全路径,更确切地说,是一个一个指向存储了文件完整路径的字符串的指针。如果路径不包括盘符,且不以“\”开头,那么这个路径是相对路径(相对于程序当前的运行目录);反之这个文件路径就是绝对路径(与程序当前的运行目录无关)。
下面的代码都可以用来打开程序当前运行目录下的“test.txt”文件:
FILE* fp = fopen("test.txt", "w"); /* 第一种方式 */
FILE* fp = fopen(".\\test.txt", "w"); /* 第二种方式 */
/* 第三种方式 */
char filename[] = "test.txt";
FILE* fp = fopen(filename, "w");
下面的代码可以用来打开D盘下hello文件夹内的“world.txt”文件:
FILE* fp = fopen("D:\\hello\\world.txt", "w");
fopen函数的第二个参数是一个用来指定文件打开模式的字符串。所有可用的文件打开模式如表11-1所示:
表 111 文件打开模式
打开模式 |
名称 |
视图模式 |
描述 |
r/rt |
读取模式 |
文本 |
打开文件,只允许读取数据。目标文件必须存在 |
w/wt |
写入模式 |
文本 |
打开文件,只允许写入数据。若目标文件已存在,则覆盖原文件;否则创建新文件 |
a/at |
追加模式 |
文本 |
打开文件,只允许追加数据(在文件末尾写入)。若目标文件已存在,则覆盖原文件;否则创建新文件 |
r+ |
读取/更新模式 |
文本 |
打开文件,可以进行读取和写入。目标文件必须存在 |
w+ |
写入/更新模式 |
文本 |
打开文件,可以进行读取和写入。若目标文件已存在,则覆盖原文件;否则创建新文件 |
a+ |
追加/更新模式 |
文本 |
打开文件,允许读取和追加数据。若目标文件已存在,则覆盖原文件;否则创建新文件 |
若在除了rt、wt和at以外的每个打开模式字符串的字母后加上“b”,则是以二进制视图模式打开文件,其它操作完全相同。为便于参考,列举如下:rb,wb,ab,rb+,wb+,ab+。
脚下留心:小心覆盖已有文件的“w”模式
以任意一个“w”模式打开文件都会覆盖已有文件,就算不写入任何数据,仅仅使用fopen打开也会清空文件已有的内容。因此在使用“w”模式的时候一定要多加小心!
脚下留心:文件打开模式是字符串,不是字符
大黄写了下面的程序用来测试打开文件函数,但是运行时会报错。请问大家知道是为什么吗?
例程 114 错误的文件打开模式
1 #include <stdio.h>
29 int main()
30 {
31 FILE* fp;
32 fp = fopen("hello.txt", 'w');
33 if(fp == NULL)
34 {
35 printf("无法打开 hello.txt。\n");
36 return -1;
37 }
38 fclose(fp);
39 return 0;
40 }
编译并运行后立刻出错了:
图 1111 打开文件后程序出错
这个错误的关键在于,大黄用'w'替代了字符串"w"作为fopen的第二个参数。原本fopen函数希望第二个参数是字符串指针,'w'被自动转换成一个值为119的字符串指针,并被fopen作为指针使用去访问内存了。但是内存地址119并不合法,因此程序访问内存时出现错误,便报错退出了。
所以请一定使用字符串(或字符串指针)作为filemode的参数,而不是单个字符——即使原来的文件打开模式只有一个字符也是一样。
调用fopen函数之后,一般情况下都可以正常打开文件,这时将返回一个非NULL的指针。如果文件打开失败,fopen将返回NULL。因此在fopen后应当判断函数的返回值,若返回一个空指针,应首先进行必要的错误处理,而不能直接继续进行其它文件操作。
下面的例程展示了如何根据fopen函数的返回值进行必要的错误处理,如例程11-5所示:
例程 115 文件处理时进行必要的错误处理
1 #include <stdio.h>
41 int main()
42 {
43 FILE* fp;
44 fp = fopen("C:\\", "r");
45 if(fp == NULL)
46 {
47 printf("文件打开失败,请检查可能发生的错误。\n");
48 }
49 else
50 {
51 printf("文件成功打开!\n");
52 /* 进行其它文件操作 */
53 fclose(fp);
54 }
55 }
程序的运行结果如图11-12所示:
图 1112 文件处理时进行必要的错误处理
程序中故意使用了“C:\”作为文件路径——因为“C:\”是C分区的根目录,不能作为文件打开,所以fopen一定会失败。判断fopen的返回值fp是否为空指针,如果为空,则提示用户文件打开失败;否则继续进行其它文件操作。
11.2.4 关闭文件
幼儿园的老师最喜欢说:“用完别人的东西要及时归还。”这道理在程序的世界里也是相同的。C语言程序里使用fopen函数打开一个文件,好比程序向系统借了这个文件的使用权;用完该文件之后自然要及时将使用权交还回去,这样其它的程序才能再次打开(借用)这个文件。
在C语言中可通过调用fclose函数来关闭一个已经打开的文件。fclose函数的原型如下:
int fclose(FILE* p);
只要将成功打开的文件指针作为唯一参数传递给fclose函数即可。如果fclose返回0,意味着文件成功关闭,否则就是出现了错误。不过一般而言,关闭文件不会发生错误,而且就算发生了错误,原来的文件指针p也已经失效。因此在调用fclose之后,一般来说无需检查返回值,且不能再次使用原来的文件指针进行任何文件操作。
11.2.5 读写文件
成功地打开文件后,先来学习最简单的文件读写方法:从文件中读入一个字符,以及向文件中写入一个字符。假定已经打开了一个文件,文件指针为fp。下面的语句表示从fp对应的文件中读入一个字符:
char ch;
ch = getc(fp);
下面的语句表示向fp对应的文件写入一个字符:
char ch;
putc(ch, fp);
本章开头讲过,C语言程序会默认打开三个流,分别是stdin(标准输入流)、stdout(标准输出流)和stderr(标准错误输出流)。这三个流和文件指针完全相同,因此getc和putc函数也可以用来操作这三个流。下面的例程演示了如何使用getc函数从键盘读入一个字符,将这个字符对应的ASCII码加一,再输出到屏幕上。
例程 116 使用getc和putc操作标准输入输出流
1 #include <stdio.h>
56 int main()
57 {
58 char ch;
59 ch = fgetc(stdin);
60 ++ch;
61 fputc(ch, stdout);
62 }
程序的运行结果如图11-13所示:
图 1113 使用getc和putc操作标准输入输出流
下面将演示了如何打开一个真正的文件(程序运行目录下的hello.txt),写入26个英文字母,重新打开该文件,再逐个字符读出的过程,如例程11-7所示:
例程 117 输出并读入英文字母表
1 #include <stdio.h>
63 int main()
64 {
65 FILE *fp;
66 int i;
67 char ch;
68 fp = fopen("hello.txt", "w");
69 if(fp == NULL)
70 {
71 printf("打开文件失败!\n");
72 return -1;
73 }
74 for(i = 0; i < 26; ++i)
75 {
76 fputc('A' + i, fp);
77 }
78 fclose(fp);
79 printf("写入完毕,请打开 hello.txt 文件手动检查内容是否正确写入。\n");
80 system("pause");
81 fp = fopen("hello.txt", "r");
82 if(fp == NULL)
83 {
84 printf("打开文件失败!\n");
85 return -1;
86 }
87 ch = fgetc(fp);
88 while(ch != EOF)
89 {
90 printf("%c", ch);
91 ch = fgetc(fp);
92 }
93 printf("\n");
94 fclose(fp);
95 printf("全部输出完毕。\n");
96 }
程序的运行结果如图11-14所示:
图 1114 输出并读入英文字母表
运行程序后,运行文件夹下应当生成hello.txt文件。
程序中27至31行是循环从文件中读出字符,直到读到文件结束为止。文件中没有新的字符可供读出的情形称为到达文件末尾。到达文件末尾后,fgetc函数会返回一个特殊值EOF,只要判断fgetc的返回值是否为EOF即可知道是否将整个文件读完。
第一次试图使用循环读取文件的时候,可能写出下面的代码:
例程 118 不好的实现方式
FILE* fp;
/* 打开文件,fp为相应的文件指针 */
char ch;
while(ch != EOF)
{
fgetc(ch);
printf("%c", ch);
}
例程11-8有两个问题:首先,ch的值在循环开始时是不确定的。如果未初始化的ch刚好等于EOF的值(一般EOF的值为-1,是在stdio.h中定义的常量),那就不会进入循环了。其次,如果读到了文件末尾,通过fgetc读到值就是EOF,printf同样会将EOF作为一个字符输出到屏幕上,这显然是不正确的。因此在今后使用循环读取整个文件的内容时,建议直接使用原例程中29至34行的标准实现方式。
11.2.6 按行读写文件
fputs和fgets函数只能以单个字符为单位进行文件读写,这显然是很低效的。C语言提供了更高效的文件读写函数供使用。fgets函数可以实现读入一行字符串,或读入指定长度的字符串。下面是fgets的调用示例:
#define MAX 255
char buf[MAX];
FILE* fp;
/* 打开文件,fp为相应的文件指针 */
fgets(buf, MAX, fp);
fgets将读取一个字符串,直到下面任何一种情况发生:
• 读到文件末尾;
• 读取了(MAX - 1)个字符;
• 读到换行符。
前两种情况下,fgets向buf中写入读到的全部字符,并在末尾添加'\0'(空字符)以表示字符串的结束。第三种情况下,fgets会将换行符也放在读取的字符串末尾,再在后面添加空字符来结束字符串。如果fgets从文件末尾开始读取(即遇到EOF),则将返回NULL。可以通过判断fgets的返回值来确定是否读取到达文件末尾。
fputs函数将指定的字符串写入指定的文件。调用示例如下:
char buf[] = "Hello, world!";
FILE* fp;
/* 打开文件,fp 为相应的文件指针 */
fputs(buf, fp);
fputs将把字符串buf原原本本地写入文件,而不会在末尾添加换行符。由于fgets在字符串末尾会保留换行符,而fputs并不会自作主张地在输出时再次添加换行符,因此这两个函数可以很好地配合使用:用fgets从一个文本文件里读入字符串,然后用fputs原样输出到另一个文本文件中,可以得到一份完整的拷贝。
11.2.7 格式化文件输入输出
请回顾之前讲过的格式化输入输出函数:函数printf和scanf分别用来完成格式化输入和格式化输出的功能。printf的输入来自于stdin(标准输入流),scanf的输出目的地是stdout(标准输出流)。同样地,可以以文件流作为格式化输入输出的来源和目的地,只要使用fprintf和fscanf即可。前缀“f”表示这是以文件流作为操作对象的函数。
下面的语句实现了向文件流fp中写入字符串“Hello, world”的效果:
fprintf(fp, "Hello, world");
下面的语句则是从文件流fp中读入一个整数,并存在变量d中:
fscanf(fp, "%d", &d);
与操作标准流的scanf和printf相比,fscanf和fprintf除了在函数的变量列表之前加上一个文件流指针作为第一个参数之外,在使用上没有其它差别了。
11.3 文件的高级操作
11.3.1 随机读写文件
上一小节中介绍的读写文件方法只能顺序读取文件。C语言程序在内部维护了一个数据结构用以保存已打开文件的当前状态,该数据结构中存储了文件当前读取和写入的位置,称为文件位置指针。
文件位置指针指向的位置使用相对于文件起始位置的偏移字节数来表示。刚打开文件时,文件位置指针指向文件的开头,其值为0;每读取一个长度为N的字符串,文件位置指针就向后移动N个字节;同样地,每写入一个长度为N的字符串,文件位置指针也会向后移动N个字节。
“随机读写文件”与顺序读写文件不同,要求能够在读写文件时调整文件指针的位置,从而能够做到不按照指定顺序读取文件的内容。如果想从文件的任意位置读取数据,需要使用特定函数操作文件指针。
1. 得到文件位置指针的当前位置
函数ftell用来获取文件位置指针当前指向的位置。随机读写文件时,程序不容易确定文件位置指针的当前位置,使用ftell就可以轻松确定当前位置。下面的语句用来获取文件指针fp指向的文件的文件位置指针所在位置。
long n = ftell(fp);
ftell的返回值类型是long,这是因为文件大小可能超过unsigned int型变量能表示的最大值(大约4GB),所以文件位置指针的值也可能超过unsigned int型变量所允许的最大值。
2. 修改文件位置指针的位置
fseek函数用来修改文件指针的位置,它的原型如下:
int fseek(FILE* fp, long ing offset, int origin);
第一个参数fp是待操作的文件流指针,第二个参数offset是新的位移,第三个参数origin决定了fseek函数要如何解读位移。成功执行后,fseek返回0;否则返回非0值。
origin的所有可能取值如表11-2所示。
表 112 fseek第三个参数origin的可能取值
值 |
描述 |
数字值 |
SEEK_SET |
从文件开头计算偏移 |
0 |
SEEK_CUR |
相对于文件位置指针当前位置的偏移 |
1 |
SEEK_END |
相对于文件末尾的偏移 |
2 |
例如将origin取为SEEK_SET,下面语句的含义是“将文件位置指针移动到文件第五个字节的地方”:
fseek(fp, 5, SEEK_SET);
若origin为SEEK_END,则下面语句的含义是“将文件位置指针移动到离文件末尾五个字节的地方”:
fseep(fp, -5, SEEK_END);
脚下留心:
SEEK_SET、SEEK_CUR和SEEK_END都是在stdio.h中定义的常量,为保证代码的兼容性和可读性,在代码中请不要直接使用数字0、1、2替换这三个常量。
多学一招:使用fseek和ftell函数获取文件大小
下面的例程使用ftell函数获取指定文件的大小。
例程 119 使用fseek和ftell函数获取文件大小
1 #include <stdio.h>
97 long getFileSize(char* filepath)
98 {
99 long size = 0;
100 FILE* fp = fopen(filepath, "rb");
101 if(fp == NULL)
102 {
103 return -1;
104 }
105 fseek(fp, 0, SEEK_END);
106 size = ftell(fp);
107 fclose(fp);
108 return size;
109 }
110 int main()
111 {
112 char filepath[255];
113 long size = 0;
114 printf("请输入文件路径:");
115 scanf("%s", filepath);
116 size = getFileSize(filepath);
117 if(size == -1)
118 {
119 printf("打开文件失败。\n");
120 }
121 else
122 {
123 printf("文件大小为 %ld 字节。\n", size);
124 }
125 }
程序的运行结果如图11-15所示:
图 1115 使用fseek和ftell函数获取文件大小
为了保证程序打开文件后看到的内容与文件的原始数据完全一致,程序中使用二进制视图打开文件。
3. 使文件位置指针指向文件开头
函数rewind可以使文件位置指针重新指向文件开头,调用方式如下:
FILE* fp;
/* 打开文件,并使fp为指向文件流的指针 */
/* 进行各种文件操作 */
rewind(fp); /* 执行后,文件位置指针重新指向文件的开头 */
上述代码与下面的语句等价:
fseek(fp, 0, SEEK_SET);
11.3.2 统计文件内容
在办公中一个常用的功能就是统计文件的内容,如单词数目、数字数目、标点符号数目等等。下面实现了对文件中大小写英文字母、空格、数字及其它字符数量的统计功能,如例程11-10所示:
例程 1110 统计文本文件中的内容
1 #include <stdio.h>
126 int main()
127 {
128 char filename[255];
129 FILE* fp;
130 char ch;
131 long capitalLetters = 0, smallLetters = 0, digits = 0, spaces = 0, others = 0;
132 printf("请输出待统计文件的路径:");
133 scanf("%s", filename);
134 fp = fopen(filename, "r");
135 if(fp == NULL)
136 {
137 printf("打开文件时发生错误。\n");
138 return -1;
139 }
140 ch = fgetc(fp);
141 while(ch != EOF)
142 {
143 if(ch >= 'A' && ch <= 'Z')
144 {
145 ++capitalLetters;
146 }
147 else if(ch >= 'a' && ch <= 'z')
148 {
149 ++smallLetters;
150 }
151 else if(ch >= '0' && ch <= '9')
152 {
153 ++digits;
154 }
155 else if(ch == ' ')
156 {
157 ++spaces;
158 }
159 else
160 {
161 ++others;
162 }
163 ch = fgetc(fp);
164 }
165 printf("统计结束。\n");
166 printf("文件中有 %ld 个大写字母,%ld 个小写字母,%ld 个数字,%ld 个空格,以及 %ld 个其它字符。\n",
167 capitalLetters, smallLetters, digits, spaces, others);
168 }
程序的运行结果如图11-16所示:
图 1116 统计文本文件中的内容
由于例程11-10中只统计文本文件中各种类型字符的数量,因此例程11-10中使用文本视图打开待统计的文件。请注意,统计量存放在五个long型变量中,一定要为这五个变量设置初始值。
11.3.3 错误处理
在C语言中进行文件操作时可能遇到各种错误,最常见的错误是由于文件打开方式错误、文件权限不足和硬件错误(例如拔出正在写入文件的U盘)造成文件打开或读写出错。
1. 错误的打开方式
下面的例程使用只写模式“w”打开文件hello.txt,但却尝试从文件中读入数据,如例程11-11所示:
例程 1111 错误的文件打开方式
1 #include <stdio.h>
169 int main()
170 {
171 FILE* fp;
172 char ch;
173 fp = fopen("hello.txt", "w");
174 if(fp == NULL)
175 {
176 printf("无法打开 hello.txt。\n");
177 return -1;
178 }
179 fputs("Hello, world!\n", fp);
180 fseek(fp, 0, SEEK_SET);
181 printf("文件写入结束。\n");
182 ch = fgetc(fp);
183 while(ch != EOF)
184 {
185 putc(ch, stdout);
186 }
187 printf("文件读取结束。\n");
188 fclose(fp);
189 return 0;
190 }
程序的运行结果如图11-17所示:
图 1117 错误的文件打开方式
这意味着程序并没有从文件中读入任何字符。由于文件的打开模式并不包含读取权限,因此第18行调用fgetc试图读取文件时发生错误,直接返回了EOF。程序认为读取已经结束,因此直接输出了“文件读取结束”字样。
C语言提供了feof和ferror函数用来判断文件流的当前状态。feof可以判断文件流是否已经到达文件尾,若返回非零值则意味着文件流到达文件尾了。ferror可以判断文件流是否发生错误,同样地,若返回则意味着文件流发生了错误。
下面的程序实现了对文件操作是否发生错误的判断,并对用户做出相应的提示,如例程11-12所示:
例程 1112 判断文件操作是否发生错误
1 #include <stdio.h>
191 int main()
192 {
193 FILE* fp;
194 char ch;
195 fp = fopen("hello.txt", "w");
196 if(fp == NULL)
197 {
198 printf("无法打开 hello.txt。\n");
199 return -1;
200 }
201 fputs("Hello, world!\n", fp);
202 fseek(fp, 0, SEEK_SET);
203 printf("文件写入结束。\n");
204 ch = fgetc(fp);
205 while(ch != EOF)
206 {
207 putc(ch, stdout);
208 }
209 if(ferror(fp))
210 {
211 printf("文件读取过程中发生错误。\n");
212 }
213 else
214 {
215 if(feof(fp))
216 {
217 printf("文件读取结束。\n");
218 }
219 else
220 {
221 printf("不可能执行这行语句。\n");
222 }
223 }
224 fclose(fp);
225 return 0;
226 }
程序的运行结果如图11-18所示:
图 1118 判断文件操作是否发生错误
程序中第26行的判断是为了展示feof的用法而设,实际中无需另行调用feof来判断文件是否读取结束——因为只有fgetc函数返回EOF时才会退出循环,而在20行又已经通过ferror函数判断了是否发生读取错误,因此当程序执行到24行的else时只有一种可能,就是文件读取已经结束了。
2. 文件权限不足
计算机的文件系统为每个文件维护了一个属性集,尽管不同的文件系统提供了不尽相同的文件属性,但这里以常见的只读属性为例进行介绍。下面的例程试图以只写模式打开程序运行目录下的hello.txt文件,但是这个文件被设置了只读属性,如图11-19:
图 1119 为文件设置只读属性
程序实现如下例程11-13:
例程 1113 向只读文件中写入数据
1 #include <stdio.h>
227 int main()
228 {
229 FILE* fp;
230 char ch;
231 fp = fopen("hello.txt", "w");
232 if(fp == NULL)
233 {
234 printf("无法打开 hello.txt。\n");
235 return -1;
236 }
237 fputs("Hello, world!\n", fp);
238 printf("文件写入结束。\n");
239 fclose(fp);
240 return 0;
241 }
程序的运行结果如图11-20所示:
图 1120 向只读文件中写入数据
运行程序后提示错误,要解决这个问题,只要去掉hello.txt文件的只读属性即可。
11.3.4 文件的加密与解密
随着社会的发展,人们对个人隐私越来越重视,因此对数据进行加密也变得越来越重要。本节将带领大家完成一个文件加解密小工具,使用这个工具,可以对文件中的进行基础的保护,更好地保护用户的隐私。
现代加密的基础是异或运算(xor)。在“运算符”一章中介绍过异或运算,它是位运算的一种,其对应的运算符为“^”。异或运算的特点是:一个数与另一个数异或两次,就能得到它本身。利用这个特性就可以实现文件的加密和解密了。基本思路是将文件中的数据(称为明文)和用户输入的密钥进行逐位异或,从而得到加密后的文件(称为密文);解密时需要用户输入完全相同的密钥,将密文与密钥逐位异或,从而得到原始文件数据。
明文、密钥和密文的关系如下式所示:
明文 ^ 密钥 = 密文
密文 ^ 密钥 = 明文 ^ 密钥 ^ 密钥 = 明文
这也保证了输入错误的密码无法解密文件。解密时,只要使用同样的密码再次运行程序即可。
实现文件加密与解密的例程如例程11-14,供参考。
例程 1114 文件的加密与解密
1 #include <stdio.h>
242 #include <string.h>
243 /* in 有读取权限,out 有写入权限 */
244 void enc_dec(FILE* in, FILE* out, char* password, int passwordSize)
245 {
246 long pos = 0;
247 int ch;
248 ch = fgetc(in);
249 while(ch != EOF)
250 {
251 ch = ch ^ (password[pos % passwordSize]);
252 fputc(ch, out);
253 ++pos;
254 ch = fgetc(in);
255 }
256 }
257 int main()
258 {
259 char filepathIn[255], filepathOut[255];
260 char password[101];
261 int passwordSize;
262 FILE *in, *out;
263 printf("请输入需要加密/解密的文件路径:");
264 scanf("%s", filepathIn);
265 printf("请输入目的文件路径:");
266 scanf("%s", filepathOut);
267 in = fopen(filepathIn, "rb");
268 if(in == NULL)
269 {
270 printf("打开源文件时发生错误。\n");
271 return -1;
272 }
273 out = fopen(filepathOut, "wb");
274 if(out == NULL)
275 {
276 printf("打开目标文件时发生错误。\n");
277 return -1;
278 }
279 printf("请输入密码(最大 100 字节):");
280 scanf("%s", password);
281 passwordSize = strlen(password);
282 enc_dec(in, out, password, passwordSize);
283 fclose(in);
284 fclose(out);
285 printf("加密/解密结束。\n");
286 }
程序的运行结果如图11-21所示:
图 1121 文件的加密与解密
下面是加密前后的两个文本文件的对比。
图 1122 文本文件加密前后的对比
实现中应注意如下几点:
• 由于加密和解密的算法完全相同,因此程序中只实现了一个enc_dec函数,同时用在加密过程和解密过程中;
• 为保证读入的数据和文件中的数据完全一致,应使用二进制视图打开源文件和目标文件;
• 密码的最大长度为100,由于字符串的末尾有空字符'\0',因此存放密码的数组password的大小应为101个字节。
• 为保证原始数据的安全,可以考虑加密后将原始文件删除。
11.4 本章小结
本章介绍了文件流的基础知识及操作文件流可用的函数,并通过大量实例来演示这些函数的使用方法。全书末尾的“综合案例”部分还会进一步使用更多的文件操作,希望大家能做到举一反三,真正掌握使用C语言进行文件处理的方法。
- 点赞
- 收藏
- 关注作者
评论(0)