1. 文件概述

1.1 什么是文件I/O

文件 I/O(Input/Output)指的是程序与外部文件之间的数据传输操作。在许多编程语言中,包括C、C++、Python等,都提供了用于进行文件 I/O 操作的内置函数或库。

文件 I/O 操作主要涉及两个方面:读取文件和写入文件。

读取文件:

  1. 打开文件:使用文件打开函数(如fopen)打开待读取的文件,指定文件名和打开模式(如只读、读写等)。
  2. 读取文件内容:使用文件读取函数(如fread、fgets等)从打开的文件中读取内容,将内容保存到程序中的变量中进行处理。
  3. 关闭文件:使用文件关闭函数(如fclose)关闭已打开的文件,释放系统资源。

写入文件:

  1. 打开文件:使用文件打开函数(如fopen)打开待写入的文件,指定文件名和打开模式(如只写、追加等)。
  2. 写入文件内容:使用文件写入函数(如fwrite、fprintf等)将程序中的数据写入到打开的文件中。
  3. 关闭文件:使用文件关闭函数(如fclose)关闭已打开的文件,释放系统资源。

在计算机中,我们最常见的就是磁盘文件,磁盘文件由文件系统管理,文件系统负责在磁盘上分配、组织和管理文件的存储空间。每个磁盘文件都有一个唯一的文件名,可以通过文件名来定位和访问文件。只有在使用的时候磁盘数据才会被加载到内存中

1.2 磁盘文件分类

计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储

img

从用户或者操作系统使用的角度(逻辑上)把文件分为两大类:

  • 文本文件:基于字符编码的文件
    • 可读性高:文本文件的内容是针对人类可读的,可以使用文本编辑器或终端窗口进行查看和编辑。
    • 无格式化信息:文本文件不包含任何格式化信息,如字体、颜色、大小或页面布局等
    • 逐行存储:文本文件通常以逐行的方式存储文本内容。每行以换行符(如回车符或换行符)结尾,用于划分不同的文本行。
    • 压缩效率较低:文本文件通常包含了大量重复的文本数据,而通用的文本压缩算法效果有限。
    • 通用性好:文本文件使用的是常见的字符编码(ASCII、UTF-8、UTF-16 等),在各种平台和操作系统上被广泛支持和使用。
  • 二进制文件:基于值编码的文件
    • 机器可读性:二进制文件不是以可读的文本形式存储的,其中的数据以计算机能够理解和处理的方式进行编码。
    • 可包含复杂的结构:这些结构(如数组、记录、树、图等)在文件中具有特定的布局和编码方式,以便计算机能够正确读取和解析它们。
    • 任意数据类型:二进制文件可以包含任意的数据类型,包括整数、浮点数、布尔值、字符、字节、指针等。它们会编码为特定的字节序列
    • 高效的存储空间利用:二进制文件可以使用各种压缩算法、编码方式和数据结构,以减小文件大小并提高数据的存储效率。
    • 平台和操作系统相关性:二进制文件的数据编码方式和存储结构可能与特定的机器、操作系统或平台相关,所以在跨平台或不同操作系统之间处理二进制文件时需要适应相应的技术和工具。

文本文件在许多领域中都有着广泛的应用,如日志文件、配置文件、源代码文件、文档、电子邮件等。它是人与计算机之间常用的信息交流和数据存储方式之一。

二进制文件在许多应用中扮演着重要的角色,如可执行文件、图像文件、音频文件、视频文件、数据库文件等。它们提供了一种灵活、高效的方式来存储和处理大量的非文本数据,为各种应用程序和领域提供了丰富的功能和性能。

2. 文件的打开和关闭

2.1 文件指针

在C语言中,文件指针是用来处理文件读写操作的一种特殊类型的指针。C语言提供了一组文件操作函数,通过这些函数可以创建、打开、关闭、读取和写入文件,文件指针在这些操作中起到了关键的作用。

在使用文件指针之前,需要先定义一个指向FILE类型的指针变量,用于表示文件指针。FILE类型定义在stdio.h头文件中。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
short level; // 缓冲区"满"或者"空"的程度
unsigned flags; // 文件状态标志
char fd; // 文件描述符
unsigned char hold; // 如无缓冲区不读取字符
short bsize; // 缓冲区的大小
unsigned char* buffer; // 数据缓冲区的位置
unsigned ar; // 指针,当前的指向
unsigned istemp; // 临时文件,指示器
short token; // 用于有效性的检查
}FILE;

FILE是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息

在进行文件操作的时候,定义文件指针的方式如下:

1
FILE *filePointer;

C语言中对文件的各种操作都是基于文件指针来实现的,每操作一个文件都应该让其对应一个文件指针:

image-20231006085027785

在C语言中,有三个默认的文件指针,它们对应标准的输入/输出流,可以用于读取用户输入和向屏幕输出信息,或者用于错误信息的输出。

  1. 标准输入(stdin):标准输入是默认的输入文件指针。它通常与键盘相连,用于接收用户的输入。可以使用输入函数如scanf来读取标准输入的内容。

  2. 标准输出(stdout):标准输出是默认的输出文件指针。它通常与屏幕相连,用于向屏幕输出信息。可以使用输出函数如printf来向标准输出打印内容。

  3. 标准错误(stderr):标准错误是默认的错误输出文件指针。它也通常与屏幕相连,用于输出错误信息。可以使用输出函数如fprintf来向标准错误打印错误信息。

这些文件指针是预定义的,可以在程序中直接使用,无需手动打开或关闭。它们的文件指针类型是FILE*

例如,可以使用scanf函数从标准输入读取用户的输入:

1
2
3
int num;
// 从标准输入读取一个整数
scanf("%d", &num);

也可以使用printf函数向标准输出打印信息:

1
2
3
int num = 10;
// 向标准输出打印信息
printf("The number is: %d\n", num);

而对于错误信息的输出,可以使用fprintf函数向标准错误打印错误信息:

1
2
// 向标准错误输出错误信息
fprintf(stderr, "Error: Division by zero\n");

在C语言中,默认的文件指针提供了简单而便捷的输入和输出功能,使得对用户输入的处理和对程序输出的控制更加灵活和方便。

2.2 打开文件

除了标准输入(stdin)、标准输出(stdout)和标准错误(stderr)三个文件指针可以直接使用以外,其它的文件指针在使用之前必须进行初始化,初始化方式就是将相应的文件打开,这需要用到C语言提供的fopen函数。

fopen函数的原型如下:

1
2
#include <stdio.h>
FILE *fopen(const char *filename, const char *mode);
  • 参数:
    • filename:指定要打开的文件名,需要加上路径(相对、绝对路径)
    • mode:指定文件的打开模式
  • 返回值:
    • 成功:返回指向打开文件的文件指针
    • 失败:返回 NULL

关于fopen函数第二个参数mode对应的文件打开模式如下表:

打开模式 含义
rrb 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
wwb 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
aab 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件
r+rb+ 以可读、可写的方式打开文件(不创建新文件)
w+wb+ 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+ab+ 以添加方式打开文件,打开文件并在末尾更改文件,若文件不存在则创建文件
  • b是二进制模式的意思
    • b只是在Windows有效
    • Linuxrrb的效果是一样的
  • Unix/Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾
  • Windows平台下,以“文本”方式打开文件,不加b
    • 读取文件的时候,系统会将所有的"\r\n"转换成 "\n"
    • 写入文件的时候,系统会将 "\n" 转换成"\r\n"写入
    • "二进制"方式打开文件,则读\写都不会进行这样的转换
  • Unix/Linux平台下,“文本”“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出

使用fopen函数打开一个文件,可以使用相对路径,也可以使用绝对路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ========== 相对路径 ========== 
// 打开当前目录 hello.txt 文件
FILE* fp = fopen("hello.txt", "r");
fp = fopen("./hello.txt", "r");

// 打开当前目录(test)下 hello.txt 文件
fp = fopen("./test/hello.txt", "r");

// 打开当前目录上一级目录(相对当前目录)hello.txt 文件
fp = fopen("../hello.txt", "r");

// ========== 绝对路径 ==========
// 打开D盘 test 目录下一个叫 hello.txt 文件
fp = fopen("d:\\test\\hello.txt", "r");
fp = fopen("d:/test/hello.txt", "r");
  1. 相对路径(Relative Path):相对路径是相对于当前工作目录(Working Directory)的路径表示。它指定了从当前工作目录到目标文件或目录的相对关系。
  2. 绝对路径(Absolute Path):绝对路径是一个完整的路径表示,从文件系统的根路径开始,描述了从根路径到目标文件或目录的完整路径。

Windows中路径的根节点的对应的盘符,描述路径使用的分隔符是反斜杠\,但是在C语言中反斜杠是转义字符,所以需要将其转换为普通字符,正确的写法是写两个反斜杠\\,除此之外还可以使用斜杠/来描述路径。

Linux中路径的根节点的/,描述路径使用的分隔符是/

下面是一个示例程序,它演示了如果打开文件和判断文件是否打开成功了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
FILE* fp = fopen("hello.txt", "r");
if (fp == NULL)
{
perror("fopen");
}
else
{
printf("文件打开成功!\n");
}
return 0;
}

如果指定打开的文件不存在就会输出如下信息:

1
fopen: No such file or directory

perror是一个C标准库函数,它可以将最后一次发生的错误信息输出到终端。它的函数原型如下:

1
2
#include <stdio.h>
void perror(const char *str);

perror函数接受一个字符串参数 str,用于作为错误信息的前缀。它会自动获取最近一次错误的错误码,并将相应的错误描述信息格式化输出到标准错误流(stderr)中。

2.3 关闭文件

通过open函数可以打开一个文件,并基于得到的文件指针对文件进行各种操作,使用完毕后还需要将文件关闭,其原因如下:

  • 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
  • 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败

C语言中提供的文件关闭函数是fclose,函数原型如下:

1
2
#include <stdio.h>
int fclose(FILE * stream);
  • 参数:接受一个文件指针 stream,用于指定要关闭的文件。它会将缓冲区中的数据写回到文件中,并释放与文件相关的资源。
  • 返回值:返回一个整数值来指示关闭操作的成功与否。
    • 成功:关闭文件,它会返回0;
    • 失败:返回非零值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
FILE* fp = fopen("hello1.txt", "r");
if (fp == NULL)
{
perror("fopen");
return -1;
}
else
{
printf("文件打开成功!\n");
flcose(fp);
}
return 0;
}

如果没有在程序中明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭,但是还是建议大家养成用完之后随时关闭文件的好习惯。

3. 文件的读写

3.1 按照字符读写文件

3.1.1 写文件

fputc是一个C标准库函数,用于将一个字符写入到文件中。它的函数原型如下:

1
2
3
#include <stdio.h>
int fputc(int character, FILE *stream);
int putc(int character, FILE *stream);
  • 参数:
    • character:要写入的字符,注意这个参数是整形
    • stream:文件指针,对应要写入字符的文件
  • 返回值:
    • 成功:返回写入的字符
    • 失败:返回 EOF

在标准C库中,putc函数实际上是一个宏,而不是一个真正的函数。可以将其视为fputc函数的别名。因此,它们的功能是相同的。

以下是一个示例,展示了如何使用fputc函数将字符写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

int main()
{
char* buf = "One world one dream!";
FILE* fp = fopen("example.txt", "w");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

int len = strlen(buf);
for (int i = 0; i < len; ++i)
{
int ch = fputc(buf[i], fp);
printf("%c", ch);
}
printf("\n");
fclose(fp);

return 0;
}

在上面的示例中,程序打开名为example.txt的文件以供写入,使用fputc函数将字符串"One world one dream!"写入文件,并根据返回值将写入文件的字符打印到终端。

需要注意的是,fputc函数每次只能写入一个字符。如果需要连续写入多个字符,可以像上面程序中一样使用循环的方式多次调用fputc函数。

fputc函数通常用于以字符形式写入文件,特别适用于处理文本文件。如果需要以二进制形式写入数据,可以使用fwrite函数。

3.1.2 读文件

fgetc是一个C标准库函数,用于从文件中读取一个字符。它的函数原型如下:

1
2
3
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);

fgetc函数接受一个文件指针 stream,用于指定要从中读取字符的文件。它会从文件中读取一个字符,并返回读取的字符(或者在到达文件结尾或发生错误时返回EOF)。

在标准C库中,getc函数实际上是一个宏,而不是一个真正的函数。可以将其视为fgetc函数的别名。因此,它们的功能是相同的。

以下是一个示例,展示了如何使用fgetc函数从文件中读取字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main()
{
FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

char ch;
while ((ch = fgetc(fp)) != EOF)
{
printf("%c", ch);
}
printf("\n");
fclose(fp);

return 0;
}

fgetc函数每次只能读取一个字符。在上面的示例中,程序以只读的方式打开名为example.txt的文件,并通过循环使用fgetc函数读取了文件中的所有字符。

fgetc函数通常用于以字符形式读取文件,特别适用于处理文本文件。如果需要以二进制形式读取数据,可以使用fread函数。

3.1.3 EOF

在C语言中,EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。

1
#define EOF  (-1)

当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ANSI C提供一个feof函数,用于检查文件流的文件结束标志feof函数既可用以判断二进制文件又可用以判断文本文件。

feof的函数原型如下:

1
2
#include <stdio.h>
int feof(FILE *stream);

feof函数接受一个文件指针 stream,用于指定要检查的文件流。它会检查文件流的文件结束标志:

  • 返回一个非零值(真):表示已经到文件结尾
  • 返回0(假):表示没有到文件结尾

以下是一个示例,展示了如何使用feof函数检查文件的结束标志:

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
#include <stdio.h>

int main()
{
FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

char ch;
while ((ch = fgetc(fp)))
{
if (feof(fp))
{
printf("\nReached end of file.\n");
break;
}
printf("%c", ch);
}
printf("\n");
fclose(fp);

return 0;
}

程序打开名为example.txt的文件以供读取,并使用feof函数在每次读取字符前检查文件结束标志。

需要注意的是,在读取文件时,feof函数需要在读取操作之后进行调用,以保证能够正确判断文件结束标志。

3.2 按照行读写文件

3.2.1 写文件

fputs是一个C标准库函数,用于将字符串写入文件。它的函数原型如下:

1
2
#include <stdio.h>
int fputs(const char *string, FILE *stream);
  • 参数:

    • string:字符串的指针,表示要写入的内容
    • stream:文件指针,用于指定要写入字符的文件
  • 返回值:

    • 成功:返回一个非负值
    • 失败:返回EOF

以下是一个示例,展示了如何使用fputs函数将字符串写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main()
{
const char* string = "Hello, world!";
FILE* fp = fopen("example.txt", "w");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return -1;
}

if (fputs(string, fp) == EOF)
{
printf("Failed to write the string.\n");
return -1;
}
fclose(fp);

return 0;
}

在上面的示例中,程序打开名为example.txt的文件以供写入,并使用fputs函数将字符串"Hello, world!"写入文件中。

需要注意的是,fputs函数只能写入字符串,而不能写入其他类型的数据。如果需要将其他类型的数据写入文件,可以使用例如fprintf函数等。

此外,fputs函数每次只能写入一个字符串,如果需要连续写入多个字符串,可以多次调用fputs函数。如果需要在每个字符串之间添加换行符或其他分隔符,需要在调用fputs之后手动添加。

3.2.2 读文件

fgets是一个C标准库函数,用于从文件中读取一行字符。它的函数原型如下:

1
2
#include <stdio.h>
char *fgets(char *string, int size, FILE *stream);
  • 参数:

    • string:字符指针,用于存储读取的字符
    • size:指定要读取的最大字符数(包括终止符)
    • stream:文件指针,用于指定要从中读取字符的文件
  • 返回值:

    • 成功:返回参数string的首地址
    • 失败:返回NULL

以下是一个示例,展示了如何使用fgets函数从文件中读取一行字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main()
{
char line[100]; // 假设一行最多100个字符

FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

while (fgets(line, sizeof(line), fp) != NULL)
{
printf("Read line: %s", line);
}
fclose(fp);

return 0;
}

在上面的示例中,程序打开名为example.txt的文件以供读取,并使用fgets函数从文件中逐行读取字符。每次成功读取一行后,程序将该行输出到控制台。如果到达文件尾或发生错误,则退出循环。

需要注意的是,fgets 读取的行包括换行符,因此在输出时,可以选择输出整行(包括换行符)或者忽略换行符。

fgets函数通常用于以文本行的形式读取文件内容,特别适用于处理文本文件。如果需要以二进制方式从文件中读取数据,可以使用fread函数。

3.3 按照块读写文件

3.3.1 写文件

fwrite是一个C标准库函数,用于以二进制形式将数据写入文件。它的函数原型如下:

1
2
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • 参数:

    • ptr:指向要写入数据的指针
    • size:要写入的每个元素的字节数
    • count:要写入的元素数量
    • stream:文件指针,用于指定要写入数据的文件
  • 返回值:

    • 成功:返回写入的元素数量,即count的值
    • 失败:或者返回一个小于count的值

fwrite函数会将指针ptr指向的数据写入到文件中。写入的总字节数是sizecount相乘的积。

以下是一个示例,展示了如何使用fwrite函数将数据以二进制形式写入文件:

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
#include <stdio.h>
#include <string.h>

struct Person
{
char name[20];
int age;
double height;
};

int main()
{
struct Person person;
FILE* fp = fopen("example.bin", "wb");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

strncpy(person.name, "John Doe", sizeof(person.name));
person.age = 30;
person.height = 1.8;

if (fwrite(&person, sizeof(person), 1, fp) != 1)
{
printf("Failed to write the data.\n");
return 1;
}
fclose(fp);

return 0;
}

在上面的示例中,程序创建了一个名为example.bin的二进制文件,用于写入Person结构体的数据。程序使用fwrite函数将person结构体的数据写入文件中。

需要注意的是,示例程序中fwrite函数是以二进制形式写入数据,因此在读取数据时,也需要以二进制方式读取,使用fread函数来读取相应的数据。

3.3.2 读文件

fread是一个C标准库函数,用于从文件中以二进制形式读取数据。它的函数原型如下:

1
2
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • 参数:

    • ptr:指向用于存储读取数据的缓冲区的指针
    • size:每个元素的字节数
    • count:要读取的元素数量
    • stream:文件指针,用于指定要从中读取数据的文件
  • 返回值:

    • 成功:返回实际读取的元素数量
    • 失败:返回的元素数量与count不相等

fread函数会从文件中读取指定数量的元素,每个元素占据size个字节,将它们存储在ptr指向的缓冲区中。

以下是一个示例,展示了如何使用fread函数从文件中以二进制形式读取数据:

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
#include <stdio.h>

struct Person
{
char name[20];
int age;
double height;
};

int main()
{
struct Person person;
FILE* fp = fopen("example.bin", "rb");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

if (fread(&person, sizeof(person), 1, fp) != 1)
{
printf("Failed to read the data.\n");
return 1;
}

printf("Name: %s\n", person.name);
printf("Age: %d\n", person.age);
printf("Height: %f\n", person.height);
fclose(fp);

return 0;
}

在上面的示例中,程序以二进制形式打开名为example.bin的文件,并使用fread函数读取文件中的数据到person结构体中。

需要注意的是,fread函数是以二进制形式读取数据,因此在写入数据时,也需要以二进制方式写入,使用fwrite函数来写入相应的数据。

3.4 文件指针偏移

3.4.1 fseek

fseek是一个C标准库函数,用于在文件中定位文件指针的位置。它的函数原型如下:

1
2
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
  • 参数:
    • stream:文件指针,用于指定要定位的文件
    • offset:指定文件指针的偏移量
    • whence:指定文件指针的起始位置,它对应的值有三个
      • SEEK_SET:表示从文件开始处偏移,偏移量为offset个字节
      • SEEK_CUR:表示从当前位置偏移,偏移量为offset个字节
      • SEEK_END:表示从文件末尾处偏移,偏移量为offset个字节
  • 返回值:
    • 成功:返回 0
    • 失败:返回非零值

以下是一个示例,展示了如何使用fseek函数来定位文件指针的位置:

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
#include <stdio.h>

int main()
{
FILE* fp = fopen("example.txt", "rb");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

char buf[1024] = { 0 };
fseek(fp, 10, SEEK_SET);
fread(buf, 1, sizeof(buf), fp);
printf("从头部偏移10字节开始读数据, 内容是: %s\n", buf);

// rewind(fp);
fseek(fp, 0, SEEK_SET);
fread(buf, 1, sizeof(buf), fp);
printf("从头部开始读数据, 内容是: %s\n", buf);

fclose(fp);

return 0;
}

在上面的示例中,程序打开了名为example.txt的文件以供读取,并使用fseek函数将文件指针从开始处偏移10个字节,然后再把文件中的数据读出来,此时文件指针已经移动到了最后,如果要从头读文件需要再次把文件移动到文件头。

需要注意的是,fseek函数在二进制文件和文本文件中的行为可能有所不同。

  • 在文本文件中,由于使用的编码不同,字符和行的长度可以可变,因此将文件指针定位在某个位置可能无法准确找到这个位置。
  • 在二进制文件中,fseek函数可以在任意位置准确定位。

如果想要将文件指针移动到文件的头部,有两种写法:

1
2
3
fseek(fp, 0, SEEK_END);   // 文件指针移动到文件尾部
fseek(fp, 0, SEEK_SET); // 文件指针移动到文件头部
rewind(fp); // 文件指针移动到文件头部

rewind是一个C标准库函数,它用于将文件指针重新定位到文件的起始位置。它的函数原型如下:

1
2
#include <stdio.h>
void rewind(FILE *stream);

rewind函数接受一个参数:stream是一个文件指针,用于指定要重新定位的文件。

另外,ftell函数可用于获取当前文件指针的偏移量。

3.4.2 ftell

ftell是一个C标准库函数,用于获取文件指针的当前位置(偏移量)。它的函数原型如下:

1
2
#include <stdio.h>
long ftell(FILE *stream);
  • 参数:

    • stream:文件指针,用于指定要获取当前位置的文件。
  • 返回值:

    • 成功:表示文件指针相对于文件起始位置的偏移量。
    • 失败:返回值为负数

以下是一个示例,展示了如何使用ftell函数获取文件指针的当前位置:

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 <stdio.h>

int main()
{
long position;
FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

position = ftell(fp);
if (position < 0)
{
printf("Failed to get the current position.\n");
return 1;
}
printf("Current position: %ld\n", position);

fseek(fp, 15, SEEK_SET);
position = ftell(fp);
printf("Current position: %ld\n", position);

fclose(fp);

return 0;
}

在上面的示例中,程序打开了名为example.txt的文件以供读取,并使用ftell函数获取文件指针的当前位置。

需要注意的是,ftell函数返回的偏移量是以字节为单位的相对值。初始位置为0,向文件末尾方向的偏移量为正,向文件开始方向的偏移量为负。

此外,在Linux系统中,还可以使用ftello函数来处理大文件(超过2GB)的偏移量。这些函数返回的偏移量类型为off_t,可以处理更大的文件。

4. 删除、重命名文件

4.1 删除文件

remove是一个C标准库函数,用于删除文件。它的函数原型如下:

1
int remove(const char *filename);

remove函数接受一个字符串参数 filename,用于指定要删除的文件的路径和名称。它会删除指定路径下的文件,如果成功删除则返回0,否则返回非零值。

以下是一个示例,展示了如何使用remove函数删除文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
if (remove("example.txt") != 0)
{
printf("Failed to delete the file.\n");
return 1;
}
printf("File deleted successfully.\n");

return 0;
}

在上面的示例中,程序调用remove函数尝试删除名为example.txt的文件。如果成功删除文件,则输出"File deleted successfully.",否则,输出"Failed to delete the file."

请注意,使用remove函数删除文件时需谨慎,因为该操作是不可撤销的。可以在调用remove函数之前,确保文件不再需要,并且已经完成对文件的所有操作。另外,需要确保拥有足够的权限来删除文件,否则删除操作可能会失败。

4.2 重命名文件

rename是一个C标准库函数,用于重命名文件或将文件移动到另一个位置。它的函数原型如下:

1
int rename(const char *old_filename, const char *new_filename);

rename函数接受两个字符串参数 old_filenamenew_filename,分别表示原始文件名和新文件名(包括路径)。它会将原始文件重命名为新文件名或将文件移动到新位置。如果成功重命名或移动文件,则返回0;否则,返回非零值。

以下是一个示例,展示了如何使用rename函数重命名文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
if (rename("oldname.txt", "newname.txt") != 0)
{
printf("Failed to rename the file.\n");
return 1;
}
printf("File renamed successfully.\n");

return 0;
}

在上面的示例中,程序调用rename函数将名为oldname.txt的文件重命名为newname.txt。如果成功重命名文件,则输出"File renamed successfully.",否则输出"Failed to rename the file."

请注意,使用rename函数重命名文件时需谨慎,因为该操作是不可撤销的。同时,还需要确保有足够的权限来执行重命名操作,并且新文件名不会与现有文件冲突。

此外,rename函数还可以用于将文件移动到不同的目录中,只需要在新文件名中指定目标文件夹的路径。例如:

1
rename("file.txt", "new/location/newfile.txt");

这样,就可以将文件file.txt移动到new/location目录下,并命名为newfile.txt。但是一定要注意,该函数并不会创建新的目录,也就是说要保证new/location/目录是存在的,移动才能成功。

5. 文件缓冲区

ANSI C标准采用“缓冲文件系统”处理数据文件。缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。

如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量)。

image-20231006102829346

我们编写的C程序和磁盘文件之间的关系是这样的:

  • 磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在程序需要访问的时候加载到内存

  • 通过程序对内存中的数据进行编辑处理后,再将其保存到磁盘文件中

  • 程序与磁盘文件之间交互,可以不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率

    • 磁盘(机械硬盘)读写速度相较于内存不是一个数量级,频繁读写磁盘效率是比较低的
    • 给文件函数提供缓冲区以提高效率,这相当于是空间换时间

fflush是一个C标准库函数,用于将输出缓冲区的内容立即写入文件。

它的函数原型如下:

1
2
#include <stdio.h>
int fflush(FILE *stream);

fflush函数接受一个文件指针 stream 作为参数,用于指定要刷新缓冲区的文件。它会将输出缓冲区中的内容强制写入文件,并清空缓冲区。

fflush函数返回一个整数值来指示刷新操作的成功与否。如果成功刷新缓冲区,它会返回0;否则,返回非零值。通常情况下,我们可以将其与0进行比较,以确定刷新是否成功。

以下是一个示例,展示了如何使用fflush函数将输出缓冲区的内容立即写入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int main()
{
FILE* fp = fopen("example.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}

fprintf(fp, "Hello, World!");
if (fflush(fp) != 0)
{
printf("Failed to flush the output buffer.\n");
return 1;
}
fclose(fp);

return 0;
}

在上面的示例中,程序打开名为example.txt的文件进行写入操作,并使用fprintf函数向文件写入字符串。然后,使用fflush函数刷新输出缓冲区,将字符串立即写入文件中。

使用fflush函数可以确保数据被及时写入文件,而不是在程序结束时或者缓冲区达到一定大小时才写入。这在某些情况下很有用,例如当需要及时查看或共享文件内容时。

fprintf是一个C标准库函数,用于将格式化的数据写入文件中。

它的函数原型如下:

1
2
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);

fprintf函数接受一个文件指针 stream、一个格式化字符串 format 和一系列的可变参数,用于按照指定的格式将数据写入到指定的文件中。

fprintf函数与 printf 函数的用法类似,不同之处在于它将结果输出到指定的文件,而不是标准输出流(stdout)。

6. 附录

字符编码是一种将字符映射到数字表示的方法。由于计算机只能处理数字,因此需要一种方式将字符转换为可以在计算机中存储和处理的数据。

常见的字符编码方式有以下几种:

  1. ASCII(American Standard Code for Information Interchange):ASCII是最早和最简单的字符编码方案,使用7位二进制数表示128个常用的字符,包括英文字母、数字、标点符号和一些控制字符。ASCII编码在计算机系统中得到广泛应用。

  2. Unicode:Unicode是一种更全面的字符编码方案,用于表示世界上几乎所有的字符集。Unicode编码使用固定的代码点表示字符,可以包含范围广泛的字符集,包括所有国际语言的字符、符号和标点符号。Unicode使用不同的编码方案,最为广泛使用的是UTF-8、UTF-16和UTF-32。

    • UTF-8(Unicode Transformation Format-8):UTF-8是一种可变长度字符编码方案,可以表示Unicode字符集中的所有字符。UTF-8使用1到4个字节表示不同的字符,对于ASCII字符,使用1个字节表示,而对于非ASCII字符,使用多个字节表示。UTF-8是互联网上最常用的字符编码方式之一。

    • UTF-16(Unicode Transformation Format-16):UTF-16是一种固定长度字符编码方案,使用16位或32位的代码单元表示不同的字符。UTF-16可以表示基本多语言平面(BMP)中的所有字符,对于非BMP字符,使用一对代理对来表示。UTF-16在一些基于Windows的系统中广泛使用。

    • UTF-32(Unicode Transformation Format-32)是一种将Unicode字符编码为32位无符号整数的字符编码方案。它使用固定长度的四个字节来表示每个Unicode字符,因此每个字符都被编码为一个32位整数。在实践中,UTF-32一般用于要求高效随机访问和字符处理的应用,例如一些特定的系统级库或字符处理算法。对于一般的文本存储和传输,通常推荐使用UTF-8或UTF-16编码,以平衡存储空间和处理效率的需求。

字符编码在计算机领域中非常重要,它决定了计算机如何处理和表示文本数据。选择适当的字符编码方式可以确保文本正确地显示和处理,避免乱码和字符转换错误的问题。在实际应用中,需要根据具体的需求和环境选择合适的字符编码方式。