补上之前的文件操作部分,下面是对文件操作的具体知识点列举与使用说明:
文件操作
文件是根据特定的目的而收集在一起的有关数据的集合。C++把每一个文件都看成是一个有序的字节流,每个文件都以文件结束标志结束,如果要操作某个文件,程序必须首先打开该文件。当一个文件被打开后,该文件就和一个流关联起来,这里的流实际上是一个字节序列。
C++将文件分为文本文件和二进制文件。
二进制文件一般含有特殊的格式或计算机代码,如图文件和可执行文件等(实际上二进制文件适用于任何文件,包括文本文件,音频文件,视频文件,图像文件以及可执行文件等)。文本文件则是可以用任何文字处理程序阅读和编辑的简单ASCII文件(人们能够看明白其含义的文件)。
二进制文件和文本文件都是按照二进制存储的,只不过文本文件是把一个字节一个字节解读成字符,而二进制文件可以任意定义解读方式。因此二进制文件与文本文件的区别是在逻辑上而不是物理储存。
记事本无论打开什么文件都按既定的字符编码工作(如ASCII码),所以当它打开二进制文件时,出现乱码也是很必然的一件事情了,解码和译码不对应嘛。例如文件流”00000000_00000000_00000000_00000001”可能在二进制文件中对应的是一个四字节的整数int 1,在记事本里解释就变成了“NULL_NULL_NULL_SOH”这四个控制符。
在windows上,用记事本就可以打开文本文件了,但要打开二进制文件需要对应的二进制文件解码器,因此,文本文件是更为大家所熟知的文件形式,而二进制文件的优点在于它的输入输出都省去了转换的过程,而且更省空间。
下面我们学习如何编写C++代码来实现对文本文件的输入和输出。
文件操作主要有fopen,freopen重定向版与文件输入输出流三类:
一、用fopen打开文件:
函数原型:
FILe*fopen(const char *filename,const char *mode);
FILe是在stdio.h中定义的一个结构,用于存放和文件有关的信息。打开文件的模式主要有以下几种:
r:以文本方式打开文件,只进行读操作。
w:以文本方式打开文件,只进行写操作。
a:以文本方式打开文件,只往其末尾添加内容。
rb:以二进制方式打开文件,只进行读操作。
wb:以二进制方式打开文件,只进行写操作。
ab:以二进制方式打开文件,只往其末尾添加内容。
r+:以文本方式打开文件,既读取数据,也要往文件中写入数据。
r+b:以二进制形式打开文件,既读取数据,也要往文件中写入数据。
fopen函数返回一个FILE *类型的指针,称为文件指针,该指针指向的FILE类型变量中存放着关于文件的一些信息,文件打开后,对文件的读写操作就不再使用文件名而都是通过fopen函数返回的指针进行。
如果试图以只读方式打开一个不存在的文件或因其他原因(比如没有权限等)导致打开文件失败时,fopen函数会返回NULL指针。对文件进行读写操作前,判断fopen函数的返回值是否为NULL,是非常重要的习惯。
用 fclose 关闭文件:
fclose函数的原型是:
int fclose(FILE *stream);
stream即是先前用fopen打开文件时得到的文件指针。
一定要注意,打开文件后,要确保程序执行的每一条路径上都会关闭该文件。一个程序
能同时打开的文件数目是有限的,如果总是打开文件没有关闭,那么文件打开数目到达一定限度后,就再也不能打开新文件了。一个文件,可以被以只写的方式同时打开很多次,这种
情况也会占用程序能同时打开的文件总数的资源。
调用fclose函数时,如果参数stream的值是NULL, 那么很可能会出现程序异常终止的错误。
用 fscanf,fprintf 读写文件:
fscanf函数原型如下;
int fscanf(FILE *stream, const char *format[, address, ...]);
fscanf和scanf函数很象,区别在于多了第一个参数----文件指针stream。scanf函数从
键盘获取输入数据,而fscanf函数从与stream相关联的文件中读取数据。该函数适用于读取以文本方式打开的文件。如果文件的内容都读完了,那么fscanf函数返回值为 EOF (stdio.h 中定义的一个常量)。
fprintf函数能用于向文件中写入数据,用法和printf、fscanf类似。
其原型是:
int fprintf(FILE *stream, const char *format[, argument, ...]);
具体用法参考下面的程序。
用 fgetc读文件,用fputc写文件:
fgetc函数原型如下:
int fgetc(FILE *stream);
它用于从文件中读取一个字节,返回值即是所读取的字节数。每个字节都被当作一个无
符号的8位(二进制位)数,因此每个被读取字节的取值范围都是0-255。反复调用fgetc函数可以读取整个文件。如果已经读到文件末尾,无法再读,那么fgetc函数返回EOF(实际上就是-1)。
fputc函数原型如下:
int fputc(int c, FILE *stream);
它将一个字节写入文件。参数c即是要被写入的字节。虽然c是int类型的,但实际上只有其低8位才被写入文件。如果写入失败,该函数返回EOF。
用fgets函数读文件, fputs函数写文件:
fgets函数原型如下;
char *fgets(char *s, int n, FILE *stream);
它一次从文件中读取一行,包括换行符,放入字符串s中,并且加上字符串结尾标志
符’\0’。参数n代表缓冲区s中最多能容纳多少个字符(不算结尾标志符’\0’)。
fgets函数的返回值是一个char *类型的指针,和s指向同一个地方。如果再没有数据可
以读取,那么函数的返回值就是NULL。
fputs函数原型如下:
int fputs(const char *s, FILE *stream);
它往文件中写入字符串s。注意,写完s后它并不会再自动向文件中写换行符。
用 fread读文件,用fwrite写文件:
fread函数原型如下:
unsigned fread(void *ptr, unsigned size, unsigned n, FILE *stream);
该函数从文件中读取n个大小为size字节的数据块, 总计n*size字节,存放到从地址
ptr 开始的内存中。返回值是读取的字节数。如果一个字节也没有读取,返回值就是0。
fwrite函数原型如下:
unsigned fwrite(const void *ptr, unsigned size, unsigned n, FILE *stream);
该函数将内存中从地址 ptr 开始的n*size个字节的内容,写入到文件中去。
这两个函数的返回值,表示成功读取或写入的“项目”数。每个“项目”的大小是size
字节。 其实使用这两个函数时,总是将size置为1,n置为实际要读写的字节数,也是没有问题的。
fread函数成功读取的字节数,有可能小于期望读取的字节数。比如反复调用fread读取
整个文件,每次读取100个字节,而文件有1250字节,那么显然最后一次读取,只能读取50 字节。
使用fread和fwrite函数读写文件,文件必须用二进制方式打开。
有些文件由一个个“记录”组成,一个记录就对应于C/C++中的一个结构,这样的文件,就适合用fread和fwrite来读写。比如一个记录学生信息的文件students.dat,该文件里每个“记录”对应于以下结构:
struct Student
{
char szName[20];
unsigned nId;
short nGender; //性别
short nBirthYear, nBirthMonth, nBirthDay;
float fGPA;
};
下面的程序先读取前例提到的students.txt中的学生信息,然后将这些信息写入
students.dat中。接下来再打开students.dat,将出生年份在1985年之后的学生记录提取出来,写到另一个文件 students2.dat中去。
#include <stdio.h>
#include <string.h>
struct Student
{
char szName[20];
unsigned nId;
short nGender; //性别
short nBirthYear, nBirthMonth, nBirthDay;
float fGPA;
};
int main()
{
FILE * fpSrc, * fpDest;
struct Student Stu;
fpSrc = fopen( "c:\\tmp\\students.txt", "rb");
if( fpSrc == NULL )
{
printf( "Failed to open the file.");
return 0;
}
fpDest = fopen( "students .dat", "wb");
if( fpDest == NULL)
{
fclose( fpSrc);
printf( "Destination file open failure.");
return 0;
}
char szName[30], szGender[30];
int nId, nBirthYear, nBirthMonth, nBirthDay;
float fGPA;
while( fscanf( fpSrc, "%s%d%s%d%d%d%f", szName, & nId, szGender, & nBirthYear, & nBirthMonth, & nBirthDay, & fGPA) != EOF)
{
strcpy(Stu.szName, szName);
Stu.nId = nId;
if( szGender[0] == 'f' )
Stu.nGender = 0;
else
{
Stu.nGender = 1;
Stu.nBirthYear = nBirthYear;
Stu.nBirthMonth = nBirthMonth;
Stu.nBirthDay = nBirthDay;
fwrite( & Stu, sizeof(Stu), 1, fpDest);
}
fclose(fpSrc);
fclose(fpDest);
fpSrc = fopen( "students.dat", "rb");
if( fpSrc == NULL )
{
printf( "Source file open failure.");
return 0;
}
fpDest = fopen( "students2.dat", "wb");
if( fpDest == NULL)
{
fclose( fpSrc);
printf( "Destination file open failure.");
return 0;
}
while(fread( & Stu, sizeof(Stu), 1, fpSrc))
{
if( Stu.nBirthYear >= 1985 )
fwrite( & Stu, sizeof(Stu), 1, fpDest);
}
fclose( fpSrc);
fclose( fpDest);
return 0;
}
}
一般来说使用fscanf,fprintf的频率比较高,fgetc,fputc,fgets,fputs相对少一些,对于fread,fwrite这样的二进制读取写入相对更少,因为对于一个类来讲使用fscanf,fprintf搭配重载输入输出有时就基本可以代替fread,fwrite。
用fseek 改变文件当前位置:
文件是可以随机读写的,即读写文件并不一定要从头开始,而是直接可以从文件的任意
位置开始读写。比如,可以直接读取文件的第200个字节,而不需将前面的199个字节都读一遍。同样,也可以直接往文件第1000个字节处写若干字节,覆盖此处原有内容。甚至可以先在文件的第200字节处读取100字节,然后跳到文件的第1000字节处读取20字节,然后再跳到文件的第20字节处写入30字节。这就叫“随机读写”。然而,前面提到的那些文件读写函数,都没有参数能够指明读写是从哪个位置开始,这又是怎么回事呢?
答案是:所有的文件读写函数,都是从文件的“当前位置”开始读写的。文件的“当前位置”信息保存在文件指针指向的 FILE结构变量中。一个文件在以非 “添加”方式打开,尚未进行其他操作时,其“当前位置”就是文件的开头;以添加方式打开时,其“当前位置”在文件的末尾。此后调用读写函数读取或写入了n个字节,“当前位置”就往后移动n个字节。
如果“当前位置”到达了文件的末尾,那么文件读取函数再进行读操作就会失败。
注意,文件开头的“当前位置”值是0,而不是1。
综上所述,要实现随机读写,前提是能够随意改变文件的“当前位置”。fseek函数就
起到这个作用。其原型如下:
int fseek(FILE *stream, long offset, int whence);
该函数将与stream关联的文件的“当前位置”设为距whence处offset字节的地方。whence可以有以下三种取值,这三种取值都是在stdio.h里定义的标识符:
SEEK_SET: 代表文件开头
SEEK_CUR: 代表执行本函数前文件的当前位置
SEEK_END: 代表文件结尾处
例如,假设 fp是文件指针,那么:
fseek(fp, 200, SEEK_SET);
就将文件的当前位置设为200,即距文件开头200个字节处;
fseek(fp, 0, SEEK_SET);
将文件的当前位置设为文件的开头。
fseek(fp, -100, SEEK_END);
将文件的当前位置设为距文件尾部100字节处。
fseek(fp, 100, SEEK_CUR);
将文件的当前位置往后(即往文件尾方向)移动100个字节。
fseek(fp, -100, SEEK_CUR);
将文件的当前位置往前(即往文件开头方向)移动100个字节。
(offset过大,需要long long 型的offset时,fseek 改用 _fseeki64。)
当然也可以这样表示:
origin 数值 代表的具***置
SEEK_SET 0 文件开头
SEEK_CUR 1 文件指针当前位置
SEEK_END 2 文件尾
例如: fseek(fp,10L,0); 把文件指针从文件开头移到第10字节处,fseek(fp,-15L,2); 把文件指针从文件尾向前移动15字节。由于offset参数要求是长整型数,故其数后带L。
下面的程序,读取文件students.dat中的第4个记录到第10个记录(记录从0开始算),
并将这部分内容写入到第20个记录开始的地方,覆盖原有的内容。
#include <stdio.h>
#include <string.h>
#define NUM 10
#define NAME_LEN 20
struct Student
{
char szName[NAME_LEN];
unsigned nId;
short nGender; //性别
short nBirthYear, nBirthMonth, nBirthDay;
float fGPA;
};
int main()
{
FILE * fpSrc;
Student aStu[NUM];
fpSrc = fopen( "c:\\tmp\\students4.dat", "r+b");
if( fpSrc == NULL )
{
printf( "Failed to open the file.");
return 0;
}
fseek( fpSrc, sizeof(Student)* 4, SEEK_SET);
fread( aStu, sizeof(Student), 7, fpSrc);
fseek( fpSrc, sizeof(Student) * 20, SEEK_SET);
fwrite( aStu, sizeof(Student), 7, fpSrc);
fclose( fpSrc);
return 0;
}
在这里用到了fread与fwrite,这里要比for循环搭配重载输入输出好用一些。
当然对于文件操作还有很多函数可以使用,比如类似于fgetc,fputc,fgets,fputs的fgetw,fputw函数对整数的操作(其实用的时候特别特别少),其中fgetw,fputw与getw,putw表现形式有些差异,同fgetc,fputc与getc,putc差异类似:
fgetc一定是函数, 而getc可能由宏来实现(但不一定)。 这就是两者的差别。一般来说,调用宏比调用函数耗费的时间少。一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
用得较多的可能有ftell()函数:给出处在文件中的当前位置(从文件头算起的字节数)rewind()函数(把位置设在文件开头)
谨记:
文件未打开之前不能使用。
如果以w模式打开一个已有的文件,那么该文件的内容将被清除。
EOF是一个值为-1的整数,因此必须使用一个整数变量来测试EOF。
当使用文件函数时,遗漏了文件指针是错误的。
如果要从以写模式打开的文件中读取数据是错误的,反之亦然。
程序终止之前关闭所有文件是一个良好的编程习惯。
二、freopen重定向版
命令格式:
FILE * freopen ( const char * filename, const char * mode, FILE * stream );
参数说明:
filename: 要打开的文件名
mode: 文件打开的模式,和fopen中的模式(r/w)相同
stream: 文件指针,通常使用标准流文件(stdin/stdout/stderr)
其中stdin是标准输入流,默认为键盘;stdout是标准输出流,默认为屏幕;
stderr是标准错误流,一般把屏幕设为默认。通过调用freopen,就可以修改标准流文件的默认值,实现重定向。
因为文件指针使用的是标准流文件,因此我们可以不定义文件指针。
接下来就是使用freopen()函数的优点了,我们不再需要修改scanf,printf,cin和cout。而是维持代码的原样就可以了。因为freopen()函数重定向了标准流,使其指向前面指定的文件,省时省力。最后只要使用fclose关闭输入文件和输出文件即可。
格式:fclose(stdin);fclose(stdout);
重定向与System(“pause”);冲突:执行的时候出于某些原因,有字符从文件流中输入,导致程序终止。 解决办法:
恢复句柄,可以重新打开标准控制台设备文件,这个设备文件的名字是与操作系统相关的。格式:freopen("CON", "r", stdin);
问题:
- 如何判断文件是否打开了
可以直接 if( freopen(“a.txt”,“r”,stdin)== NULL ) return false;
或 if( freopen(“b.txt”,“w”,stdout)== NULL ) return false;
表示没有打开 - 如何使流重新回到控制台上
如果你不想输入或输出到文件了,就加上一句
freopen(“CON”,“r”,stdin ); 对应输入
freopen(“CON”,“w”,stdout); 对应输出
注意的问题, 因为参数都是 c_字符串, 故不能把 c++ 里面的 string 类对象作为参数传进去,比如 string str= “a.txt”;你不能这样写 freopen( str, “r”, stdin );可以先把 string 类对像化成 c_字符串, 就用 c_str() 函数,上面的可以这样写 freopen( str.c_str(), “r”, stdin );
另外while (fin>>temp)和(scanf("%d",&temp)==1)主要是用于判断数据是否已经读完,以便及时终止循环。还可以用成员函数eof来判断是否达到数据流的末尾。
重定向用起来很方便,但并不是所有算法竞赛都允许读写文件。甚至有的竞赛允许访问文件,但不允许使用freopen这样的重定向方式读写文件,可以使用fopen版,对scanf和printf语句适用。
三、文件输入输出流
在C++中,文件输入流(ifstream)和文件输出流(ofstream)的类,它们的默认输入输出设备都是磁盘文件。C++可以在创建对象时,设定输入或输出到哪个文件。由于这些类的定义是在fstream中进行的,因此,在使用这此类进行输入输出操作时,必须要在程序的首部利用#include指令包进fstream头文件。
例如:若想用fin作为输入对象,fout作为输出对象,则可以使用如下定义:
ifstream fin(“输入文件名.扩展名”);
ofstream fout(“输出文件名.扩展名”);
例:
#include<fstream> //使用文件输入输出流,对cin、cout语句适用
using namespace std;
int main()
{
ifstream fin("in.txt"); //定义输入文件名
ofstream fout("out.txt"); //定义输出文件名
int temp,sum=0;
while (fin>>temp) sum=sum+temp; //从输入文件中读入数据
fout<<sum<<endl;
fin.close();fout.close(); //关闭文件,可省略
return 0;
}
命令行参数:
如果我们编写了一个在屏幕上输出文本文件内容的程序,编译生成的可执行文件是 listfile.exe,那么,很可能我们希望该程序的用法是,在 Windows 的控制台窗口(也叫DOS 命令窗口)中输入: listfile 文件名 然后敲回车,就能启动 listfile 程序,并将“文件名”所指定的文件的内容输出。比如敲“listfile file1.txt”,就能将 file1.txt 这个文件的内容输出。 要做到这一点,显然,listfile 程序必须知道用户输入的那个文件名。我们将用户在DOS 窗口输入可执行文件名的方式启动程序时,跟在可执行文件名后面的那些字符串,称为“命令行参数”。比如上例中的“file1.txt”,就是一个命令行参数。命令行参数可以有多个,以空格分隔。比如“listfile file1.txt file2.txt”。
在程序中如何知道用户输入的命令行参数呢? 要做到这一点,main 函数的写法须和以
往的不同,要增加两个参数:
int main(int argc, char * argv[])
{
……
}
参数 argc 就代表启动程序时,命令行参数的个数。C/C++语言规定,可执行程序程序本身的文件名,也算一个命令行参数,因此,argc 的值至少是 1。argv 是一个数组,其中的每个元素都是一个 char* 类型的指针,该指针指向一个字符串,这个字符串里就存放着命令行参数。例如,argv[0]指向的字符串就是第一个命令行参数,即可执行程序的文件名,
argv[1]指向第二个命令行参数,argv[2]指向第三个命令行参数……。
例:
#include <stdio.h>
int main(int argc, char * argv[])
{
for(int i = 0; i < argc; i ++ )
printf( "%s\n", argv[i]);
return 0;
}
将上面的程序编译成 2.18.exe,然后在控制台窗口敲:
2.18 para1 para2 s.txt 5 4
输出结果就是:
2.18
para1
para2 s.txt
5
4