C语言文件
1文件的基本概念
1.1文件分类
在程序设计中,一般谈的文件有两种:程序文件、数据文件。
1.1.1程序文件
一般包括源程序文件(后缀为.c),目标文件(后缀为.obj),可执行程序(后缀为.exe)。(注:若本文无特殊说明,均以64位的Windows 10操作系统为例)
1.1.2数据文件
程序运行时要从文件中读取数据的文件或向文件输出内容的文件,都统称为数据文件,根据数据的组织形式,数据文件又分为二进制文件和文本文件.
1.1.2.1二进制文件
数据在内存中以二进制的形式进行存储,如果不加转换的输出到外存,就是二进制文件.
1.1.2.2文本文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前进行转换,以ASCII码字符形式存储的文件就是文本文件.
1.1.2.3数据的存储
对于字符,一律以ASCII码的形式进行存储,而数值型数据既可以用ASCII码形式进行存储,也可以使用二进制形式存储.
1.2文件命名
对于每个文件都要有一个唯一的文件标识,以便用户识别和引用,文件标识被称为文件名.
通常一个文件名包含3个部分:文件路径+文件名主干+文件后缀.例如:d:\c语言\test.txt
.根据常识我们知道,在同一文件路径下不能拥有两个文件名主干和后缀完全相同的文件.
1.3文件缓冲区
ANSIC标准采用"缓冲文件系统"处理数据文件.文件缓冲系统指的是系统自动地在内存中为程序中每一个正在使用的文件开辟一个"文件缓冲区".从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后再一起送到磁盘上.同样的,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,充满缓冲区后再从缓冲区将数据输送到程序数据区.缓冲区的大小时根据C语言的编译系统决定的.
1.4文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用于存放文件的相关信息(文件的名字,文字当前的状态、文件的位置以及其他跟文件相关的信息等).这些信息保存在一个结构体变量中.该结构体类型的系统声明为FILE
,其对应的指针为FILE*
,是文件类型指针,简称文件指针.
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE
的变量,并且填充其中的信息,我们不用关心这其中的细节,一般通过文件指针来维护这个结构体变量.
例如:创建一个FILE*
的指针变量
FILE* pf;//文件指针变量
pf
是一个指向FILE
类型数据的指针变量.可以使pf
指向某个文件的文件信息区,通过该信息区就可以访问这个文件.即通过文件指针变量能够找到与它关联的文件.
2文件的操作
2.1 文件的打开和关闭
文件的最基础操作就是打开和关闭,文件在读写前应该先被打开,使用结束后应该关闭文件.
在程序中,打开文件的同时,会返回一个FILE*
的指针变量指向该文件,相当于建立了指针与文件的关系(1.4节).
ANSIC规定使用fopen
函数来打开文件,使用fclose
来关闭文件.这两个函数的声明如下:
FILE *fopen( const char *filename, const char *mode );
int fclose( FILE *stream );
对于一个文件,我们可能只从中读取数据,也可能只写入数据,有时希望往里追加数据等等,因此,打开文件时我们需要指定其模式.fopen
函数的第一个参数为要打开的文件名,第二个参数即为打开模式.对于文件的打开模式见下表.(要注意的是,对于程序来说,往文件写入数据相当于是输出数据,从文件里读入数据进来是输入数据,而对于文件则相反,表中的输入输出是针对程序而言的)
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文本文件 |
“a” (追加) | 向文本文件的末尾添加数据,打开一个已经存在的文本文件 | 出错 |
“rb” (只读) | 为了输入数据,打开一个已经存在的二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文本文件 |
“ab” (追加) | 向二进制文件的末尾添加数据,打开一个已经存在的二进制文件 | 出错 |
“r+”(读写) | 为了输入输出数据,打开一个已经存在的文本文件 | 出错 |
“w+”(读写) | 为了输入输出数据,打开一个文本文件 | 建立一个新的文本文件 |
“a+”(读写) | 为了在文件的末尾输入输出数据,打开一个文本文件 | 建立一个新的文本文件 |
“rb+”(读写) | 为了输入输出数据,打开一个已经存在的二进制文件 | 出错 |
“wb+”(读写) | 为了输入输出数据,打开一个二进制文件 | 建立一个新的二进制文件 |
“ab+”(读写) | 为了在文件末尾输入输出数据,打开一个二进制文件 | 建立一个新的二进制文件 |
例如:往一个文本文件写入一段文本
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("myfile.txt","w");
if(pf != NULL)
{
fputs("fopen example",pf);
}
fclose(pf);
pf=NULL;
}
其中fclose
函数和把指针置空应该成对出现,在C语言–动态内存&&通讯录的改造(可增长空间)一文中已经有过相关解释(关于free
函数的),此处不再赘述。
2.2文件的顺序读写
文件的读写就是从文件读取数据或输出数据,相应的数据输入输出函数有:
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | int fgetc( FILE *stream ); | 所有输入流 |
字符输出函数 | int fputc( int c, FILE *stream ); | 所有输出流 |
文本行输入函数 | char *fgets( char *string, int n, FILE *stream ); | 所有输入流 |
文本行输出函数 | int fputs( const char *string, FILE *stream ); | 所有输出流 |
格式化输入函数 | int fscanf( FILE *stream, const char *format [, argument ]… ); | 所有输入流 |
格式化输出函数 | int fprintf( FILE *stream, const char *format [, argument ]…); | 所有输出流 |
二进制输入函数 | size_t fread( void *buffer, size_t size, size_t count, FILE *stream ); | 文件 |
二进制输出函数 | size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream ); | 文件 |
“流”:是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为“流”。 在这里只说两种流,一种是标准的输入输出流,一种是文件。标准的输入流(stdin
)即为键盘,标准的输出流(stdout
)即为我们的显示器,此外还有其他的标准流,但不是我们这里讨论的重点。还有一种流则是我们这里讲到的文件。
2.2.1字符输入函数-fgetc
int fgetc( FILE *stream );
fgetc()
函数的功能是从文件指针指定的文件(或者其他的流中如键盘)中读入一个字符,该字符的ASCII值作为函数的返回值,若返回值为EOF
,说明文件结束,EOF
是文件结束标志,值为-1(ASCII值是大于0的整数)。
使用fgetc()
函数调用前,需要读取的文件必须是以读或读/写方式打开的,并且该文件应该已经存在。
读操作的位置由文件内部位置指针来确定,对于已经存在的文件,文件被打开时,文件内部位置指针指向文件的第一个字节。这时,调用fgetc()
函数读的是第一个字节的字符,读入一个字符以后,位置指针将自动向后移动一个字节,那么再调用一次fgetc()
函数,则读取的是第2个字符。连续调用该函数就可以读取文件的每个字符,并且可以使用EOF
来判断是否已经到了文件末尾。
使用案例:(文本文件test.txt中已有字符“abcde”)
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test.txt", "r");
int c = 0;
if(pf != NULL)
{
while (c != EOF)
{
c = fgetc(pf);
printf("%c\n",c);
}
}
return 0;
}
程序的运行结果如下:
2.2.2字符输出函数-fputc
int fputc( int c, FILE *stream );
把字符c
输出到FILE *
所指向的流上,同fgetc
函数,写入的文件必须是以写或读/写方式打开的,并且该文件应该已经存在,如果该文件不存在,则系统会在当前程序所在目录下或指定的目录下创建同名文件。
写操作的位置由文件内部位置指针来确定,文件被打开时,文件内部位置指针指向文件的第一个字节。这时,调用fputc()
函数写入的是第一个字节的字符,写入一个字符以后,位置指针将自动向后移动一个字节,那么再调用一次fputc()
函数,则写入的是第2个字符。
例如:把2.2.1中的文件内容写入到文件test1.txt中
int main(void)
{
FILE* pf = fopen("test.txt", "r");
int c = 0;
//pf为空指针,说明文件test.txt打开失败了,则不用拷贝,直接程序结束
if (pf == NULL)
{
perror("打开文件(test.txt)");
return;
}
else
{
FILE* pf1 = fopen("test1.txt", "w");
if (pf1 == NULL)
{
perror("打开文件(test1.txt)");
fclose(pf);//pf1为空指针说明test1.txt文件打开(或创建)失败了,则拷贝动作就不需要了,此时直接将test.txt文件也关闭
pf = NULL;
}
else
{
while (c != EOF)
{
c = fgetc(pf);
fputc(c, pf1);
}
}
}
return 0;
}
程序运行结果如下:
可以看出程序成功创建文件test1.txt并成功往里写入test.txt的数据。
2.2.3文本行输入函数-fgets
char *fgets( char *string, int n, FILE *stream );
该函数功能为从指定的流中读取数据,每次读取一行,并把它存储在string
所指向的字符串内。当读取(n-1)个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。确切地说,调用fgets
函数时,最多只能读入n-1个字符。读入结束后,系统将自动在最后加’\0
’,并以string
作为函数值返回,如果在未读满n-1个字符之时,已读到一个换行符或EOF
,则结束本次读操作,读入的字符串中最后包含读到的换行符。
案例1
读取n-1个字符然后结束。(从test.txt文件的第一行读取,n=3)
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test.txt", "r");
char str[5] = “****”;
if (pf == NULL)
{
perror("打开文件(test.txt)");
return;
}
else
{
fgets(str,3,pf);
printf("%s",str);
}
return 0;
}
程序运行结果如下:
可以看出程序在读取了n-1=2个字符后停止,并且在第二个字符的后边追加了’\0
’。
案例2
读取n-1个字符,但是没有读够n-1个字符遇到了文件结束符,导致结束。
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test.txt", "r");
char str[10] = “*********”;
if (pf == NULL)
{
perror("打开文件(test.txt)");
return;
}
else
{
fgets(str,10,pf);
printf("%s",str);
}
return 0;
}
程序的运行结果如下:
因为test.txt文件的第一行只有“abcde\0”,字符数不够n-1=9个,所以遇到EOF
,读取结束。
案例3
读取n-1个字符,但是没有读够n-1个字符遇到了换行符,导致结束(假设此时test.txt文件有三行字符,第一行是abcde,第二行是fghijk,第三行是lmnopq)。
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test.txt", "r");
char str1[10] = "*********";
char str2[10] = "*********";
char str3[10] = "*********";
char str4[10] = "*********";
if (pf == NULL)
{
perror("打开文件(test.txt)");
return;
}
else
{
fgets(str1,4,pf);
printf("str1=%s\n",str1);
fgets(str2, 5, pf);
printf("str2=%s\n", str2);
fgets(str3, 10, pf);
printf("str3=%s\n", str3);
fgets(str4, 10, pf);
printf("str4=%s\n", str4);
}
fclose(pf);
pf = NULL;
return 0;
}
程序的运行结果如下:
可以看出
- str1读取文件第一行的n-1=3个字符,即abc.此时第一行还剩“de\n”三个字符(应当注意,换行符\n一般在文件中不显示,但是它实际存在),文件内部指针指向字符d.
- 文件内部指针指向字符d,str2要读n-1=4个字符,但当文件内部指针指向换行符时,读取结束。同时在已经读取的字符串后边追加’\0’。此时str2=“de\n\0”(如上图所示),同时文件内部指针移动到下一行,即指向f.
- str3要读取n-1=9个字符,但当读取到换行符时只读够了7个字符,由于遇到了换行符,读取结束,同时在已经读取的字符串后追加‘\0’,同时文件内部指针移动到下一行,即指向l.
- str4分析则同案例2,由读者自行分析。
2.2.4文本行输出函数-fputs
int fputs( const char *string, FILE *stream );
该函数的功能时向流stream
(如果文件,则文件存在时打开该文件,文件不存在时创建一个文件)输入字符串string(字符串的结束标志’\0
'不输入进去),如果输入成功,则返回一个非负整数,此时文件内部指针指向下一个字节,否则返回-1.
使用案例:向test2.txt中输入字符串str1和str2。
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test2.txt", "w");
char str1[10] = "Hello\n";
char str2[10] = "World";
if (pf == NULL)
{
perror("打开文件(test.txt)");
return;
}
else
{
fputs(str1,pf);
fputs(str2, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果如下图所示:
2.2.5格式化输出函数-fprintf
int fprintf( FILE *stream, const char *format [, argument ]...);
该函数的作用是把数据以格式format
输出到流stream
,与函数printf
的使用基本类似,但函数printf
只是输出到屏幕上,而fprintf
则是输入到流stream
中。
使用案例:(把一个结构体变量的数据输入到文件中)
struct Student
{
char name[20];
int age;
char class[5];
};
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test3.txt", "w");
struct Student s = {"zhangsan",10,"2班"};
if (pf == NULL)
{
perror("打开文件(test3.txt)");
return;
}
fprintf(pf,"%s %d %s",s.name,s.age,s.class);
fclose(pf);
pf = NULL;
return 0;
}
程序运行结果如下:
2.2.6格式化输入函数-fscanf
int fscanf( FILE *stream, const char *format [, argument ]... );
该函数的功能是从流stream
中以格式format
读入数据输入到[argument]
。其使用方法和scanf
函数类似,只是scanf
函数是从键盘以格式format
读入数据,而该函数是从流stream
中读数据。
使用案例:把上述文件test3.txt的数据按照对应格式输入到新的结构体s1中
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test3.txt", "r");
struct Student s1 = {0};
if (pf == NULL)
{
perror("打开文件(test3.txt)");
return;
}
fscanf(pf, "%s %d %s", s1.name, &(s1.age), s1.class);
printf("%s\n",s1.name);
printf("%d\n", s1.age);
printf("%s\n", s1.class);
fclose(pf);
pf = NULL;
return 0;
}
程序的运行结果如下:
2.2.7二进制输出函数-fwrite
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
该函数的含义为从buffer
指向的数组向stream指向的文件输入count
个字节为size
的二进制数据。输入成功,返回实际输入的count
数(因为可能buffer
中的元素个数小于count
)。
使用案例:从数组向文件test4.txt中写入多个结构体(同一类型)数据。
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test4.txt", "w");
struct Student s1[] = { {"zhangsan",10,"2班"},{"lisi",5,"1班"},{"wangwu",12,"3班"}};
//size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
size_t ret1 = fwrite(&s1, sizeof(struct Student), 3, pf);
printf("ret1=%u\n", ret1);
fclose(pf);
pf = NULL;
return 0;
}
实际测试时发现,虽然s1里只有3个元素,但当count
大于3时仍能往文件里写如数据(超过3肯定造成了非法访问),对应的返回值也是count
。(不知道是编译器的问题还是函数自身问题,有待考证。因此实际使用时数组里有几个就写几个。)
2.2.8二进制输入函数-fread
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
其含义为从文件中读取count
个大小为size
字节的数据放入到buffer
所指向的数组中,返回实际读取的元素个数(因为可能实际读取的元素个数小于count
)。
使用案例:把test4.txt文件里的数据读取出来放到数组s2中去
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test4.txt", "r");
struct Student s2[5] = { 0 };
//size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t ret1 = fread(s2, sizeof(struct Student), 2, pf);
size_t ret2 = fread(s2, sizeof(struct Student), 2, pf);
printf("ret1=%u\n", ret1);
printf("ret2=%u\n", ret2);
fclose(pf);
pf = NULL;
return 0;
}
程序的调试结果如下:
可以看出,第一次调用fread
时,读两个大小为struct Student
的元素,文件里有3个,所以可以读到2个,返回值ret1
为2,此时文件内部指针从文件的开始移动了2个struct Student
大小的位置,即指向“wangwu
”所在的位置,第二次调用时,由于文件只剩一个大小为struct Student
的元素,所以在遇到EOF
后提前结束,只读到了1个元素,所以返回值ret2
为1.
应当注意的是,fread
的第一个参数是要放入数据的地址,如果第二次调用时仍然填s2
,则读取的数据会放到s2[0]
的位置把"zhangsan
"覆盖掉(所以这里用了s2+2
,即把读取的数据放在s2[1]
位置处),使用时应注意这一点。
2.3文件的其他函数
2.3.1fseek
int fseek( FILE *stream, long offset, int origin );
该函数的功能是设置文件指针stream
的位置。如果执行成功,stream
将指向以origin
(偏移起始位置:文件头0(SEEK_SET
),当前位置1(SEEK_CUR
),文件尾2(SEEK_END
))为基准,偏移offset
(指针偏移量)个字节的位置。如果执行失败(比如offset
超过文件自身大小),则不改变stream
指向的位置。
使用案例:
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test5.txt", "wb");
char str1[20],str2[20];
if (pf != NULL)
{
fputs("This is an apple.",pf);
fseek(pf,9,SEEK_SET);
fputs(" sam",pf);
}
fclose(pf);
pf = NULL;
return 0;
}
首先,向文档test5.txt中写入字符串"This is an apple.",随后把指针从文件头(SEEK_SET
)向后偏移了9个位置,即指向字符’n’,然后写入了字符串" sam",相当于把字符串"n ap"替换成了" sam",所以最终文件的结果应为"This is a sample."如下图所示:
2.3.2ftell
long ftell( FILE *stream );
该函数的功能是返回文件指针相对于起始位置的偏移量(单位为字节)。在2.2中,无论是输入还是输出函数,文件位置指针都会发生偏移,用该函数可以检测到指针相当于起始位置的偏移量。
使用案例:(以fread
的案例为基础)
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test4.txt", "r");
struct Student s2[5] = { 0 };
//size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
size_t ret1 = fread(s2, sizeof(struct Student), 2, pf);
long a = ftell(pf);
size_t ret2 = fread(s2, sizeof(struct Student), 2, pf);
long b = ftell(pf);
printf("ret1=%u,a=%d\n", ret1,a);
printf("ret2=%u,b=%d\n", ret2,b);
fclose(pf);
pf = NULL;
return 0;
}
程序的运行结果如下:
可以看出,一个struct Student
所占的字节数为32(在《C语言自定义数据类型》一文中有详细介绍结构体大小的计算方法,这里不再赘述),所以第一次读取后,读了两个struct Student
的大小,即读了
32
×
2
=
64
32\times2=64
32×2=64个字节,本次读取完后文件指针指向第64的下一个字节即65字节处,所以偏移量为65.同理可以分析出第二次读取后偏移量为97.
2.3.3rewind
void rewind( FILE *stream );
该函数的功能是让文件指针回到文件的起始位置。
使用案例:(以上边2.3.2的案例为基础)
#include <stdio.h>
int main(void)
{
FILE* pf = fopen("test4.txt", "r");
struct Student s2[5] = { 0 };
size_t ret1 = fread(s2, sizeof(struct Student), 2, pf);
long a = ftell(pf);
rewind(pf);
long b = ftell(pf);
printf("ret1=%u,a=%d\n", ret1, a);
printf("b=%d\n", b);
fclose(pf);
pf = NULL;
return 0;
}
第一次读取结束后直接使用rewind函数使文件指针回到开头(那么此时偏移量为0),所以程序的输出结果如下:
2.4文件结束判定-feof
int feof( FILE *stream );
文件读取过程中,feof
函数的返回值不能直接用来判断文件是否结束.正确的用法是当文件读取结束时,判断是读取失败结束,还是遇到了文件尾结束.
- 文本文件读取是否结束,判断返回值是否为
EOF
(fgetc
)或者NULL
(fgets
). - 二进制文件读取判断结束,判断其(
fread
)返回值是否小于要读取数据的个数。
文本文件判断结束的例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
int c;
FILE* fp = NULL;
fpr = fopen("test1.txt", "r");
if (fp == NULL)
{
perror("File opening failed"); //提示错误原因
return EXIT_FAILURE;
}
//fgetc当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fpr)) != EOF)
{
putchar(c);
}
putchar('\n');
//判断什么原因结束
if (ferror(fpr))
{
//判断是否因读取失败结束
// int ferrot(FILE* stream),没有错误返回0,有错返回飞0
puts("I/O error when reading");
}
else if (feof(fpr))
{
//判断是否遇到文件尾结束
puts("End of file reached successfully");
}
fclose(fpr);
fpr = NULL;
return 0;
}
二进制文件判断结束的例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
double a[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 };
double b = 0.0;
size_t ret_code = 0;
FILE* fp = fopen("test6.txt", "wb");
fwrite(a, sizeof(*a), 5, fp); //写 double 的数组
fclose(fp);
fp = NULL;
fp = fopen("C:\\Users\\lenovo\\Desktop\\2.txt", "rb");
while ((ret_code = fread(&b, sizeof(double), 1, fp)) >= 1)
{
printf("%lf\n", b);
}
if (feof(fp))
{
printf("Error reading 2.txt:unexpected end of file\n");
}
else if (ferror(fp))
{
perror("Error reading 2.txt");
}
fclose(fp);
fp = NULL;
return 0;
}
通讯录的改造
在此前,实现了通讯录的动态增长(《通讯录的改造1》),但是退出程序后通讯录随即被销毁,无法实现保存,所以应当使用文件对数据进行保存。
通讯录的保存
实现策略是在退出前将数据进行保存(实际上应当在每次修改后都应保存,但是这样会使程序的效率降低,所以这里只采用退出前保存)。使用fwrite
函数来实现此功能:
//把通讯录保存到文件中
void save_contact(const Contact* pc)
{
int i;
FILE* pfwrite = fopen("Contact.dat", "wb");
if (pfwrite == NULL)
{
perror("Open Contact.txt(write)");
return;
}
else
{
for (i = 0; i < pc->sz; i++)
{
fwrite(pc->data + i, sizeof(PeoInfo), 1, pfwrite);
}
printf("通讯录已保存。\n");
}
fclose(pfwrite);
pfwrite = NULL;
}
通讯录的加载
在每次使用通讯录时,我们还应当先把保存的信息(如果有的话。在第一次使用时因为还没保存所以还没有,但是从第二次开始就会有信息)加载进来,这样我们才能看到联系人的信息。所以我们使用fread
函数把数据从文件中读出来。
void load_contact(Contact* pc)
{
printf("正在加载数据...\n");
PeoInfo tmp = { 0 };
FILE* pfread = fopen("Contact.dat", "rb");
if (pfread == NULL)
{
perror("Open Contact.txt(read)");
printf("加载失败!\n");
return;
}
else
{
//加载数据
while (fread(&tmp, sizeof(PeoInfo), 1, pfread))
{
check_capacity(pc);
pc->data[pc->sz] = tmp;
pc->sz++;
}
printf("数据加载成功。\n");
}
fclose(pfread);
pfread = NULL;
}
最终版
最终,完整的程序代码如下《通讯录的改造2(文件版)》。