文件 I/O (Input/Output)和标准 I/O 库是用于在 C 语言中进行文件操作的两种不同的方法。
1、文件I/O
文件 I/O(Input/Output)是指程序与文件之间进行数据交换的过程。在计算机编程中,文件 I/O 是通过读取和写入文件来实现数据的输入和输出操作。文件 I/O 主要涉及打开文件、读取文件内容、写入文件内容和关闭文件等操作。
常见的文件 I/O 操作包括使用系统调用(如 open()、read()、write()、close())来进行文件操作。通过文件 I/O,程序可以从文件中读取数据,对数据进行处理,然后将结果写入文件中,实现数据的持久化存储和处理。
在Linux系统中,一切皆文件是其核心设计理念之一,因此文件I/O操作在Linux系统中显得尤为重要。
1.1、文件描述符
文件描述符是操作系统中用于标识打开文件的整数值。它是进程与文件之间的桥梁,允许进程对文件进行读取、写入和其他操作。在Linux系统中,每个打开的文件都与一个文件描述符相关联,这个文件描述符是一个非负整数,通常是从0开始递增的。
文件描述符直接与操作系统的文件表项相关联,是操作系统提供的抽象。
举例来说,假设我们有一个C语言程序,打开了一个名为“example.txt”的文本文件进行读取。在这个程序中,文件描述符是用于表示这个打开的文件的整数值。当程序调用open函数打开文件时,操作系统会分配一个文件描述符,并将其返回给程序。程序可以使用这个文件描述符执行读取操作,如读取文件内容并将其输出到终端上。
代码语言:javascript复制#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd; // 文件描述符
char buf[1024]; // 用于存储读取的数据
// 打开文件 example.txt
fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
// 读取文件内容并输出到终端上
ssize_t bytes_read;
while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
write(STDOUT_FILENO, buf, bytes_read);
}
// 关闭文件
close(fd);
return 0;
}
在这个示例中,open函数打开文件example.txt并返回一个文件描述符,然后read函数使用这个文件描述符来从文件中读取数据。最后,close函数关闭文件,并释放对应的文件描述符。
1.2、open打开文件
在Linux系统中,操作文件需要先打开它以获取文件描述符,然后进行读写或其他操作,最后关闭文件。open函数可用于打开现有文件或创建新文件。函数原型如下所示:
代码语言:javascript复制#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数的参数和返回值含义如下:
-
pathname
:字符串类型,用于标识需要打开或创建的文件。它可以包含路径信息,可以是绝对路径或相对路径,例如:"./src_file"(当前目录下的 src_file 文件)或 "/home/dengtao/hello.c"。如果pathname
是一个符号链接,open
函数会对其进行解引用。 -
flags
:调用open
函数时需要提供的标志,包括文件访问模式标志以及其他文件相关标志。这些标志使用宏定义进行描述,并都是常量。open
函数提供了丰富的标志选项,我们可以单独使用某一个标志,也可以通过位或运算(|)将多个标志进行组合。 -
mode:
用于指定新建文件的访问权限,仅在flags
参数中包含O_CREAT
或O_TMPFILE
标志时有效。在Linux系统中,权限对于文件是一个重要的属性。我们可以使用touch
命令在Linux系统中创建一个文件,此时文件会有默认的权限。如果需要修改文件权限,可以使用chmod
命令进行修改。例如,在Linux系统下,我们可以使用ls -l
命令查看文件对应的权限。 - 返回值:成功将返回文件描述符,文件描述符是一个非负整数;失败将返回-1。
open
函数的flags
参数用于指定打开文件时的行为和权限。下面是一些常用的flags
参数值:
O_RDONLY
:只读方式打开文件。O_WRONLY
:只写方式打开文件。O_RDWR
:读写方式打开文件。O_CREAT
:如果文件不存在,则创建文件。O_EXCL
:与O_CREAT
一同使用,如果文件已经存在,则返回错误。O_TRUNC
:如果文件存在且为只写或读写打开,则将其长度截断为0。O_APPEND
:追加方式打开文件,在写入数据时追加到文件末尾。O_NONBLOCK
:非阻塞方式打开文件,在没有数据可读取时不阻塞。O_SYNC
:同步写入方式打开文件,对写入文件的每个操作进行同步。O_DIRECT
:直接IO方式打开文件,绕过系统缓存,数据直接读写到磁盘。O_TMPFILE
:创建一个临时文件,文件在关闭时自动删除。
open
函数的常用的mode参数:
S_IRUSR
:文件所有者读权限。S_IWUSR
:文件所有者写权限。S_IXUSR
:文件所有者执行权限。S_IRGRP
:文件组用户读权限。S_IWGRP
:文件组用户写权限。S_IXGRP
:文件组用户执行权限。S_IROTH
:其他用户读权限。S_IWOTH
:其他用户写权限。S_IXOTH
:其他用户执行权限。
在应用程序中使用 open 函数时,需要包含 3 个头文件“#include <sys/types.h>”、“#include <sys/stat.h>”、“#include <fcntl.h>”。
下面是一个使用 open
函数的简单示例:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
const char *filename = "example.txt";
int fd;
// 使用 open 函数打开文件,如果文件不存在,则创建
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 向文件写入内容
if (write(fd, "Hello, World!", 13) == -1) {
perror("write");
close(fd);
exit(EXIT_FAILURE);
}
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
printf("File created and written successfully.n");
return 0;
}
在这个示例中:
- 我们首先定义了一个文件名
example.txt
。 - 使用
open
函数打开文件,使用O_WRONLY
标志表示以只写方式打开文件,O_CREAT
标志表示如果文件不存在则创建,O_TRUNC
标志表示如果文件存在则将其截断为空文件,最后一个参数S_IRUSR | S_IWUSR
指定了新创建文件的权限为用户可读可写。 - 如果
open
函数调用失败,会打印错误消息并退出程序。 - 使用
write
函数向文件中写入内容。 - 最后使用
close
函数关闭文件。
1.3、write写文件
write
函数用于将数据写入文件。其函数原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
函数的参数和返回值含义如下:
fd
:文件描述符,代表要写入数据的文件。需要将要写入数据的文件对应的文件描述符传递给write
函数。buf
:指定要写入数据的缓冲区。count
:指定要写入的字节数。- 返回值:成功时返回写入的字节数(0 表示未写入任何字节)。如果返回值小于
count
参数,这不一定是错误,例如磁盘空间已满可能导致未写入所有字节。如果写入出错,则返回 -1。
对于普通文件,无论是读取还是写入,一个关键问题是确定从文件的哪个位置开始进行操作。即所谓的I/O操作位置偏移量。读写操作都从文件的当前位置偏移量开始。默认情况下,当前位置偏移量通常是0,即指向文件的起始位置。随着read、write函数的调用,当前位置偏移量也会相应移动。例如,如果当前位置偏移量为1000字节,调用write()写入或read()读取500字节后,当前位置偏移量将移动到1500字节处。
使用 write 函数需要先包含 unistd.h 头文件。
下面是一个示例代码,将字符串写入文件:
代码语言:javascript复制#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_WRONLY | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
const char *message = "Hello, world!";
ssize_t bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
close(fd);
printf("Data written successfully.n");
return 0;
}
在此示例中,我们首先打开了一个文件 example.txt
以供写入,然后使用 write
函数将字符串 "Hello, world!"
写入文件中。
1.4、read读文件
调用 read 函数可从打开的文件中读取数据,其函数原型如下所示:
代码语言:javascript复制#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
函数参数和返回值含义如下:
fd
:文件描述符,用于标识要读取的文件。buf
:用于存储读取数据的缓冲区。count
:需要读取的字节数。- 返回值:如果读取成功,返回读取到的字节数。实际读取到的字节数可能小于请求的字节数,也可能为0,例如当文件已到达末尾时。
使用 read
函数需要先包含 unistd.h
头文件。
例如,下面是一个简单的示例,从文件中读取数据:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUF_SIZE 1024
int main() {
int fd;
ssize_t bytes_read;
char buffer[BUF_SIZE];
// 打开文件
fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 读取文件内容
bytes_read = read(fd, buffer, BUF_SIZE);
if (bytes_read == -1) {
perror("read");
exit(EXIT_FAILURE);
}
// 输出读取的内容
write(STDOUT_FILENO, buffer, bytes_read);
// 关闭文件
close(fd);
return 0;
}
这个示例打开一个名为example.txt
的文件,从中读取数据并将其写入标准输出。
1.5、close关闭文件
close
函数用于关闭一个已经打开的文件描述符,释放对应的资源。在Linux系统中,文件描述符是有限资源,因此在不再需要使用文件时,应该及时关闭,以释放资源并避免资源泄漏。
函数原型如下所示:
代码语言:javascript复制#include <unistd.h>
int close(int fd);
函数参数和返回值含义如下:
- fd:文件描述符,需要关闭的文件所对应的文件描述符。
- 返回值:如果成功返回 0,如果失败则返回-1。
使用 close
函数需要先包含 <unistd.h>
头文件。
以下是一个简单的示例,演示如何使用 close
函数关闭文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// 打开一个文件,获取文件描述符
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 关闭文件
if (close(fd) == -1) {
perror("close");
exit(EXIT_FAILURE);
}
printf("File closed successfully.n");
return 0;
}
在这个例子中,首先通过 open
函数打开了一个文件,然后使用 close
函数关闭了文件描述符。perror
函数用于打印出发生错误的详细信息。
除了使用 close
函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件。这意味着如果一个程序在退出时没有关闭已打开的文件,内核会代为关闭这些文件。许多程序都依赖于这一特性,因此没有显式地使用 close
函数来关闭文件。
然而,显式关闭不再需要的文件描述符通常是良好的编程习惯。这样做可以提高代码的可读性和可靠性,并确保在后续修改时代码的行为符合预期。此外,释放不再需要的文件描述符可以有效地管理有限的系统资源。
2、标准I/O库
标准I/O库是C语言中用于进行输入和输出操作的标准库之一。它提供了一组函数和数据结构,用于与文件、终端设备、管道等进行交互,使得程序可以方便地进行输入和输出操作,而无需直接操作文件描述符。
标准I/O库函数构建在文件I/O系统调用(如 open()
、read()
、write()
、lseek()
、close()
等)之上。例如,fopen()
利用 open()
系统调用打开文件,fread()
利用 read()
系统调用读取文件,fwrite()
利用 write()
系统调用写入文件等。
尽管标准I/O和文件I/O都是C语言函数,但它们有明显区别:
- 标准 I/O 是标准 C 库函数,而文件 I/O 是 Linux 系统调用;
- 标准 I/O 是文件 I/O 的封装,实际上调用文件 I/O 完成操作;
- 可移植性方面,标准 I/O 更优,因为不同操作系统的系统调用接口不同,而标准 I/O 接口几乎相同;
- 在性能和效率方面,标准 I/O 由于维护自己的缓冲区,性能更高,而文件 I/O 在用户空间无缓存。
标准I/O库通常包含在C标准库中,其函数和数据结构被定义在<stdio.h>
头文件中。一些常用的标准I/O函数包括fopen
、fclose
、fread
、fwrite
、fprintf
、fscanf
等。所以使用时候需要在程序源码中包含<stdio.h>头文件。
标准I/O库的主要特点包括:
- 缓冲机制:标准I/O库通常使用缓冲区来提高性能。例如,在输出时,数据首先写入到缓冲区,然后在适当的时机才会被刷新到实际的输出设备上,从而减少了系统调用的次数,提高了效率。
- 格式化输入输出:标准I/O库提供了格式化输入输出的功能,例如
printf
和scanf
函数允许以特定格式输出和输入数据,使得数据的处理更加方便。 - 文件操作:标准I/O库提供了一系列函数用于文件的打开、关闭、读取、写入等操作,例如
fopen
、fclose
、fread
、fwrite
等。 - 错误处理:标准I/O库提供了一套错误处理机制,允许程序员检测和处理输入输出操作中可能出现的错误情况。
使用标准I/O库可以使得程序更加可移植,因为它们提供了对底层系统调用的封装,使得程序不依赖于特定的操作系统或文件系统。因此,标准I/O库是C语言中进行文件操作和输入输出的主要方式之一。
2.1、FILE指针
标准I/O库函数操作围绕FILE
指针展开。调用标准I/O库函数打开或创建文件时,返回一个指向FILE
类型对象的指针(FILE *
),该指针与被打开或创建的文件相关联,用于后续的标准I/O操作。因此,FILE
指针在标准I/O库中扮演了与文件描述符类似的角色,但用于更高级别的操作。
FILE
结构体包含了标准I/O库函数所需的所有文件管理信息,如文件描述符、文件缓冲区指针、缓冲区长度、当前缓冲区字节数以及出错标志等。
当使用标准I/O库函数打开或创建文件时,会返回一个指向FILE
类型对象的指针,该指针与被打开或创建的文件相关联。下面是一个简单的示例:
#include <stdio.h>
int main() {
FILE *file_ptr;
// 使用标准I/O库函数打开文件
file_ptr = fopen("example.txt", "w");
if (file_ptr == NULL) {
printf("Failed to open file.n");
return 1;
}
// 使用 FILE 指针进行写入操作
fprintf(file_ptr, "Hello, world!n");
// 关闭文件
fclose(file_ptr);
return 0;
}
在这个示例中,file_ptr
指针与文件相关联,用于后续的标准I/O操作(写入操作)。通过FILE
指针,我们可以方便地进行文件的读写操作,而不必直接操作文件描述符和底层的文件系统。
2.2、fopen打开文件
fopen()
是C语言标准库中用于打开文件的函数之一。它的原型如下:
FILE *fopen(const char *filename, const char *mode);
函数参数和返回值含义如下:
- path:参数
path
是一个指向文件路径的指针,可以是文件的绝对路径或相对路径。这个路径指定了要打开或创建的文件的位置和名称。 - mode:参数
mode
是一个字符串,指定了对文件的读写权限。它描述了打开或创建文件时所需的操作类型。常见的模式包括:"r"
:只读模式,用于打开一个已存在的文本文件,文件必须存在。"w"
:写入模式,用于创建一个新的空文本文件,如果文件已存在,则删除其内容。"a"
:追加模式,用于打开一个文本文件以便写入,如果文件不存在,则创建文件,文件指针被放在文件的末尾。"r "
:读写模式,用于打开一个文本文件用于读取和写入,文件必须存在。"w "
:读写模式,用于创建一个新的空文本文件用于读取和写入,如果文件已存在,则删除其内容。"a "
:读写模式,用于打开一个文本文件用于读取和写入,如果文件不存在,则创建文件,文件指针被放在文件的末尾。
- 返回值:函数调用成功时,返回一个指向
FILE
类型对象的指针(FILE *
),该指针与打开或创建的文件相关联。后续的标准 I/O 操作将围绕这个FILE
指针进行。如果函数调用失败,则返回NULL
,并设置errno
以指示错误原因。
以下是一个简单的示例,演示了如何使用 fopen()
打开文件:
#include <stdio.h>
int main() {
FILE *file_ptr;
// 以只读模式打开文件
file_ptr = fopen("example.txt", "r");
if (file_ptr == NULL) {
printf("Failed to open file.n");
return 1;
}
// 在这里可以进行文件读取操作
// 关闭文件
fclose(file_ptr);
return 0;
}
在这个示例中,我们尝试以只读模式打开名为 example.txt
的文件。如果文件打开失败,则会打印一条消息并退出程序。否则,我们可以在之后的代码中对文件进行读取操作。最后,我们使用 fclose()
函数关闭文件,释放资源。
2.3、fwrite写文件
fwrite()
是C语言标准库中用于向文件写入数据的函数之一。它的原型如下:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
函数参数和返回值含义如下:
- ptr:参数
ptr
是一个指向缓冲区的指针,该缓冲区中存储了要写入到文件中的数据。函数将会把这个缓冲区中的数据写入到文件中。 - size:参数
size
指定了每个数据项的字节大小,即每次写入的数据的大小。 - nmemb:参数
nmemb
指定了写入的数据项的个数,即要写入到文件中的数据项的数量。 - stream:参数
stream
是一个指向FILE
结构的指针,它标识了要写入数据的文件。 - 返回值:调用成功时,
fwrite()
函数返回实际成功写入到文件中的数据项的数目。如果发生错误,则返回值可能小于参数nmemb
(或者等于 0)。
fwrite()
函数返回成功写入的数据项数目,如果返回值与 nmemb
不同,则表示写入出现了错误。
以下是一个简单的示例,演示了如何使用 fwrite()
向文件写入数据:
#include <stdio.h>
int main() {
FILE *file_ptr;
char buffer[] = "Hello, world!";
// 打开文件以便写入
file_ptr = fopen("example.txt", "w");
if (file_ptr == NULL) {
printf("Failed to open file.n");
return 1;
}
// 向文件写入数据
size_t num_written = fwrite(buffer, sizeof(char), sizeof(buffer), file_ptr);
if (num_written != sizeof(buffer)) {
printf("Failed to write to file.n");
fclose(file_ptr);
return 1;
}
// 关闭文件
fclose(file_ptr);
return 0;
}
在这个示例中,我们向文件 "example.txt" 写入了字符串 "Hello, world!"。首先我们打开文件以便写入,然后使用 fwrite()
函数将数据写入文件,最后关闭文件。
2.4、fread读文件
fread()
是C语言标准库中用于从文件读取数据的函数之一。它的原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数参数和返回值含义如下:
- ptr:
fread()
函数将读取到的数据存放在参数ptr
指向的缓冲区中。这个缓冲区是用来存储从文件中读取的数据的。 - size:
fread()
函数从文件读取nmemb
个数据项,每个数据项的大小为size
个字节。因此,总共读取的数据大小为nmemb * size
个字节。 - nmemb:参数
nmemb
指定了要读取的数据项的个数。 - stream:参数
stream
是一个指向FILE
结构的指针,它标识了要从中读取数据的文件。 - 返回值:调用成功时,
fread()
函数返回成功读取到的数据项的数目。如果发生错误或到达文件末尾,则返回值可能小于参数nmemb
。由于fread()
无法区分文件结尾和错误,返回值小于nmemb
时,可以使用ferror()
或feof()
函数来进一步判断是发生了错误还是已经到达了文件末尾。
fread()
函数返回成功读取的数据项数目,如果返回值与 nmemb
不同,则表示读取出现了错误。
以下是一个简单的示例,演示了如何使用 fread()
从文件中读取数据:
#include <stdio.h>
int main() {
FILE *file_ptr;
char buffer[100]; // 缓冲区用于存储读取的数据
// 打开文件以便读取
file_ptr = fopen("example.txt", "r");
if (file_ptr == NULL) {
printf("Failed to open file.n");
return 1;
}
// 从文件读取数据
size_t num_read = fread(buffer, sizeof(char), sizeof(buffer), file_ptr);
if (num_read == 0) {
printf("Failed to read from file.n");
fclose(file_ptr);
return 1;
}
// 输出读取的数据
printf("Read from file: %sn", buffer);
// 关闭文件
fclose(file_ptr);
return 0;
}
在这个示例中,我们打开了一个名为 "example.txt" 的文件以便读取。我们使用 fread()
函数从文件中读取数据,并将其存储在名为 buffer
的缓冲区中。最后,我们打印出读取到的数据,并关闭文件。
2.5、fclose关闭文件
fclose()
是C语言标准库中用于关闭文件的函数之一。它的原型如下:
int fclose(FILE *stream);
函数参数和返回值含义如下:
stream
:指向FILE
结构的指针,标识要关闭的文件。- 返回值:调用成功返回 0;失败将返回 EOF(也就是-1)。
以下是一个示例,演示了如何使用 fclose()
关闭文件:
#include <stdio.h>
int main() {
FILE *file_ptr;
// 打开文件以便读取
file_ptr = fopen("example.txt", "r");
if (file_ptr == NULL) {
printf("Failed to open file.n");
return 1;
}
// 在这里可以进行文件读取操作...
// 关闭文件
if (fclose(file_ptr) != 0) {
printf("Failed to close file.n");
return 1;
}
return 0;
}
在这个示例中,我们打开了一个名为 "example.txt" 的文件以便读取。在文件读取操作完成后,我们使用 fclose()
函数关闭了文件。如果关闭文件失败,则会打印一条错误消息并退出程序。