文件入门
程序运行时,数据位于内存之中;程序终止时,数据就完全销毁了。为了让数据持久化,可以将其写入到文件这样的外部存储中,当再次启动程序时,我们便可以从文件中加载这些数据,让应用恢复到所需的状态 。文件从编码的方式来看,有文本文件和二进制文件两种。文本文件是基于字符编码的文件,常见的有ASCII、Unicode等,二进制文件把数据对应的二进制形式存储到文件中,是字节序列文件。
打开与关闭文件
C语言提供的文件操作函数,包含于stdio.h中,打开文件是通过fopen函数实现的。
FILE* fopen(char *filename, char *mode);
- 第一个参数是文件名。
- 第二个参数是文件打开模式,指出对该文件可进行的操作。
- 创建或打开成功时,返回该文件对应的FILE类型的指针;否则返回NULL。
如:
FILE *fp = fopen("hello.txt", "w"); // 创建文件hello.txt
if (fp == NULL)
printf("create file failed!\n");
当参数只有文件名时,默认文件在当前工作目录;这等同于显示的指出当前路径。
FILE *fp = fopen(".\\hello.txt", "w");
文件系统通常有两个保留的特殊目录,当前目录(.)、上层目录(..)。下图使用了dir命令列出了当前目录 c:\windows 下的文件信息,前两项就是(.)与(..);这两个目录在窗口模式下是不可见的。

当你使用进入目录命令 【cd .】 时,目录仍旧位于c:\windows下。

而当使用 【cd ..】 时,则会切换到根目录,即c盘下。

当需要指定文件的路径时,相对或绝对路径都可以,这里需要注意对路径分隔符'\'的转义。如:
FILE *fp1 = fopen("c:\\my_data\\hello.txt", "w"); // 指定绝对路径
FILE *fp2 = fopen("..\\debug\\data.dat", w); // 指定相对路径,打开当前工作目录的上层debug目录下的data.dat文件
当然,路径分隔符也支持'/'分割,这可以消除转义字符的影响。
FILE *fp1 = fopen("c:/my_data/hello.txt", "w");
FILE *fp2 = fopen("../debug/data.dat", w);
此外,fopen对于这个路径还有一定的智能处理,如下面的代码所示:
// 在当前目录下创建文本文件
FILE *fp = fopen( "./debug/../test.txt", "w" );
- ./debug/ 代表目标路径是当前目录下的debug子目录
- ./debug/../ 被解释为debug的上级目录
因此最终文件还会创建在当前目录下。
文件的基础打开模式如下表:
| 模式 | 含义 | 说明 |
|---|---|---|
| r | 只读 | 文件必须存在,否则打开失败。 |
| w | 只写 | 若文件存在,则清除原文件内容后写入;否则,新建文件后写入。 |
| a | 追加只写 | 若文件存在,在文件尾部追加写人,若文件不存在,则打开失败。 |
围绕"r","w","a"有以下扩展:
- "b":以原模式为基的二进制方式操作文件,如 "rb"(二进制读), "wb(二进制写)", "ab"(二进制追加)
- "+":使原模式可读写。"r+"(可读写,增加可写), "w+"(可读写,增加可读), "a+"(可读写,增加可读)
- "b+":使原模式可读写。"rb+"、"wb+"、"ab+"
现代操作系统的保护机制比较完善,有时使用fopen创建文件时并未成功。这可能意味着操作需要管理员权限。对于个人电脑,你的用户角色通常就是系统管理员,因此常常会忽略这种情况,如果碰到不能创建文件时,可以检查是否由此引起。由于fopen()函数在VS2022下属于非安全函数,故而在建立项目时,需要关闭SDL检查。
在文件使用完成后,我们需要使用fclose函数关闭文件;否则可能会造成其它用户不能访问此文件。
int fclose(FILE *fp);
参数fp是已打开的文件指针。正常关闭后返回0,否则返回EOF(-1)。
写文本文件
// 把字符c输出到fp所指的文件中
int fputc(int c, FILE *fp);
// 把字符串str输出到fp所指的文件中
int fputs(const char *str, FILE *fp);
// 文件格式化输出函数,把输出表列中的数据按照指定的格式输出到文件中。
int fprintf(FILE *fp, const char *format, ...);
这些函数执行成功后返回写入的字符数,否则返回EOF(-1)。
#include <stdio.h>
int main()
{
// 在当前目录下创建文本文件
FILE *fp = fopen("test.txt", "w");
if (fp == NULL)
{
printf("failed to open the file.\n");
return 1;
}
// 逐字符写文件
const char *web = "cs.glimix.com";
for (int i = 0; i < strlen(web); i++)
fputc(web[i], fp);
fputc('\n', fp);
// 使用fputs函数把文本信息写入文件
fputs("hello, world!\n", fp);
// 使用fprintf函数将格式化信息写入文件
int px = 2;
int py = 5;
double len = 3.14;
fprintf(fp, "point(%d,%d), length(%.2f)\n", px, py, len);
// 使用完成后关闭文件
fclose(fp);
printf("write done!");
return 0;
}
使用这些函数写文件时,数据并不一定会立即写入到文件中。操作系统为这些IO(输入输出)函数提供了一个中间缓冲区,数据会先被缓存于此,当写条件满足时,如缓冲区满、或强制刷新时,会一次性将这些数据写入到文件中。因为磁盘写入读取速度总是比较慢,这样会提高读写效率。你可以在fclose处设置断点,然后打开文件,会发现内容为空,而此时文件的大小是0字节。
读文本文件
文本文件的读取操作可通过下面三个函数实现:
// 从文件中读取一个字符
int fgetc(FILE *fp);
// 从文件中读取一个字符串
char* fgets(char *s, int size, FILE *fp);
// 格式化读取文件中数据
int fscanf(FILE *fp, const char *format, ....);
我们可以通过使用feof()函数是判断否到达文件结尾。
int feof(FILE * fp);
示例程序演示了使用fgetc每次从文件自身读取一个字符,显示到控制台。其中FILE,属于C语言提供的内置宏(先将它想像为内置的计算变量),指示当前源代码的文件名。
#include <stdio.h>
int main()
{
// 显示当前文件名
printf("read file: %s\n\n", __FILE__);
// 打开文件进行读取
FILE *fp = fopen(__FILE__, "r");
if (fp == NULL) return -1;
// 未到达文件结尾时一直读取
char ch;
while (!feof(fp))
{
// 读取一个字符并显示出来
ch = fgetc(fp);
printf("%c", ch);
}
fclose(fp);
return 0;
}

C语言还提供了如下几个类似的宏。
| 名称 | 描述 | 格式符 |
|---|---|---|
| __DATA__ | 记录文件的编译日期 | %s |
| __TIME__ | 记录文件的编译日期 | %s |
| __FILE__ | 记录文件绝对路径 | %s |
| __LINE__ | 记录文件已经被编译的行数 | %d |
| __FUNCTION__ | 当前所在函数名 | %s |
#include <stdio.h>
int main()
{
printf("date:\t %s\n", __DATE__);
printf("time:\t %s\n", __TIME__);
printf("file:\t %s\n", __FILE__);
printf("line:\t %d\n", __LINE__);
printf("func:\t %s\n\n\n\n\n", __FUNCTION__);
return 0;
}

接下来,我们使用fgets函数重写前面的例子。
char* fgets(char *s, int size, FILE *fp);
fget从fp所指向的文件内,读取若干字符(通常是一行),并在其后自动添加字符串结束标志'\0'后,存入s所指的缓冲内存空间中。读取操作在遇到回车换行符 或 已读取 size-1 个字符 或 文件结尾 时为止。因为会自动追加字符串结束标记,所以函数读取的字符串最大长度为 size-1。这也是示例中printf没有换行标记的原因。
#include <stdio.h>
int main()
{
FILE *fp = fopen(__FILE__, "r");
if (fp == NULL) return -1;
char line[256];
while (!feof(fp))
{
fgets(line, 256, fp);
printf("%s", line);
}
fclose(fp);
return 0;
}

函数 fscanf 可用于读取格式化的文本信息,这与 fprintf 刚好相对。
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char buffer[256];
// 写格式化文件
fp = fopen("format.txt", "w");
if (fp != NULL)
{
const char *names[3] ={"cs", "glimix", "com"};
for (int i = 0; i < 3; i++)
fprintf(fp, "%s\t\t %d \t %d\n", names[i], rand() % 100, rand() % 100);
fclose(fp);
}
// 读格式化文件
fp = fopen("format.txt", "r");
if (fp != NULL)
{
char str[64];
int x, y;
while (!feof(fp))
{
fscanf(fp, "%s\t\t %d \t %d\n", str, &x, &y);
printf("%s %d %d\n", str, x, y);
}
}
return 0;
}

示例程序中我们使用格式字符串:
"%s\t\t %d \t %d\n"
向文本format.txt写入了三组数据,之后在fscanf中以同样的格式字符串从文件读出数据。在使用fscanf读取时,你需要明确的告知它数据与数据之间的分割符是什么,比如(,)、(-)、(空格)等。由制表符'\t'生成的一个或多个空白可以看作是一个空白,因此使用格式字符串
fscanf(fp, "%s %d %d\n", str, &x, &y);
读取也是正确的。假如我们使用这样的写格式,
fprintf(fp, "%s:%d-%d\n", names[i], rand() % 100, rand() % 100);
此时生成的文件内容是:

在读取时,使用同样的格式字符串:
fscanf(fp, "%s:%d-%d\n", str, &x, &y);
或
fscanf(fp, "%s%d-%d\n", str, &x, &y);
都是错误的,虽然我们我们执行了格式化写,但使用fscanf在读取%s对应的字符串时,这些连续的没有空白分隔的字符都被读取字符串中了,后面又根据格式 %d-%d 读取两个整数,显然此时已经没有数据。

显然,保证fscanf正确的工作,原始数据需要有分隔符作出限定。
练习
1 编写程序练习使用文件的"a"模式。
2 通讯录导出的联系人信息通常是CSV格式,尝试分析并读取信息。
陕公网安备61011202001108号