1、绪论
文件是程序设计中的一个重要概念。要向外部介质上存储数据也必须先建立一个文件(以文件名标识),才能向它输出数据。,所谓“文件”一般是指存储在外部介质上数据的集合。一批文件是以数据的形式存放在外部介质(如磁盘)上的。操作系统是以文件为单位对数据进行管理的,也就是说,如果想找存在外部介质上的数据,必须先按文件名找到指定的文件,然后再从该文件中读取数据,在程序运行时,常常需要将一些数据(运行的最终结果或中间数据)输出到磁盘上存放起来,以后需要时再从磁盘中输入到计算机的内存。这就要用到磁盘文件。
在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。 例如:
通常把显示器称为标准输出文件,printf 就是向这个文件输出数据; 通常把键盘称为标准输入文件 , scanf 就是从这个文件读取数据
我们不去探讨硬件设备是如何被映射成文件的,只需要记住,在C语言中硬件设备可以看成文件,有些输入输出函数不需要你指明到底读写哪个文件,OS已经为它们设置了默认的文件,当然也可以更改,例如让 printf 向磁盘上的文件输出数据。
程序为什么要文件?
配置:Unix用文本。windows用注册表 数据:稍微有点量大的数据都放数据库了 媒体:这个是必须只能二进制的
而在现实中,程序通过第三方库来读写文件,很少直接读写二进制文件了,因为这样的二进制文件不具有可移植性,在int为32的机器上写成的数据文件,无法直接在int为64的机器上正确的读出,所以更好的方案是用文本
文件流 : 一个C文件就是一个字节流或二进制流,所有的文件(保存在磁盘)都要载入内存才能处理,所有的数据必须写入文件(磁盘)才不会丢失。数据在文件和内存之间传递的过程叫做文件流,类似水从一个地方流动到另一个地方。数据从文件复制到内存的过程叫做输入流,从内存保存到文件的过程叫做输出流。
文件是数据源的一种,除了文件,还有数据库、网络、键盘等;数据传递到内存也就是保存到C语言的变量
输入输出 ( I/O ) 是指程序(内存)与外部设备(键盘、显示器、磁盘、其他计算机等)进行交互的操作。几乎所有的程序都有输入与输出操作,如从键盘上读取数据,从本地或网络上的文件读取数据或写入数据等。通过输入和输出操作可以从外界接收信息,或者是把信息传递给外界。 我们可以说,打开文件就是打开了一个流。
文本文件与二进制
文本文件:
ASCII码文件也称为文本文件,它是一种计算机文件,它是一种典型的顺序文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存放对应的ASCII码,其文件的逻辑结构又是属于流式文件。特别的是,文本文件是指以ASCII码方式(也称文本方式)存储的文件,更确切地说,英文、数字等字符存储的是ASCII码,而汉字存储的是机内码。文本文件中除了存储文件有效字符信息(包括能用ASCII码字符表示的回车、换行等信息)外,不能存储其他任何信息。 文本文件是一种由若干行字符构成的计算机文件。文本文件存在于计算机文件系统中。通常,通过在文本文件最后一行后放置文件结束标志来指明文件的结束。
二进制文件:
二进制是按照二进制的编码来存放的,虽然也可以在屏幕上显示,但其内容无法读懂,C系统在处理这些文件的时候,并不区别类型,都看成字符流,按字节进行处理,它是基于值编码的文件,你可以根据具体应用,指定某个值是什么意思(这样一个过程,可以看作是自定义编码)。用户一般不能直接读懂它们,只有通过相应的软件才能将其显示出来。二进制文件一般是可执行程序、图形、图像、声音等等。因此它也称为是“流式文件”的一种。
区别:
文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。这两者只是在编码层次上有差异,文本文件基本上是定长编码的,而二进制文件则可看成是变长编码,因为是值编码,多少个比特代表一个值,完全由你决定。
文本流是解释性的,最长可达到255个字符,其中回车,换行都会被转换成换行符“\n”,(如果以“文本”的方式打开一个文件,那么在读字符的时候,系统会把所有的“\r\n转换成“\n”,在写入时把”\n“转换成”\r\n“),二进制是非解释性的,一次处理一个字符,并且不转换字符。
存储方式上的区别: 文本工具打开一个文件,首先读取文件物理上所对应的二进制比特流,然后按照所选择的解码方式来解释这个流,然后将解释结果显示出来。 一般来说,选取的解码方式会是ASCII码形式(ASCII码的一个字符是8个比特),接下来,它8个比特8个比特地来解释这个文件流。 记事本无论打开什么文件都按既定的字符编码工作(如ASCII码),所以当打开二进制文件时,出现乱码也是很必然的一件事情了,解码和译码不对应。 文本文件的存储与其读取基本上是个逆过程。而二进制文件的存取与文本文件的存取差不多,只是编/解码方式不同而已。 二进制文件就是把内存中的数据按其在内存中存储的形式原样输出到磁盘中存放,即存放的是数据的原形式。文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,即存放的是数据的终端形式
二进制文件优点: 1、更节省空间, 2、使用二进制储存到文件就更快捷, 3、就是一些比较精确的数据,使用二进制储存不会造成有效位的丢失, 4、二进制文件编码是变长的,所以它灵活,存储利用率要高些,译码难一些 缺点:而二进制用的计算机原始语言,不存贮兼容性。
文本文件优点:
1、一般认为,文本文件编码基于字符定长,译码容易些 2、文本文件不一定是ASCII来存贮的,因为ASCII码只能表示128的标识,打开一个txt文档,然后另存为,有个选项是编码,可以选择存贮格式,一般来说编码格式兼容性要好一些(具有兼容性) 3、由于结构简单,文本文件被广泛用于记录信息。它能够避免其它文件格式遇到的一些问题
4、当文本文件中的部分信息出现错误时,往往能够比较容易的从错误中恢复出来,并继续处理其余的内容 缺点:(空间利用差)二进制文件甚至可以用一个比特来代表一个意思(位操作),而文本文件任何一个意思至少是一个字符.
总的来说:文本的优势是方便人们读写,而且跨平台,缺点是程序输入输出需要经过格式化,开销大,二进制优点是程序读写快,缺点是人们读写困难,且不跨平台,int大小不一样,带着大小端的问题
缓冲区:
程序执行时,在所提供的额外内存,可用来暂时存放做准备执行的数据。它的设置是为了提高存取效率,因为内存的存取速度比磁盘驱动器快得多。
当使用较低级的I/O函数 ( 包含在头文件 io.h 和 fcntl.h ) 中来直接对磁盘存取,这种方式的存取速度慢,并且由于不是C的标准函数,跨平台操作时容易出问题。
而使用标准I/O函数(包含在头文件(stdio.h中)时,系统会自动设置缓冲区,并通过数据流来读写文件。当进行文件读取时,不会直接对磁盘进行读取,而是先打开数据流,将磁盘上的文件信息拷贝到缓冲区内,然后程序再从缓冲区中读取所需数据。当写入文件时,并不会马上写入磁盘中,而是先写入缓冲区,只有在缓冲区已满或“关闭文件”时,才会将数据写入磁盘(这种方式是比较常用的)缓冲区的三种:
缓冲区的三种: 全缓冲:
填满标准I/O缓存区才进行实际的I/O操作。磁盘上的了件用标准I/O打开,默认都是全缓存的。当缓存区填满或者进行flush(清空)操作时候才会进行磁盘操作。 比如:内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,只要1024个字节都放满了,那么程序会立即来读取这1024个字节的数据。只要1024个字节没有放满,哪怕只放了1023个字节,程序都不会来读取,除非使用fflush函数
行缓冲:
当输入输出遇到换行符时候就是行缓存了。标准输入和标准输出都是行缓存。
比如:内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,假如放了10个字节的数据,你敲了回车键,那么程序就马上来读取了。假如放了20个字节,你敲了回车奖,程序也会来读取。所以即使1024个字节没有放满,但是你敲了回车键,程序就会来读取,这个就叫做行缓冲。
无缓冲:
不对I/O操作进行缓存,对流的读写可以立即操作实际文件。典型例子就是标准出错
比如:内存中有一段存储区域,比如有1024个字节大小,有一个程序会从这段存储区域中读取数据。现在系统把一个文件的内容放入这个存储区,刚放了1个字节,程序就马上来读取了;又放了一个字节,程序又马上来读取了,这就是没有缓冲。
注意:
1、C语言中规定,一般都必须设置缓冲区,但是使用scanf函数和getchar时,如果行缓冲的换行符没有处理好,程序运行可能会有异常或者闪退等现象。 2、一下情况可使缓冲区的清空: 缓冲区满时 , 行缓冲遇到回车时, 关闭文件时 , 使用特定函数刷新缓冲区
其实所有的文件最终都是二进制文件的,文本文件无非是用最简单的方式可以读写的文件,而二进制文件是需要专门的输入格式化,可能经过转码
2、预处理(头文件)
头文件是扩展名为 .h 的文件,在C语言家族程序中,头文件被大量使用。一般而言,每个C程序通常由头文件和定义文件组成。头文件作为一种包含功能函数、数据接口声明的载体文件,主要用于保存程序的声明,而定义文件用于保存程序的实现
头文件的主要作用在于多个代码文件全局变量(函数)的重用、防止定义的冲突,对各个被调用函数给出一个描述,其本身不需要包含程序的逻辑实现代码,它只起描述性作用,用户程序只需要按照头文件中的接口声明来调用相关函数或变量,链接器会从库中寻找相应的实际定义代码
头文件是用户应用程序和函数库之间的桥梁和纽带。在整个软件中,头文件不是最重要的部分,但它是C语言家族中不可缺少的组成部分。编译时,编译器通过头文件找到对应的函数库,进而把已引用函数的实际内容导出来代替原有函数。进而在硬件层面实现功能
有两种类型的头文件:用户自己编写的头文件和编译器自带的头文件(如<stdio.h>)。在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。就像#include<stdio.h>一样,但是也不一定是要在.C文件的最前面加#include它只是它把那个文件的全部文本内容原封不动地插入到它所在的地方,它在编译之前就处理了
作用是:把一些所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件,就可以让编译器知道你所需要的数据(如函数的原型),是为了保证你调用时给出的参数值和类型是正确的
注意: 一般只引用一次头文件。在同一个编译单元里边,同名的结构不能被重复声明,如果你在的头文件有结构的声明,很难这个头文件不会在一个编译单元里被include多次,如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。所以需要使用 “ 标准头文件结构 ” (条件编译)
标准头文件结构:
头文件中使用#ifdef和#ifndef是非常重要的,可以防止双重定义的错误
#ifndef _link_ //这里表示,这是个预编译指令,判断有没有定义_link_这个宏,没有就预编译下面的代码
#define _link_//在这里定义一个定义一个宏名为_link_ (这个名字是什么不重要,它只是做一个记录而已)
typedef struct _link{
int number;
struct _link *nxte;
}link;
#endif
//当再次引用头文件时,查看如果上面的宏不存在,不存在就预编译这个代码,存在就跳过,防止两次include"link",因为相同的类不能定义两次,否则就会出错
使用“标准头文件结构”,就可以保证这个头文件在一个编译单元中只会被#include一次。因为当你已经包含过这个文件,
一个名为_link_就会有了定义,那么#ifndef的条件为假,就不会再执行后面的宏定义了。#pragma once也能起到相同的作用,但不是所有的编译器都支持
上面的#ifndef主要是为避免重复包含头文件宏 ,有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句
“条件引用”:#ifdef 和 #if
#ifdef 标识符 //判断标识符
#define 标识符 //定义标识符
程序段1 //如果被定义 执行它
#else
程序段2 //如果被没定义 执行它
#endif
它的作用是:当标识符已经被定义过,则对 程序段1 进行编译,否则编译 程序段2。
其中 #else可选添加或不添加,当没有#else时,当标识符已经被定义过,则对 程序段1 进行编译,否则跳过
#ifdef的用法和 #ifndef相似,但是它们的逻辑相反, #ifdef表示如果编译器未定义标识符,且有#else指令,则执行#else和#endif之间的所有代码
#if 常量表达式 //判断表达式的值
#define 标识符 //定义标识符
//程序段1
#else //#else也是可选添加或不添加的
// 程序段2
#endif
它的作用是,如常量表达式的值为真(非0时为真),则对程序段1 进行编译,
否则对程序段2进行编译。因此可以使程序在不同条件下,完成不同的功能。它和条件语句if—else语句很像,
条件编译还有一个用途是让程序更加容易移植,改变文件开头部分的几个关键字的定义,
即可根据不同的系统设置不同的值和包含不同的文件
注: 上面这里的“程序段”可以是语句组,也可以是命令行。
补充:一个.C文件就一个编译单元,编译器每次只处理一个编译单元,打开两个以上将会出错
3、文件的链接
在大型项目中,仅仅一个源文件是不够的,巨大的代码量需要分别放在几个文件中,当然分开存放的更主要的目的是便于模块化。我们把代码按照不同的功能或作用分隔存放在不同的文件中,那么当其中一个功能有改动时,只需要重新编译相关的文件,而不必编译整个项目的所有源文件。
多个.C文件, 在一个编译单元里边的mian函数所包含的代码太庞杂了,适合分成几个函数,在一个编译代码文件中的代码太长了,适合分成几个文件,但是 两个独立的源文件不能编译形成可执行的程序
而头文件,在使用和定义这个函数的地方都应该#inlcude这个头文件,一般的做法是任何.C文件都有对应的同名.h,把所有对外的函数原型和全局变量的声明放进去(只能是声明),否则会造成一个项目多个编译单元里有重名的实体(某些编译器允许几个编译单元里有重名的函数,或者用weak修饰符来强调这种存在)
在编译器中新建一个项目,然后把几个源文件加进去,对于项目,有些编译器会把一个项目中所有的源文件代码文件都编译后,链接起来,有点编译器分开的编译和构建两个按钮,前者是对单个源代码文件编译,后者是对整个项目做 链接:
//链接就是在一个.C文件中使用#include指令包含所需要头文件,
用于告诉编译器,文件中包含的头文件,属于预处理文件的一部分
如:
一个名为link的.h文件:
#ifndef _link_
#define _link_
typedef struct _link{
int number;
struct _link *nxte;
}link;
#endif
一个.C的文件:
#include<stdio.h>
#include "link" //这样可以引用到link文件里边的结构体了
int main(void){
link p;
p.number;
return 0;
}
多个.C文件的链接:
创建名为a.c文件:
#include <stdio.h>
#include "t" //在这里我们引用头文件的连接,访问到了b.c文件里边的数据
int main(void){
int i = myMax(20,40); //在这里我们想要调用b.c文件里边的mymax函数
printf("i = %d",i);
return 0;
}
创建名为b.c文件:
int myMax(int a,int b) {//函数的定义
if(a>b)
return a;
else
return b;
}
创建名为t.h头文件
#ifndef _link_
#define _link_
int myMax(int a,int b);//函数的声明
#endef
补充:
1、声明和定义:
函数的定义是定义,函数的原形是声明。变量的定义是定义,extrrn的声明是声明,声明:int i;是变量的定义 ,extern int i是变量的声明
声明是不产生代码的数据,如:函数原型,变量声明,结构声明,宏声明,枚举声明,类型声明,inline函数。而定义是会产生代码的数据
不对外开放的函数(static): 在函数前加上sastic就可使得它只能在所在的编译单元(源文件)中被使用的函数,在全局变量前面加上static就使得它成为只能在所在的编译单元中被使用的全局变量
对外开放的函数(extern): 如果在一个程序中如果需要在一个源文件内扩展到另一个源文件的作用域的全局变量,就可以在定义函数时引用extern关键字将该变量转为对外开放的函数,就可让其他的编译单元也可访问到此单元的函数了 (这两个函数在第一章有介绍)
4、文件的函数的操作
在C语言中,没有输入输出语句,对文件的读写都是用库函数来实现的。ANSI规定了标准输入输出函数,用它们对文件进行读写。
1、(读) 二进制文件的数据时,用到文件头类型指针FILE * ,三个步骤分别是: (1)打开指针函数:fopen_s函数 (2)读文件指针fread函数 (3)关闭文件fclose函数
2、(写 )二进制文件的数据内容时,用到文件头类型指针FILE * ,三个步骤分别是: (1)打开指针函数:fopen_s函数 (2)读文件指针 fwrite 函数 (3)关闭文件 fclose 函数
打开文件fopen函数:
注意在使用fopes_s函数的时候,其中的两个参数代表打开文件的模式,“读/写” 分别对应 “r/w”, "b" 表示 “二进制方法”; “ rb 和 wb ”,分别表示 “二进制方式 读 / 写”文件。
C语言中没有输入输出语句,所有的输入输出功能和文件操作都是都用 ANSI C提供的一组标准库函数来实现。 常用文件操作标准库函数有:
(函数原型):FILE * fopen (char *pname,char *mode),该函数声明在stdio.h中,它的第一个参数是待打开文件的名称,更确切的说是一个包含改文件名的字符串地址,第二个参数是一个字符,指定待打开文件的模式。
1、功能说明 按照mode 规定的方式,打开由pname指定的文件。若找不到由pname指定的相应文件,就按以下方式之一处理: (1) 此时如mode 规定按写方式打开文件,就按由pname指定的名字建立一个新文件; (2) 此时如mode 规定按读方式打开文件,就会产生一个错误。
2、打开文件的作用是: (1)分配给打开文件一个FILE 类型的文件结构体变量,并将有关信息填入文件结构体变量; (2)开辟一个缓冲区; (3)调用操作系统提供的打开文件或建立新文件功能,打开或建立指定文件; FILE *:指出fopen是一个返回文件类型的指针函数;
3.参数说明 pname:是一个字符指针,它将指向要打开或建立的文件的文件名字符串。 mode: 是一个指向文件处理方式字符串的字符指针
4.返回值 正常返回:被打开文件的文件指针。 异常返回:NULL,表示打开操作不成功。
fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。
如果希望接收 fopen() 的返回值,就需要定义一个 FILE 类型的指针。例如:
FILE *fp = fopen("demo.txt", "r"); //这里的第二个参数是可更改其他的模式的
表示以“只读”(r)方式打开当前目录下的 demo.txt 文件,并使 fp 指向该文件,这样就可以通过 fp 来操作 demo.txt 了。fp 通常被称为文件指针。
另外,文件也有不同的类型,按照数据的存储方式可以分为二进制文件和文本文件,它们的操作细节是不同的。 在调用 fopen() 函数时,这些信息都必须提供,称为“文件打开方式”。最基本的文件打开方式有以下几种:
调用 fopen() 函数时必须指明读写权限,但是可以不指明读写方式(此时默认为"t"
)。
关闭文件fclose函数:
文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。fclose() 的用法为 (函数原型):int fclose(FILE *fp); ,此函数关闭fp指定的文件,必要时刷新缓冲区,对于较正式的程序,应该检查是否成功关闭文件,如果成功关闭函数返回0,否则返回EOF (如果磁盘已经满了或者,移动硬盘被移除或出现I/O错误,都会导致调用fclose函数失败)
FILE *fp = fopen("demo.txt", "r"); //打开一个文件
fclose(fp); //关闭一个文件
1. 功能说明 关闭由fp指出的文件。此时调用操作系统提供的文件关闭功能,关闭由fp->fd指出的文件;释放由fp指出的文件类型结构体变量;返回操作结果,即0或EOF。
2. 参数说明 fp:一个已打开文件的文件指针。
3. 返回值 正常返回:0。 异常返回:EOF,表示文件在关闭时发生错误。
文件的输入输出:
当成功地打开文件后,接下来的事情就是对文件进行输入或输出操作,最简单的是调用 getc(或 fgetc) 和 putc(或 fputc) 函数进行(单字符)的输入和输出。
两者的区别: fgetc是一个函数 ,函数原型:int fgetc(FILE * stream); ,fgetc()从参数stream所指的文件中读取一个字符。若读到文件尾而无数据时便返回EOF表示到了文件末尾,它会返回读取到字符。
getc是一个宏 ,函数原型 int getc(FILE *stream); ,用函数getc(fgetc)从文件读取字符。getc、fgetc用法相同。 getc的调用形式:ch=getc(fp);此处的fp是文件指针;函数功能是从文件指针指向的文件读入一个字符,并把它作为函数值返回给字符型变量ch。
putc 是一个函数,函数原型:int fputc(int ch,FILE*fp) 这里 ch 是待输出的某个字符,它可以是一个字符常量,也可以是一个字符变量;fp 是文件指针。putc(ch, fp) 的功能是将字符 ch 写到文件指针 fp 所指的文件中去。如果输出成功,putc 函数返回所输出的字符;如果输出失败,则返回一个 EOF 值。EOF 是在 stdio.h 库函数文件中定义的符号常量,其值等于 -1
fputc函数原型:int fputc(int ch,FILE *fp) 1. 功能说明 把ch中的字符写入由fp指出的文件中去。 2. 参数说明 ch:是一个整型变量,内存要写到文件中的字符(C语言中整型量和字符量可以通用)。 fp:这是个文件指针,指出要在其中写入字符的文件。 3. 返回值 正常返回: 要写入字符的代码。 非正常返回:返回EOF。例如,要往"读打开"文件中写一个字符时,会发生错误而返回一个EOF。 两者使用的差不多
//实例:打开 关闭文件 (单字符的形式)
#define _GRT_SECURE_NO_WARNNGS
#include<stdio.h>
int main() {
FILE *fp; //创建文件结构体
char ch;
//如果文件不存在,给出提示并退出
if ((fp = fopen("E:/C/CS1/1.txt", "rt")) == NULL) {//这里表示打开E盘的C的根目录下的CS1中的1.txt文件,rt表示阅读一个已存在的文件
//rt 用只读 打开一个文本文件,只允许读数据
puts("Fail to open file!"); //如果打不开文件将输出这句
exit(0); //并且强制中断程序
}
//读取文件,直到文件末尾
while ((ch = fgetc(fp)) != EOF) { //在这里使用fgetc和fgets效果一样
putchar(ch);
}
putchar('\n');
fclose(fp);//关闭文件
return 0;
}
/*
FILE 简介:
struct _iobuf {
char *_ptr; //文件输入的下一个位置
int _cnt; //当前缓冲区的相对位置
char *_base; //指基础位置(即是文件的其始位置)
int _flag; //文件标志
int _file; //文件的有效性验证
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取
int _bufsiz; //文件的大小
char *_tmpfname; //临时文件名
};
typedef struct _iobuf FILE;
*/
fgetc() 和 fputc() 函数每次只能读写一个字符,速度较慢;实际开发中往往是每次读写一个字符串或者一个数据块,这样能明显提高效率。
fgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组(字符串)中。
其函数原型为:char *fgets(char *str, int n, FILE *stream); 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定,n为字符串大小,fp是指向FILE的指针。
返回值: 读取成功时返回字符数组首地址,也即 str读取失败时返回 NULL;如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL,如果发生错误,返回一个空指针。
注意,读取到的字符串会在末尾自动添加 '\0',n 个字符也包括 '\0'。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101
fputs()函数具有的功能是向指定的 文件写入一个字符串(不自动写入字符串结束标记符‘\0’)。成功写入一个字符串后,文件的位置指针会自动后移。
函数原型: int fputs( char *str, FILE *fp ); 它接受两个参数:第一个是字符串的地址,第二个是文件指针,该函数根据传入地址找到的字符串写入的文件中,和puts函数不同,它在打印字符串时不会在其末尾添加换行符
返回值:str 为要写入的字符串,fp 为文件指针。写入成功返回非负数,失败返回 EOF (符号常量,其值为-1)。
//实例:fgets和fputs的使用
#include<stdio.h>
#include<stdlib.h>
#define N 10 //定义字符常量 N为10
int main(void) {
FILE *fp;
char str[N+1];//字符数组要比实际多一个
if ((fp = fopen("E:/C/CS1/2.txt", "a")) == NULL) {
puts("NONONON");
exit(0);
}
/*scanf("%s", str);
fputs(str, fp); //这里是使用二进制数据的形式写入文件,所以如果直接使用printf输出的话会是乱码的
*/ //解决方案是 使用fprintf和fscanf函数 后面再介绍
while (fgets(str, N, fp) != NULL) {
printf("%s", str);
}
fclose(fp);
return 0;
}
//fgets() 遇到换行时,会将换行符一并读取到当前字符串。该
示例的输出结果之所以和 demo.txt 保持一致,该换行的地方换行,就是因为 fgets() 能够读取到换行符。
而 gets() 不一样,它会忽略换行符。
/*需要重点说明的是,在读取到 n-1 个字符之前如果出现了换行,
或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。
在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),
将 n 的值设置地足够大,每次就可以读取到一行数据。
*/
使用格式化的形式读写文件(fscanf和fprintf函数):
fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。所以适用于scanf和printf函数的规则和类型说明符,也同样适用于fscanf和fprintf
//两者的原型:
int fscanf ( FILE *fp, char * format, ... );//功能用于读取数据
int fprintf ( FILE *fp, char * format, ... );//功能用于向文件写入数据
fp 为文件指针,format 为格式控制字符串,... 表示参数列表。与 scanf() 和 printf() 相比,它们仅仅多了一个 fp 参数。例如:
FILE *fp; //文件指针
int i, j;
char *str, ch; //定义一个字符串,和一个字符型变量
fprintf(fp,"%d %c", j, ch); //向指定的文件写入数据 j是整形 ch是字符型
fscanf(fp, "%d %s", &i, str); //向指定的文件读取数据
返回值: fprintf() 返回成功写入的字符的个数,失败则返回负数。 fscanf() 返回参数列表中被成功赋值的参数个数。
使用fscanf和fprintf函数时会发现文件的内容是可以阅读的,格式非常清晰。用 fprintf() 和 fscanf() 函数读写配置文件、日志文件会非常方便,不但程序能够识别,用户也可以看懂,可以手动修改。
如果将 fp 设置为 stdin,那么 fscanf() 函数将会从键盘读取数据,与 scanf 的作用相同;设置为 stdout,那么 fprintf() 函数将会向显示器输出内容,与 printf 的作用相同。
例如:
#include<stdio.h>
int main(void){
int a, b, sum;
fprintf(stdout, "Input two numbers: ");
fscanf(stdin, "%d %d", &a, &b);
sum = a + b;
fprintf(stdout, "sum=%d\n", sum);
return 0;
}
// fprintf的用法
#include<stdio.h> //向文件写数据并保存
#include<string.h>
#include<stdlib.h>
int main()
{
int a[6]; //数据为整型
int i;
FILE *fp; //定义一个指针,用于指向文件的地址
fp = fopen("E:/C/CS1/2.txt", "w"); //打开文件test.txt,w表示只写
if (fp == NULL)
{
//打开文件失败的话退出程序
printf("File cannot open! ");
exit(0);
}
printf("请输入6个整型数字(以空格隔开):\n");
for (i = 0; i < 6; i++)
{
scanf("%d", &a[i]); //从命令行输入6个数据保存在字符串中
}
printf("输入的数字为:\n");
for (i = 0; i < 6; i++)
{
fprintf(fp, "%d\t", a[i]);//将字符串输入的数据格式化并保存到打开的文件中
printf("%d ", a[i]);
}
fclose(fp);//关闭文件
return 0;
}
//fscanf的用法
#include <stdio.h>
#include <stdlib.h>
int main()
{
char str1[10], str2[10], str3[10];
int year;
FILE * fp;
fp = fopen("E:/C/CS1/2.txt", "w+");//打开文件
fputs("We are in 2015", fp);//向文件写入数据
rewind(fp);//将文件内部的位置指针重新指向一个流(数据流/文件)的开头
fscanf(fp, "%s %s %s %d", str1, str2, str3, &year);//将文件的数据读取到程序中
printf("Read String1 |%s|\n", str1);//输出
printf("Read String2 |%s|\n", str2);
printf("Read String3 |%s|\n", str3);
printf("Read Integer |%d|\n", year);
fclose(fp);//关闭文件
return(0);
}
//综合例子:输入学生信息保存到磁盘文件中,然后读取并输出
#include<stdio.h>
#include<stdlib.h>
#define N 2
typedef struct _date {
char name[10];
char sx[10];
int age;
double socre;
}date;
date boya[N];//定义两个结构数组,一个写入文件 一个读取文件
date boyb[N];
date *pa, *pb; //定义两个结构指针,一个用来写入文件,一个用来读取文件
int main(void) {
FILE *fp;
int i;
pa = boya;//将结构体指针初始化地址
pb = boyb;
// printf("1-%p\n", pa);
if ((fp = fopen("E:/C/CS1/1.txt", "wt+")) == NULL) //wt+ 表示读写打开或建立一个文本文件,允许读写
{
puts("NO NO NO!");
exit(0);
}
//从键盘输入 保存在boya
printf("输入数据:\n");
for (i = 0; i < N; i++) {
scanf("%s %s %d %lf", pa->name, pa->sx, &pa->age, &pa->socre);
pa++;//让指针往后移动 是为了存储不同的学生信息
}
pa = boya;//pa指针指向回boya的地址 下面的boyb也是同理
//printf("2-%p\n", pa);
//将boys的数据写入到磁盘文件中
for (i = 0; i < N; i++) {
fprintf(fp, "%s %s %d %lf\n", pa->name, pa->sx, pa->age, pa->socre);
pa++;//让指针往后移动
}
//重置文件指针指向
rewind(fp);//将文件内部的位置指针重新指向一个流(数据流/文件)的开头
//从文件中读取数据到程序中的boyb
for (i = 0; i < N; i++) {
fscanf(fp,"%s %s %d %lf\n", pb->name, pb->sx, &pb->age, &pb->socre);
pb++;//让指针往后移动
}
pb = boyb;
//最后输出数据
for (i = 0; i < N; i++) {
printf("%s %s %d %lf\n", pb->name, pb->sx, pb->age, pb->socre);
pb++;//让指针往后移动
}
fclose(fp);//关闭文件
return 0;
}
使用数据块(二进制)的形式读写文件(fread和fwrite函数)
对于 Windows 系统,使用 fread() 和 fwrite() 时应该以二进制的形式打开文件,fread函数可从文件中读取二进制数据,从指定文件中读取块数据。所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。
函数原型:
size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );
size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );
//此两个函数以二进制形式对文件进行操作,不局限于文本文件。
对参数的说明:
1、ptr 为内存区块的指针,它可以是数组、变量、结构体等。fread() 中的 ptr 用来存放读取到的数据 fwrite() 中的 ptr 用来存放要写入的数据。
2、size:表示每个数据块的字节数。
3、count:表示要读写的数据块的块数。
4、fp:表示文件指针。
5、理论上,每次读写 size*count 个字节的数据。
size_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,也即非负数,常用来表示数量。
返回值:
fread():返回成功读写的块数,也即 count。返回成功读取的对象个数 fwrite() :如果成功返回元素的总数,返回一个size对象,该对象是一个整型数据类型
fread() 若出现错误或到达文件末尾,返回值就可能小于 count fwrite() 如果size数字与 nmemb 参数不同,则会显示一个错误
对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测
对于 fread() 来说,fread不区分文件尾和错误,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。若size或count为零,则fread返回零且不进行其他动作
#include<stdio.h>
#include<Stdlib.h>
#define N 2
typedef struct _date {
char name[10];
char sx[10];
int age;
double score;
}date;
date boya[N], boyb[N];
date *h, *p;
int main(void) {
FILE *fp;
int i;
h = boya;
p = boyb;
if ((fp = fopen("E:/C/CS1/x.txt", "wb+")) == NULL) {//wb+ 表示是 以读写方式打开或建立一个二进制文件,允许读和写。
puts("NO NO NO~");
exit(0);
}
for (i = 0; i < N; i++) {
scanf("%s %s %d %lf", h->name, h->sx, &h->age, &h->score);
h++;//移动指针
}
//以二进制形式写入文件
fwrite(boya, sizeof(date),N, fp);//将结构体数组 boya 的数据写入文件
rewind(fp);//将文件内部的位置指针重新指向一个流(数据流/文件)的开头
fread(boyb, sizeof(date), N, fp); //从文件读取数据并保存到结构体数组 boyb中
//输出数据
for (i = 0; i < N; i++) {
printf("%s %s %d %f\n", p->name, p->sx, p->age, p->score);
p++;//移动指针
}
fclose(fp);
return 0;
}
随机读写文件,将介绍:rewind函数 和 fseek函数 与 ftell函数
前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据。但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。 实现随机读写的关键是要按要求移动位置指针,这称为(文件的定位)
移动文件内部位置指针的函数主要有两个,即 rewind() 和 fseek():
函数原型: void rewind(FILE *stream); //将文件内部的位置指针重新指向一个流(数据流 / 文件)的开头 int fseek(FILE *stream, long offset, int fromwhere);//用来将位置指针移动到任意位置 //(重定位流(数据流/文件)上的文件内部位置指针) long ftell(FILE *stream);//返回当前文件位置
参数说明: rewind和ftell中的stream就是文件指针。
fseek中的stream就是文件指针,offset 表示偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。 fromwhere 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示: (文件开头:SEEK_SET 常量值为0)(文件当前位置:SEEK_CUR 常量值为1)(文件末尾:SEEK_END 常量值为2)
注意: 1、fseek() 一般用于二进制文件,在文本文件中要进行转换,计算的位置有时会出错。在移动位置指针之后,就可以用前面介绍的任何一种读写函数进行读写了。由于是二进制文件,因此常用 fread() 和 fwrite() 读写。
文件指针指向 文件/流。位置指针指向文件内部的字节位置,随着文件的读取会移动,文件指针如果不重新赋值将不会改变或指向别的文件。
2、rewind()不是文件指针而是文件内部的位置指针,随着对文件的读写文件的位置指针(指向当前读写字节)向后移动。而文件指针是指向整个文件,如果不重新赋值文件指针不会改变。
3、使用fseek函数后再调用函数ftell()就能非常容易地确定文件的当前位置。
返回值: ftell(); 返回一个long的数值,表示当前文件所在的位置大小
rewind(); 没有返回值
fseek (); 成功,返回0,失败返回非0值,并设置error的值,可以用perror()函数输出错误
补充 : ftell(fp);利用函数 ftell() 能方便地知道一个文件的长。如以下语句序列: fseek(fp, 0L,SEEK_END); len =ftell(fp); 首先将文件的当前位置移到文件的末尾,然后调用函数ftell()获得当前位置相对于文件首的位移,该位移值等于文件所含字节数。
#include<stdio.h>
#include<stdlib.h>
#define N 3 //三个学生
typedef struct _date {
char name[10];
char sex[10];
int age;
double score;
}date;
date boys[N];
date *pb;
date boy;
int main(void) {
FILE *fp;
int i;
pb = boys;
if ((fp = fopen("E:/C/CS1/t.txt", "wb+")) == NULL) {
printf("NONONON");
exit(0);
}
for (i = 1; i <=N; i++) {
scanf("%s %s %d %lf", pb->name, pb->sex, &pb->age, &pb->score);
pb++;
}
fwrite(boys, sizeof(date), N, fp);
fseek(fp, 0l, SEEK_SET);//定位至文件的开始处
fread(&boy, sizeof(date), N, fp);
printf("%s %s %d %lf\n", boy.name, boy.sex, boy.age, boy.score);
// fseek(fp, 10l, SEEK_SET); 定位在文件中的第10个字节处
// fseek(fp, -10l, SEEK_END); 从文件结尾处退回10个字节
fseek(fp, sizeof(date), SEEK_SET); //从开始位置移动指针
fread(&boy, sizeof(date), N, fp);
printf("%s %s %d %lf\n", boy.name, boy.sex, boy.age, boy.score);//输出了第二个学生信息
fclose(fp);
return 0;
}
其他库函数:
getw 以二进制形式读取一个整数
putw 以二进制形式存贮一个整数文件状态检查函数:
feof 文件结束
ferror 文件读/写出错
clearerr 清除文件错误标志
ftell 了解文件指针的当前位置
fgetpos 获取当前访问指针位置信息。
fsetpos 定位流上的文件指针fdopen将文件描述词转为文件指针
fflush更新缓冲区
setbuf设置文件流的缓冲区
setvbuf设置文件流的缓冲区
fileno返回文件流所使用的文件描述词
mktemp产生唯一的临时文件名
ungetc将指定字符写回文件流中