You are on page 1of 28

第七章 文件 与 结构体

文件是根据特定的目的而收集在一起的有关数据的集合。 C++ 把每一个文


件都看成是一个有序的字节流,每个文件都以文件结束标志结束,如果要操作某个
文件,程序必须首先打开该文件。当一个文件被打开后,该文件就和一个流关联起
来,这里的流实际上是一个字节序列。
C++ 将文件分为文本文件和二进制文件。二进制文件一般含有特殊的格式或计
算机代码,如图文件和可执行文件等。文本文件则是可以用任何文字处理程序阅读
和编辑的简单 ASCII 文件。
下面我们学习如何编写 C++ 代码来实现对文本文件的输入和输出。
第一节 文件操作

C++ 语言提供了一批用于文件操作的标准函数,本节不
是介绍文件打开函数 fopen ,而是介绍另一个函数 freopen ,
它们都包含于标准库 cstdio 中,文件操作基本步骤如下:
(1) 打开文件,将文件指针指向文件,决定打开文件
类型;
(2) 对文件进行读、写操作;
(3) 在使用完文件后,关闭文件。
 一、重定向版

【命令格式】

FILE * freopen ( const char * filename, const char * mode, FILE * stream );

【参数说明】

filename: 要打开的文件名

mode: 文件打开的模式,和 fopen 中的模式 (r/w) 相同

stream: 文件指针,通常使用标准流文件 (stdin/stdout/stderr)

其中 stdin 是标准输入流,默认为键盘; stdout 是标准输出流,默认为屏幕;


stderr 是标准错误流,一般把屏幕设为默认。通过调用 freopen ,就可以修改标准
流文件的默认值,实现重定向。
【使用方法】

因为文件指针使用的是标准流文件,因此我们可以不定义文件指针。接下来我们使
用 freopen() 函数以只读方式 r(read) 打开输入文件 slyar.in 。

格式: freopen("slyar.in", "r", stdin);

然后使用 freopen() 函数以写入方式 w(write) 打开输出文件 slyar.out 。

格式: freopen("slyar.out", "w", stdout);

接下来的事情就是使用 freopen() 函数的优点了,我们不再需要修改 scanf , printf ,


cin 和 cout 。而是维持代码的原样就可以了。因为 freopen() 函数重定向了标准流,使其
指向前面指定的文件,省时省力。最后只要使用 fclose 关闭输入文件和输出文件即可。

格式: fclose(stdin);fclose(stdout);

若要恢复句柄,可以重新打开标准控制台设备文件,只是这个设备文件的名字是与
操作系统相关的。

格式: freopen("CON", "r", stdin);


代码模版 :

#include<cstdio> // 使用 freopen 语句,须调用 cstdio 库


int main()
{
freopen("slyar.in", "r", stdin);
freopen("slyar.out", "w", stdout);

/* 中间按原样写代码,什么都不用修改 */

fclose(stdin);fclose(stdout);
return 0;
}
例 9.1 从 in.txt 文件中读入数据,把它们的和保存 out.txt 文件中。
#include<cstdio>
int main()
{
freopen("in.txt","r",stdin); // 定义输入文件名
freopen("out.txt","w",stdout); // 定义输出文件名
int temp,sum=0;
while (scanf("%d",&temp)==1) //(cin>>temp) 从输入文件中读入数据
// 在 C++ 中非 0 为真
{
sum=sum+temp;
}
printf("%d\n",sum); // cout<<sum<<endl;
fclose(stdin);fclose(stdout); // 关闭文件,可省略
return 0;
}
in.txt 数据:
1 2 3 4 5
out.txt 结果:
15
说明:
while (fin>>temp) 和 (scanf("%d",&temp)==1) 主要是用于判断数据是否已经读完,
以便及时终止循环。还可以用成员函数 eof 来判断是否达到数据流的末尾。对
scanf 、 printf 和 cin 、 cout 语句都适用。
 二、 fopen 版

 重定向用起来很方便,但并不是所有算法竞赛都允许读写文件。
甚至有的竞赛允许访问文件,但不允许使用 freopen 这样的重定向方式
读写文件,可以使用 fopen 版,对 scanf 和 printf 语句适用。程序如下:

 #include<cstdio>
 using namespace std;
 int main()
 {
 FILE *fin,*fout;
 fin = fopen("in.txt","rb"); // 定义输入文件名
 fout = fopen("out.txt","wb"); // 定义输出文件名
 int temp,sum=0;
 while (fscanf(fin,"%d",&temp)==1) // 从输入文件中读入数据
 {
 sum=sum+temp;
 }
 fprintf(fout,"%d\n",sum); // cout<<sum<<endl;
 fclose(fin);fclose(fout); // 关闭文件,可省略
 return 0;
 }
先声明变量 fin 和 fout (暂且不用管 FILE * 为何物),把 scanf 改成 fsc
anf ,第一个参数为 fin ;把 printf 改成 fprintf ,第一个参数为 fout ,最后执行 f
close ,关闭两个文件。

重定向和 fopen 两种方法各有优劣。重定向的方法写起来简单、自然,但


是不能同时读写文件和标准输入输出; fopen 的写法稍显繁琐,但是灵活性比较大
(例如可以反复打开并读写文件)。顺便说一句,如 果把 fopen 版的程序改成读
写标准输入输出,只需赋值 fin=stdin ; fout=stdout ;即可,不要调用 fopen 和
fclose 。

程序如下:
#include<cstdio>
using namespace std;
int main()
{
FILE *fin,*fout;
fin=stdin;
fout=stdout;
/* 本处语句同上 */
fprintf(fout,"%d\n",sum);
return 0;
}
三、文件输入输出流

在 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;
}

如果想再次使用 cin 和 cout ,是否要逐个把程序中的所有 fin 和 fout 替换为 cin 和


cout ?不用这么麻烦,只需要把 fin 和 fout 的声明语句去掉,并加上这样两行即可:
#define fin cin
#define fout cout

用条件编译,还可以让程序在本机上读写标准输入输出,比赛测试时读写文件(请
读者自行实验)。
第二节 结构体

在实际问题中,一组数据往往具有不同的数据类型。
例如,人口大普查时,我们需要记录每一位公民的
姓名,年龄,性别,住址,身份证号码。这些信息
分别要用整型,字符型,字符串型来记录。为了解
决问题, C++ 语言给出了另一种构造数据类型
——“结构体”,它在数据存储方面相当于其他高
级语言中的记录,但它有着面向对象的优势。
 一、结构体 (struct) 定义和操作
 1. 定义结构体及结构体变量
 结构体变量的定义有两种形式:
 (1) 定义结构体类型的时候同时定义变量
 struct 结构体类型名 { // 其中 struct 是关键字
 成员表 ; // 可以有多个成员
 成员函数 ; // 可以有多个成员函数,也可以没有
 } 结构体变量表 ; // 可以同时定义多个结构体变量,用“,”
隔开
 例如:
 struct student{// 定义类型名叫 student 的 struct 类型
 string name;
 int chinese,math;
 int total;
 } a[110]; // 同时定义了 a 数组变量
(2) 先定义结构体再定义结构体变量
 struct 结构体类型名 {
 成员表 ;
 成员函数 ;
 };
 结构体名 结构体变量表 // 同样可以同时定义多个结构体变

 例如:
 struct student{
 string name;
 int chinese,math;
 int total;
 };
 student a[110];
 在定义结构体变量时注意,结构体变量名和结构体名不能相
同。在定义结构体时,系统对其不分配实际内存。只有定义
结构体变量时,系统才为其分配内存。
 2. 结构体变量的特点
 ( 1 )结构体变量可以整体操作,例如:
 swap(a[j],a[j+1]);
 ( 2 )结构体变量的成员访问也很方便、清晰,例如:
 cin>>a[i].name;
 ( 3 )结构体变量的初始化和数组的初始化类似,例如:
 student op={"gaoxiang",89,90,179};
 3. 成员调用
 结构体变量与各个成员之间引用的一般形式为:
 结构体变量名 . 成员名
 对于上面定义的结构体变量,我们可以这样操作:
 cin>>a[i].name; // 一般情况下不能写 cin>>a[i];
 a[i].total=a[i].chinese+a[i].math; // 就像用整型变量一样
 实际上结构体成员的操作与该成员类型所具有的操作是一致的。
 成员运算符“ .” 在存取成员数值时使用,其优先级最高,并具有左结
合性。在处理包含结构体的结构体时,可记作:
 strua.strub.membb
 这说明结构体变量 strua 有结构体成员 strub ;结构体变量 strub 有成员
membb 。
 4. 成员函数调用
 结构体成员函数调用的一般形式为:
 结构体变量名 . 成员函数

 例 7.2 成绩统计。输入 N 个学生的姓名和语文、数学的得分,按总分从高到低输出,
分数相同的按输入先后输出。
 输入格式:
 第 1 行,有一个整数 N , N 的范围是 [1…100] ;下面有 N 行,每行一个姓名,
2 个整数。姓名由不超过 10 个的小写字母组成,整数范围是 [0…100] 。
 输出格式:
 总分排序后的名单,共 N 行,每行格式:姓名 语文 数学 总分。
 输入样例:
 4
 gaoxiang 78 96
 wangxi 70 99
 liujia 90 87
 zhangjin 78 91
 输出样例:
 liujia 90 87 177
 gaoxiang 78 96 174
 wangxi 70 99 169
 zhangjin 78 91 169
 分析:由于姓名是字符串,分数是整数,如果用数组保存,则要两个数组,
比如:
 string name[100];
 int score[100][3];
 这种方法不利于把一个学生的信息当成一个整体处理。
 下面程序中通过使用结构( struct )类型的方法来解决这个问题。
 #include<iostream>
 #include<string>
 using namespace std;
 struct student{
 string name;
 int chinese,math;
 int total;
 }; // 定义一个 struct 的类型,类型名叫: student
 student a[110]; // 定义一个数组 a ,每个元素是 student 类型
 int n;
 int main()
 { cin>>n;
 for (int i=0; i<n; i++) // 对结构体中成员的赋值、取值。
 { cin>>a[i].name;
 cin>>a[i].chinese >>a[i].math;
 a[i].total=a[i].chinese+a[i].math;
 }
 for (int i=n-1; i>0; i--)
 for (int j=0; j<i; j++) // 冒泡排序
 if (a[j].total<a[j+1].total)
 swap(a[j],a[j+1]);
 for (int i=0; i<n; i++) // 输出
 cout<<a[i].name<<' '<<a[i].chinese<<' ' <<a[i].math<<'
'<<a[i].total<<endl;
 return 0;

 二、结构体 (struct) 的使用
 例 7.3 离散化基础。以后要学习使用的离散化方法编程中,通常要知道每个
数排序后的编号( rank 值)。
 输入格式:
 第 1 行,一个整数 N ,范围在 [1…10000] ;第 2 行,有 N 个不相同的
整数,每个数都是 int 范围的。
 输出格式:
 依次输出每个数的排名。
 输入样例 :
 5
 82694
 输出样例 :
 41352
 分析:排序是必须的,关键是怎样把排名写回原来的数“下面”。程序
使用了分别对数值和下标不同关键词 2 次排序的办法来解决这个问题,一个
数据“节点”应该包含数值、排名、下标 3 个元素,用结构体比较好。
 程序如下:
 #include<iostream>
 #include<algorithm>
 using namespace std;
 struct node{
 int data; // 数值
 int rank; // 排名
 int index; // 下标
 }; // 定义 struct 类型
 node a[10001];
 int n;
 bool comp1(node x,node y)
 {
 return x.data<y.data;
 } // 自定义比较函数
 bool comp2(node x,node y)
 {
 return x.index<y.index;
 } // 同上
 int main()
 { cin>>n;
 for (int i=1; i<=n; i++)
 cin>>a[i].data,a[i].index=i;
 sort(a+1,a+1+n,comp1); // 根据值排序,求排名 rank
 for (int i=1; i<=n; i++) a[i].rank=i;
 sort(a+1,a+1+n,comp2); // 根据下标排序,回到原数据次序
 for (int i=1; i<=n; i++)
 cout<<a[i].rank<<' ';
 return 0;
比静态的数组的存取慢,很多 OI 选手就用数组模拟指针。现在就来学习一下这种方法
的编程。
有 N 个点,编号从 1 到 N 。有 M 条边,每条边用连接的 2 个顶点表示,如:
( 3 , 8 ),表示顶点 3 和 8 之间的边(无向边)。请输出每个顶点通过边相邻的顶
点。
输入格式:
第 1 行, N 和 M 两个整数, N 范围在 [1…5000] , M 范围在 [1…100000] ;下面有
M 行,每行两个整数,表示一条边。
输出格式:
N 行,第 i 行的第 1 个数 k 表示有多少边和 i 号顶点相连,后面有 k 个数,表示哪
k 个顶点和 i 连接为一条边。
输入样例:
5 6
1 3
2 4
1 4
2 3
3 5
2 5
输出样例:
2 4 3
3 5 3 4
3 5 2 1
2 1 2
2 2 3
分析:本题中邻接链表的每个节点有 2 个成员:
struct node
{
int v; // 定点编号
int next; // 链表的下一个节点的下标
};
一开始我们给足够大的数组,保证可以保存所有边。每读入一条
边( a , b ),把 a 插入到 b 的链表前,再把 b 插入到 a 的链表前。
程序如下:
#include<iostream>
using namespace std;
struct node{
int v;
int next;
}; // 定义 struct 类型
node a[200001]; // 无向图,空间是边的两倍
int n,m,p; //p 为 a 数组的空余空间下标
int k[5001],c[5001]; // 邻接链表的表头, k 数组记长

void insert(int u,int v) // 把 v 点插入到 u 点的邻接链表前
{
a[++p].v=v; // 申请一个新节点
a[p].next=c[u];
c[u]=p; // 插入到 u 链表头
k[u]++; // 链表长度增加
}
int main()
{
cin>>n>>m;
for (int i=1; i<=m; i++)
{
int u,v;
cin>>u>>v;
insert(u,v); // 插入 v 点到链表 u
insert(v,u); // 插入 u 点到链表 v
}
for (int i=1; i<=n; i++)
{
cout<<k[i]<<‘ ’; // 表的长度
for (int j=c[i]; j>0; j=a[j].next) // 遍历链表
cout<<a[j].v<<' ';
cout<<endl;
}
return 0;
}
三、联合 (union) 的定义和使用

联合也称为共用体,是一种数据格式,能够存储
不同类型的数据,但同一时间只能储存其中的一种类型
数据。例如:
union color
{
int rgb[3];
int cmyk[4];
};
“color c;” 可以定义一个颜色变量 c , c 可以有
两种方式记录颜色: rgb 形式或者 cmyk 形式,但不能
同时记录。
显然联合的用途之一:当数据项使用多种格式(但
不会同时使用)时,可以节省空间。
例 7.5 注册帐号。某网站收集了 N 个人的注册帐号,账号类型有 2 种:身份
证号、 QQ 号。请编程用恰当的数据结构保存信息,并统计身份证中男性和女
性的人数,以及 QQ 号平均值(取整)。
输入格式:
第 1 行,一个整数 N ,范围在 [1…10000] ;下面 N 行,每行一个字符和
一个字符串。第一个字符表示账户类型,有 i 、 q 两种,第二个字符串是帐号
信息。
输出格式:
1 行, 3 个整数值:男性人数、女性人数、 QQ 号平均值。
输入样例:
6
i 522635197008278006
i 51170219740419175X
i 45102519760724935X
q 505165
q 34012459
i 511702198606266283
输出样例:
2 2 17258812
分析:如果只是单纯做本题,编程很简单。但为了展示 union 的使用方法,为
今后编写复杂的程序打下基础,程序采用规范编写。
程序如下:
#include<iostream>
using namespace std;
struct tid{
char type; // 账号类型,识别联合变量储存哪个类型
union // 省略类型名,更方便存取联合的成员
{
char idc[18]; // 存储身份证号
long long qq; // 存储 QQ 号
};
}; // 定义 struct 类型
tid a[10001];
int n;
int main()
{ cin>>n;
for (int i=1; i<=n; i++)
{ cin>>a[i].type;
switch (a[i].type) // 判断类型
{ case 'i':
for (int j=0; j<18; j++) cin>>a[i].idc[j];
break;
case 'q':
cin>>a[i].qq;
}
}
long long sum=0; int c=0; // 用于计算 QQ 号平均值
int men=0,women=0; // 用于计算男性、女性人数
for (int i=1; i<=n; i++)
{ if (a[i].type=='i')
{ if ((a[i].idc[16]-'0')%2) men++;
else women++;
}
if (a[i].type=='q') sum+=a[i].qq,c++;
}
cout<<men<<' '<<women<<' '<<sum/c;
return 0;
}
注意: union 里面不能有空间不确定的数据结构类型,比如 string 。
四、枚举 (enum) 的定义和使用
C++ 中枚举( enum )提供了另一种创建符号常量的方式,有些场合可以
代替 const ,甚至定义另一种自定义类型。例如:
enum color
{
red,green,blue,yellow,black,white
};
color x;
“color x;” 定义了一个叫 color 的枚举类型,并定义了这个类型的一
个变量 x 。 x 只能取 red 、 green 、…、 white 几个值。
red 、 green 、…、 white 可看成符号常量,对应数值 0 到 5 ,可以当成
整数常量运行。
枚举常量默认的值从 0 开始递增。但也可以使用赋值语句来人为设定枚举
量的值,例如:
enum bit
{
one=1,
two=2,
three=4,
four=8,
five=16
};
例 7.6 取数。有一个 N*N 的二维网格,每格里面有 1 个整数。现在给定开始
的位置( x,y )和方向(上、下、左、右之一),一直移动到网格的边界,计
算移动过程中线路上格子里的数字和。
输入格式:
1 行, 4 个整数,第 1 个整数 N ,范围在 [1…1000] ,第 2 、 3 个整数
是开始位置的坐标 X 和 Y ,表示在第 X 行 Y 列(编号 1 到 N ),第 4 个整数
D 表示方向, D=0 表示向上, D=1 表示向下, D=2 表示向左, D=3 表示向
右;下面 N 行,每行 N 个整数,范围在 [-1000…1000] 。
输出格式:
一个整数,数字和。
输入样例:
4232
1234
5987
8274
6638
输出样例:
22
分析:输入数据中方向可以用数字表示但编程中容易出错,特别是比较大的程
序更容易混乱。在 C++ 编程中采用枚举型可以有效减少失误。
程序如下:
#include<iostream>
using namespace std;
enum dir
{
_up=0,
_down=1,
_left=2,
_right=3 // 用前缀‘ _’ 避免变量名与 C++ 关键字冲突
}; // 定义 enum 类型
int a[1001][1001];
int n,x,y,d;
int main()
{
cin>>n>>x>>y>>d;
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
cin>>a[i][j];
int sum=0;
if (d==_up)
for (int i=x; i>=1; i--) sum+=a[i][y];
if (d==_down)
for (int i=x; i<=n; i++) sum+=a[i][y];
if (d==_left)
for (int i=y; i>=1; i--) sum+=a[x][i];
if (d==_right)
for (int i=y; i>=n; i++) sum+=a[x][i]; // 分别判断四个方向
cout<<sum;
return 0;
}

You might also like