C/C++ 第十二讲 文件

12.1 文件的概述

  • 标准输入输出

    • 受限于数据类型,处理数据数量有限,程序结束不保存计算结果。
  • 文件输入输出

    • 可以处理大批量数据,长久地保存计算结果。

文件概述

  • 文件是存储在外存(如磁盘)上的数据集合。

  • 每个文件通过唯一的文件名来标识。

    • 文件路径:指明文件在外存中位置

    • 主文件名:遵循标识符命名规则

    • 文件后缀(扩展名):表示文件性质

    e.g.D:\\document\\codes.txt

  • 计算机按文件名对文件进行读写等操作。

  • 文件按数据存储形式分类

    • 文本文件(ASCII文件)

    • 把内存中数据转换为ASCII码,每个字符用一个ASCII码存储。

    • 二进制文件

    • 把内存中数据按其内存中存储形式不进行格式转换直接存放在文件上。

    例:整数123在内存占4个字节,其二进制文件也占4个字节,文本文件占3个字节。

    二进制文件因为与字符没有直接的对应关系,所以不能直接编辑和显示。

    故一般字符型数据以文本文件形式存储。而数值型数据视情况而定,二进制形式比较方便。

  • 文件存取方式

    • 顺序存取
    • 每当打开文件进行读、写操作时,总是从 文件的开头开始,从头到尾顺序地读写。
    • 随机存取
    • 可以通过调用C语言的库函数去指定开始读写的字节号,然后直接对此位置上的数据进行读写操作。
  • C语言文件操作步骤

    • 定义文件类型指针
    • FILE *文件指针变量
    • 打开文件
    • 调用fopen函数
    • 文件读写
    • 调用读写函数:
      • fgetc, fputc
      • fgets, fputs
      • fread, fwrite
      • fscanf, fprintf
    • 关闭文件
    • 调用fclose函数
  • 对文件的处理通过调用标准的输入输出库函数实现。

  • 文件操作一般采用:缓冲文件系统“的方式。

    image-20231130092504128

  • C语言文件处理的关键是定义一个文件指针,通过该指针对文件进行打开、读写、关闭等操作。

    • 定义:FILE *文件指针标识符;

    如:FILE *fp1,fp2;

    • FILE是结构体变量,存放每一个被打开文件的有关信息(如缓冲区的状态、大小,文件当前位置等)。使用FILE类型,需包含stdio.h文件。

12.2 文件的打开和关闭

实质:建立/撤销文件信息区和文件缓冲区

文件的打开:fopen

  • 函数原型:
1
FILE *fopen(char *fname,char *mode)
  • 功能:
    • 按照mode规定的方式,打开由fname指定的文件。
  • 参数:
    • fname:字符指针,指向要打开或建立的文件名字符串;
    • mode:字符指针,指向文件处理方式字符串。
  • 返回值:
    • 正常返回:被打开的文件的 文件指针;
    • 异常返回:NULL,表示打开操作不成功。

c2022:fopen_s

使用方法如下:

1
2
3
//打开文件
FILE *fp;
errno_t err = fopen_s( &fp, "D:\\users.txt", "r");

文件的打开方式:mode

有”r”, “w”, “a”, “+”, “b”, “t”六种模式选择符号。

r—-read

w—-write

a—-append

t—-text(默认)

b—-binary

+—-read+write

mode

r

只读方式打开文件,该文件必须存在。

w

打开只写文件,若文件存在则清除内容,若不存在则创建该文件。

a

追加方式打开只写文件。若文件不存在,则会建立该文件;若文件存在,写入的数据会被追加到文件尾后。

r+

读/写方式打开文件,该文件必须存在。

w+

打开可读/写文件,若文件存在则清除内容,若文件不存在则建立该文件。

a+

追加方式打开可读/写文件,若文件不存在,则会建立该文件;若文件存在,写入的数据会被追加到文件尾后。

rb

只读方式打开一个二进制文件,只允许读数据。

wb

只写方式打开或新建一个二进制文件,只允许写数据。

ab

追加打开一个二进制文件,并在文件末尾写数据。

rb+

读/写方式打开二进制文件,该文件必须存在。

wb+

读/写方式打开或建立一个二进制文件,允许读和写。

ab+

读/写方式打开一个二进制文件,允许读或在文件末追加数据。

说明:t表示打开文本文件,如果不加b(二进制文件),表示默认加了t。

  • 为了程序的通用性,文件名可在程序运行时输入,如:

    1
    2
    3
    4
    5
    FILE *fp;
    char fname[15];
    cout<<"Input filename:\n";
    cin>>fname;
    fp=fopen(fname,"r");

    注意:

    在fopen函数中,若文件名直接给出,路径中的\应写成\\;

    fp=fopen("D:\\Doc\\codes.txt","a");

    若文件名在程序运行时输入(如上例),则路径中的分隔符直接输入字符\

    (input) D:\Doc\codes.txt

  • 为保证程序正常运行,需对fopen函数的返回值进行检验,以判断文件是否成功打开,形式如下:

    1
    2
    3
    4
    5
    if ((fp=fopen(fname,mode))==NULL)
    {
    cout<<"can't open file\n";
    exit(1);
    }

    (该段程序需要头文件stdlib.h)

文件的关闭

  • 函数原型:
    • int fclose(FILE *fp)
  • 功能:
    • 关闭fp所指的文件,释放fp所指的文件类型结构体变量。
  • 参数:
    • fp:一个已打开文件的文件指针。
  • 返回值:
    • 正常返回:0;
    • 异常返回:EOF(-1),表示文件在关闭时发生错误。

应养成及时关闭文件的习惯,防止误操作或其他原因丢失数据。

判断

12.3 文件的读写

顺序读写:按照数据在文件中的存储顺序读写文件,容易但效率不高;

随机读写:反之,对任意位置的数据读写、访问。

顺序读写

  • C语言提供四种顺序读写函数:

    字符读写、字符串读写、格式读写和数据块读写。

函数

功能

函数

功能

fputc

向文件写入字符

fputs

向文件写入字符串

gfetc

从文件读取字符

fgets

从文件读取字符串

fwritec

向文件写入数据块

fprintf

向文件格式化写入数据块

fread

从文件读取数据块

fscanf

从文件格式化读取数据块

从文件读取字符:fgetc

  • 函数原型

    • int fgetc(FILE *fp)
  • 功能:

    • 从fp所指文件中读取一个字符,并使文件指针后移一个字符位置。
  • 参数:

    • fp:文件指针,指向要从中读取字符的文件。
  • 返回值:

    • 正常返回:读取的字符;
    • 异常返回:EOF(-1)。
  • 返回类型的说明:用于区分有效数据和输入的结束(EOF)

    • 若返回类型为char:内存中都是一个字节(无法区分)
    • EOF:0xFF
    • 数据:0xFF
    • 若返回类型为int:四个字节(正确区分)
    • EOF: 0xFFFFFFFF
    • 数据:0x000000XX
    • 所以将fgetc函数的返回定义为int类型。

文件结束测试函数:feof

  • 函数原型:int feof(FILE *fp)
  • 功能:判断文件是否结束;
  • 参数:fp:文件指针;
  • 返回值:
    • 0:假植,表示文件未结束。
    • 1:真值,表示文件结束。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
while(!feof(fp))
{
ch=fgetc(fp);
putchar(ch);
}
//不想输出-1的话:
while(1)
{
ch=fgetc(fp);
if(feof(fp)==1)
break;
putchar(ch);
}

向文件写入字符:fputc

  • 函数原型:int fputc(int ch,FILE *fp)
  • 功能:把ch中的字符写入fp所指的文件当前位置处,并使文件指针后移一个字符位置。
  • 参数:
    • ch:整型变量,内存要写到文件中的字符。
    • fp:文件指针,指向要在其中写入字符的文件。
  • 返回值:
    • 正常:写入的字符。
    • 异常:EOF(-1)。

例1

编程将文件file1.txt的内容显示在屏幕上,同时将该文件中的数字字符复制到文件file2.txt中。

分析:

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
int main()
{
FILE *fp1,fp2;
char ch;
fp1=fopen("c:\\file1.txt","r");//只读方式打开file1
if(fp1==NULL)
{
cout<<"fail to open file1.txt";
exit(1);
}
if((fp2=fopen("c:\\file2.txt","w"))==NULL)
{
cout<<"fail to open file2.txt";
fclose(fp1);
exit(1);
}
while(!feof(fp1))
{
ch=fgetc(fp1);
putchar(ch);//文件内容读到内存后显示在屏幕上
if(ch>='0'&&ch<='9')
fputc(ch,fp2);
}
fclose(fp1);fclose(fp2);
system("pause");
return 0;
}

向文件写入字符串:fputs

  • 函数原型:int fputs(char *str,FILE *fp)

  • 功能:将str指向的字符串的内容输入到fp所指文件的当前位置,同时将fp自动向前移动strlen(str)个字符位置。

  • 参数:

    • str: 字符串常量/字符串指针/字符数组名等。
    • fp: 文件指针,指向要被写入的文件。
  • 返回值:

    • 正常:非负整数。
    • 异常:EOF(-1)。

    说明:

    1. 字符串结束符\0不输入到文件;
    2. 不自动在字符串末尾加换行符。
    3. fputs自带换行。

从文件读取字符串:fgets

  • 函数原型:int fgets(char *str,int n,FILE *fp)

  • 功能:从由fp指出的文件中读取n-1个字符,并把它们存放到由str指出的字符数组中,最后自动加上\0

  • 参数:

    • str: 接受字符串的内存地址,可以是数组名或指针。
    • n:要读取字符的个数
    • fp: 文件指针,指向要被读取字符的文件。
  • 返回值:

    • 正常:字符串的内存首地址(str的值)。
    • 异常:NULL。

    说明:

    1. 读入n-1个字符到文件时,遇到换行符或文件结束符则提前结束;
    2. 读入结束后,系统自动加\0

例2 字符串读写函数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
using namespace std;
int main()
{
FILE *fp;
char s[100];
if((fp=fopen("file1.txt","w"))==NULL)
{
cout<<"can't open file1!\n";
exit(1);
}
fputs("I am a teacher.",fp);
fputs("I love my homeland!",fp);
fclose(fp);//必须先关闭,再以只读的方式打开。
if((fp=fopen("file1.txt","r"))==NULL)
{
cout<<"can't open file1!\n";
exit(1);
}
while(!feof(fp))//指针指到的地方没结束的话
{
fgets(s,16,fp);//每次读取15个字符
puts(s);
}
fclose(fp);//关闭指针
system("pause");
return 0;
}

从文件读取数据块:fread

  • 函数原型:int fread(void *buffer, int size, int count, FILE *fp)
  • 功能:
    • 从文件指针fp所指的文件的当前位置读取字节数为size大小的数据块共count个,存到buffer所指的内存存储区中。
  • 参数:
    • buffer:指向存放读入数据存储区的首地址的指针。
    • size:数据块的字节数,即一个数据块的大小。
    • count:一次读入数据块的数量。
    • fp:文件指针,指向要从中读出数据的文件。
  • 返回值:
    • 正常返回:实际读取数据块的个数,即count。
    • 异常返回:0。

向文件写入数据块:fwrite

  • 函数原型:int fwrite(void *buffer, int size, int count, FILE *fp)
  • 功能:
    • 将以buffer为起始地址的长度为size的count个数据块输出到文件指针fp所指的位置去。
  • 参数:
    • buffer:指向存放输出数据存储区的首地址的指针。
    • size:数据块的字节数,即一个数据块的大小。
    • count:一次输出数据块的数量。
    • fp:文件指针,指向要从中写入数据的文件。
  • 返回值:
    • 正常返回:实际输出数据块的个数,即count。
    • 异常返回:0。

块读写应用

freadfwrite常用于读写二进制文件。

例:

假设fp指向以二进制形式打开的可读写文件,并有如下说明:

float f; double d[10];

常见的块读写应用示例:

1
2
3
4
fwrite(&f,sizeof(float),1,fp); //把浮点数f写入文件
fwrite(d,sizeof(double),10,fp); //把数组d中所有数写入文件
fread(&f,sizeof(float),1,fp); //从文件中以块形式读一浮点数到变量f中
fread(d,sizeof(d),1,fp); //从文件中一次性读一个数组d大小的数据块到数组d中
例3

从键盘上读入5个学生的成绩信息,将它们以文件的形式保存在磁盘上。然后再将从文件中读取学生信息,并在屏幕上显示出来。假设学生成绩信息包括学生姓名、学号、总分。

分析:

程序处理的数据为结构数组,应选择块读写的方式进行文件操作,具体步骤如下:

  • 声明学生成绩结构类型
  • 定义学生成绩结构数组
  • 以块读写的方式对学生信息进行文件读写

程序实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include N 5
#include <iostream>
using namespace std;
struct student
{
char name[20];
char num[8];
double score;
};
int main()
{
struct student s[N],t[N];
int i;
FILE *fp;
if ((fp=fopen("student.dat","wb"))==NULL)
{
cout<<"can't openstudent.dat";
exit(1);
}//判定是否成功打开
for(i=0;i<N;i++)
{ cin>>s[i].name>>s[i].num>>s[i].score;
fwrite(&s[i],sizeof(student),1,fp); //对于第i个学生,逐元素向文件写数据
}
fclose(fp);//写文件结束先关闭,再以读的方式打开
if ((fp=fopen("student.dat","rb"))==NULL)
{
cout<<"can't openstudent.dat";
exit(1);
}
fread(t,sizeof(t),1,fp); //一次性从文件读出整个数组
for(i=0;i<N;i++)
cout<<s[i].name<<' '<<s[i].num<<' '<<s[i].score<<endl;
fclose(fp);
system("pause");
return 0;
}

向文件格式化写入数据:fprintf

  • 函数原型:int fprintf(FILE *fp,const char *format,arg_list)
  • 功能说明:将变量表列(arg_list)中的数据,按照format指出的格式,写入由fp指定的文件。
  • 参数说明:
    • fp:文件指针,指向将数据写入的文件。
    • format:指向写出数据格式字符串的指针.
    • arg_list:要写入文件的变量表列,各变量之间用逗号分隔。
  • 返回值:
    • 正常返回:输出的字节数。
    • 异常返回:负值。
  • 示例:fprintf(fp,"%d%s",4,"China");
    • 表示将整数4和字符串”China”按整数和字符串的形式写入fp所指的文件中。

从文件格式化读取数据 :fscanf

  • 函数原型:int fscanf(FILE *fp,const char *format,add_list)

  • 功能说明:按照format指出的格式,从fp指定的文件中读取数据存 放至地址表列(add_list)的变量中。

  • 参数说明:

    • fp:文件指针,指向要从中读取数据的文件。
    • format:指向读取数据格式字符串的指针。
    • add_list:存放读取数据的变量的地址表列。
  • 返回值:

    • 正常返回:成功读取的参数的个数。
    • 异常返回:EOF(-1)。
  • 示例:

    fscanf(fp,"%d%d" ,&x,&y);

    表示从fp所指的文件中顺序读取两个整数给变量x和y。

    地址符号&不能省略。

fscanf_s

vs2022那令人恼火的安全函数

和fscanf区别就是fscanf_s比fscanf多了个域宽检查,会多一个参数

1
2
fscanf(fp,"%s",str);
fscanf_s(fp,"%s",str,20);//fsanf_s会比fscanf多一个说明要读出数据内存大小的数字。

读取多个数据:

1
fscanf_s(fp, "%s %s %s", s1,20, s2,20, s3,20);

读取多个数字的字符串:

1
fscanf_s(fp, "%s %s %s %d %d", s1, 20, s2, 20, s3, 20,&a,&b);
例4

用格式化读写文件方式实现例三相同的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include N 5
#include <iostream>
using namespace std;

int main()
{
char name[20],num[8];
double score;
int i;
FILE *fp;
if ((fp=fopen("student.dat","wb"))==NULL)
{
cout<<"can't openstudent.dat";
exit(1);
}//判定是否成功打开
for(i=0;i<N;i++)
{ cin>>name>>num>>score;
fprintf(fp,"%s %s %lf",name,num,score); //对于第i个学生,逐元素向文件写数据
}
fclose(fp);//写文件结束先关闭,再以读的方式打开
if ((fp=fopen("student.dat","rb"))==NULL)
{
cout<<"can't openstudent.dat";
exit(1);
}
for(i=0;i<N;i++)
{
fscanf(fp,"%s%s%lf",name,num,&score);//name和num无需地址符号,因为它们是数组名
cout<<name<<' '<<num<<' '<<score<<endl;
}
fclose(fp);
system("pause");
return 0;
}

随机读写

  • 顺序读写文件只能从头开始,顺序读写各个数据。
  • 随机读写可按需要只读写文件中某些指定的部分。
  • 随机读写的关键是要按要求移动位置指针,即进行文件的定位
  • 实现文件定位、移动文件内部位置指针的函数主要有rewind函数 和fseek函数。

rewind函数

  • 函数原型:void rewind(FILE *fp)
  • 功能说明:使指示文件位置的指针重新返回到文件开始

fseek函数

  • 函数原型:int fseek(FILE *fp, long offset, int whence)
  • 功能说明:使文件指针fp移到基于whence的相对位置offset处。
  • 参数说明:
    • offset是相对whence的字节位移量,用长整型表示。
    • whence是移动的基准,常用符号常量表示。
  • 返回值:
    • 正常返回:0。
    • 异常返回:-1,表示定位操作出错。

whence:

符号常量

基准位置

SEEK_SET

0

文件开头

SEEK_CUR

1

当前读写的位置

SEEK_END

2

文件尾部

示例:

1
2
3
4
fseek(fp,1234L,SEEK_CUR);
//把读写位置从当前位置向后移动1234字节(L后缀表示长整数)
fseek(fp,0L,2);
//把读写位置移动到文件末尾

作业

1

编写程序,从键盘输入一串字符,要求将该串字符的倒序串先写入到文件f1.txt中,然后再将原字符串的内容接到该文件的末尾。例如,假设从键盘输入的字符串为“How do you do?”,则文件f1.txt的内容为:

?od uoy od woHHow do you do?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "iostream"
#define N 256
using namespace std;
int main()
{
FILE* fp;
char s[N],t[N];
int i;
errno_t err = fopen_s(&fp,"C:\\Users\\pc\\Desktop\\ctest.txt", "w");//只写方式打开file1
if (fp == NULL)
{
cout << "fail to open ctest.txt";
exit(1);
}
cout << "输入字符串:" << endl;
gets_s(s);
for (i = 0; s[i] != '\0'; i++)
t[i] = s[strlen(s) - i - 1];
t[i] = '\0';
fputs(t, fp);
fseek(fp, 0L, 2);
fputs(s, fp);
fclose(fp);
errno_t err1 = fopen_s(&fp,"C:\\Users\\pc\\Desktop\\ctest.txt", "r");
if (fp == NULL)
{
cout << "can't open ctest.txt";
exit(1);
}
while (!feof(fp))//指针指到的地方没结束的话
{
fgets(s, 31, fp);//每次读取31个字符
puts(s);
}
fclose(fp);
system("pause");
return 0;
}

2

用记事本建立一个文本文件f2.txt,在该文件中任意存放一组整数。编写程序统计该文件中正整数、负整数和零的个数。(提示:用fscanf函数读取文件中的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "iostream"
#define N 256
using namespace std;
int main()
{
FILE* fp;
int num=0,pos=0,neg=0,zero=0;
errno_t err = fopen_s(&fp,"C:\Users\pc\Desktop\f2.txt", "r");
if (fp == NULL)
{
cout << "fail to open ctest.txt";
exit(1);
}
while (!feof(fp))
{
fscanf_s(fp, "%d", &num);
if (num > 0)
pos++;
else if (num < 0)
neg++;
else if (num == 0)
zero++;
}
fclose(fp);
cout << "正整数有" << pos << "个,负整数有" << neg << "个,零有" << zero << "个。" << endl;
system("pause");
return 0;
}

负整数好像多了一个?

3

‎将从键盘输入的N个学生的学号和成绩存入到文件student.dat中。再从文件中读取学生的信息,求出最高分、最低分和总分。N可通过符号常量自行定义大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include "iostream"
#define N 5 //N个学生
using namespace std;
struct student
{
char num[8];
double score;
};
int main()
{
FILE* fp,* fp1;
student s[N], t[N];
int i;
errno_t err = fopen_s(&fp,"student.dat", "wb");
if (fp == NULL)
{
cout << "fail to open student.dat";
exit(1);
}
for (i = 0; i < N; i++)
{
cout << "输入第" << i+1 << "位学生的学号和成绩:" << endl;
cin >> s[i].num >> s[i].score;
fwrite(&s[i], sizeof(student), 1, fp);
}
fclose(fp);
errno_t err1 = fopen_s(&fp1, "student.dat", "rb");
if (fp1 == NULL)
{
cout << "fail to open student.dat";
exit(1);
}
double max = 0, min = 999, sum = 0,score;
char num[N];
fread(t, sizeof(t), 1, fp);
for (i = 0; i < N; i++)
{
sum += t[i].score;
if (t[i].score > max)
max = t[i].score;
if (t[i].score < min)
min = t[i].score;
}
fclose(fp1);
cout << "最高分:" << max << "最低分:" << min<< "总分:" << sum<< endl;
system("pause");
return 0;
}

4

编写程序,将一存放一行字符串的文本文件中的每一个字符及其所对应的ASCII码输出到屏幕上。例如文件的内容是“I love China”,则输出:

I(73) (32)l(108)o(111)v(118)e(101) (32)C(67)h(104)i(105)n(110)a(97)

注意:空格也是有效字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "iostream"
#define N 100
using namespace std;
int main()
{
FILE* fp;
int i;
char s[N];
errno_t err = fopen_s(&fp,"C:\Users\pc\Desktop\ctest.txt", "r");//ctest.txt是一个桌面文本文件
if (fp == NULL)
{
cout << "fail to open ctest.txt";
exit(1);
}
for (i = 0; !feof(fp); i++)
s[i]=fgetc(fp);
s[i] = '\0';
fclose(fp);
for (i = 0; s[i] != '\0'; i++)
{
cout << s[i] << "(";
printf("%d", s[i]);
cout<< ")" << ' ';
}
cout << endl;
system("pause");
return 0;
}