C/C++读写操作的过程和性能
- 在C/C++中,有多种处理读写的函数,主要分为低级别I/O函数和高级别I/O函数。每种函数都有其特定的优缺点和适用场景。
读写函数
低级别I/O函数
read()
和 write()
- 头文件:
<unistd.h>
- 级别:低级别I/O(系统调用)
- 优点:
- 直接操作文件描述符,效率高。
- 适用于处理二进制文件和性能要求较高的场景。
- 提供更精细的控制(如非阻塞I/O、异步I/O)。
- 缺点:
- 不提供缓冲机制,需要手动管理缓冲区。
- 接口相对复杂,不适合处理文本文件的高级操作。
示例:
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("example.txt", O_RDONLY);
char buffer[128];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
close(fd);
return 0;
}
高级别I/O函数(C标准库)
fgets()
和 fputs()
- 头文件:
<stdio.h>
- 级别:高级别I/O(标准库函数)
- 优点:
- 使用缓冲机制,操作更方便。
- 适用于文本文件的行级操作。
- 缺点:
- 性能相对低于低级别I/O函数,不适合高性能需求的场景。
示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
char buffer[128];
fgets(buffer, sizeof(buffer), file);
fclose(file);
return 0;
}
fread()
和 fwrite()
- 头文件:
<stdio.h>
- 级别:高级别I/O(标准库函数)
- 优点:
- 使用缓冲机制,操作方便。
- 适用于处理二进制文件和大块数据读写。
- 缺点:
- 性能相对低于低级别I/O函数,不适合高性能需求的场景。
示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.bin", "rb");
char buffer[128];
fread(buffer, sizeof(char), sizeof(buffer), file);
fclose(file);
return 0;
}
fprintf()
和 fscanf()
- 头文件:
<stdio.h>
- 级别:高级别I/O(标准库函数)
- 优点:
- 使用缓冲机制,操作方便。
- 适用于格式化的文本文件读写。
- 缺点:
- 性能相对低于低级别I/O函数,不适合高性能需求的场景。
示例:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
fprintf(file, "Hello, World!\n");
fclose(file);
return 0;
}
高级别I/O函数(C++标准库)
ifstream
和 ofstream
- 头文件:
<fstream>
- 级别:高级别I/O(C++标准库)
- 优点:
- 提供面向对象的接口,使用方便。
- 使用缓冲机制,适合处理文本文件和二进制文件。
- 继承了C++的类型安全和异常处理机制。
- 缺点:
- 性能相对低于低级别I/O函数,不适合高性能需求的场景。
示例:
#include <fstream>
int main() {
std::ifstream file("example.txt");
std::string line;
std::getline(file, line);
file.close();
return 0;
}
iostream
- 头文件:
<iostream>
- 级别:高级别I/O(C++标准库)
- 优点:
- 提供面向对象的接口,使用方便。
- 使用缓冲机制,适合处理文本文件。
- 继承了C++的类型安全和异常处理机制。
- 缺点:
- 性能相对低于低级别I/O函数,不适合高性能需求的场景。
示例:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
读写函数优缺点总结
函数 | 头文件 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
read() | <unistd.h> | 高效,直接系统调用,适合二进制文件和高性能需求 | 无缓冲机制,接口复杂 | 二进制文件,高性能I/O |
write() | <unistd.h> | 高效,直接系统调用,适合二进制文件和高性能需求 | 无缓冲机制,接口复杂 | 二进制文件,高性能I/O |
fgets() | <stdio.h> | 使用缓冲机制,适合文本文件行级操作 | 性能相对较低 | 文本文件,行级操作 |
fputs() | <stdio.h> | 使用缓冲机制,适合文本文件行级操作 | 性能相对较低 | 文本文件,行级操作 |
fread() | <stdio.h> | 使用缓冲机制,适合处理二进制文件和大块数据读写 | 性能相对较低 | 二进制文件,大块数据读写 |
fwrite() | <stdio.h> | 使用缓冲机制,适合处理二进制文件和大块数据读写 | 性能相对较低 | 二进制文件,大块数据读写 |
fprintf() | <stdio.h> | 使用缓冲机制,适合格式化文本文件读写 | 性能相对较低 | 格式化文本文件 |
fscanf() | <stdio.h> | 使用缓冲机制,适合格式化文本文件读写 | 性能相对较低 | 格式化文本文件 |
ifstream | <fstream> | 面向对象接口,使用方便,适合处理文本和二进制文件 | 性能相对较低 | 文本和二进制文件 |
ofstream | <fstream> | 面向对象接口,使用方便,适合处理文本和二进制文件 | 性能相对较低 | 文本和二进制文件 |
iostream | <iostream> | 面向对象接口,使用方便,适合处理文本文件 | 性能相对较低 | 文本文件 |
<stdio.h>
头文件里的BUFSIZ
宏定义了默认的缓冲区大小
fgets()
过程示例
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "r");
char buffer[128];
fgets(buffer, sizeof(buffer), file);
fclose(file);
return 0;
}
在使用 fgets()
函数读取文件时,文件的实际大小(例如100MB)与缓冲机制的工作原理密切相关。尽管 fgets()
只读取文件中的一行,但在幕后,标准I/O库会利用缓冲区来优化文件的读取性能。以下是详细说明 fgets()
使用缓冲机制的过程:
步骤及缓冲机制
-
打开文件:
fopen("example.txt", "r")
打开文件example.txt
并返回文件指针file
。FILE *file = fopen("example.txt", "r");
-
初始化缓冲区:标准I/O库为文件流分配一个缓冲区。默认缓冲区大小通常为 4KB 或 8KB,由具体的系统和库实现决定。这个缓冲区在内部维护,不需要程序员显式分配。
-
调用
fgets()
读取数据:- 调用
fgets(buffer, sizeof(buffer), file)
。 fgets()
首先检查文件流缓冲区中是否有足够的数据。如果缓冲区为空或数据不足,则进行一次实际的读操作,将数据从磁盘读取到缓冲区中。
char buffer[128]; fgets(buffer, sizeof(buffer), file);
- 调用
-
填充缓冲区:
- 假设缓冲区大小为 4KB,标准I/O库会尝试读取最多 4KB 的数据到缓冲区。
- 如果缓冲区数据不够或为空,标准I/O库会调用低级系统调用(如
read()
)从磁盘读取数据到缓冲区中。 - 读取的数据会存放在缓冲区中以便后续读取。
-
读取一行数据:
fgets()
从缓冲区中读取一行数据并存储到用户提供的buffer
中。fgets()
会读取最多sizeof(buffer) - 1
个字符,或直到遇到换行符\n
或文件结束符 EOF。- 此外,
fgets()
确保读取的字符串以空字符\0
结尾。
-
返回数据:
- 一旦读取到一行数据,
fgets()
将数据存储在buffer
中并返回指向该缓冲区的指针。
fgets(buffer, sizeof(buffer), file);
- 一旦读取到一行数据,
-
关闭文件:完成读操作后,调用
fclose(file)
关闭文件,释放文件流和缓冲区。fclose(file);
缓冲机制的优势
- 减少系统调用:缓冲区机制可以减少系统调用的次数。每次
fgets()
操作不会都直接导致系统调用,而是尽可能从缓冲区读取数据,只有当缓冲区数据不足时,才进行实际的磁盘读操作。 - 提高性能:通过批量读取数据到缓冲区,减少了频繁的磁盘I/O操作,提高了文件读取的效率。
- 方便使用:用户只需调用高级函数(如
fgets()
),不需要关心底层的缓冲区管理和系统调用。
示例解释
假设 example.txt
文件大小为 100MB:
- 第一次调用
fgets()
:文件指针位置在文件开头,缓冲区为空。 - 填充缓冲区:标准I/O库从文件读取 4KB 数据到缓冲区。
- 读取数据:
fgets()
从缓冲区读取一行数据,假设该行数据小于 128 字节。 - 后续调用
fgets()
:如果继续调用fgets()
,将从缓冲区读取剩余的数据,直到缓冲区数据不足,再次从文件读取下一批数据。
这种缓冲机制确保了即使文件非常大(如 100MB),每次调用 fgets()
时也只是处理较小的缓冲区内容,从而避免了直接处理大量数据的开销。
fputs()
过程示例
fputs()
函数用于将一个字符串写入到文件中,与 fgets()
类似,fputs()
也利用了标准I/O库的缓冲机制来优化文件写入操作。以下是 fputs()
使用缓冲机制的详细过程:
#include <stdio.h>
int main() {
FILE *file = fopen("example.txt", "w");
if (file) {
const char *text = "This is a test string to demonstrate fputs() buffering.";
fputs(text, file);
fclose(file);
}
return 0;
}
步骤及缓冲机制
-
打开文件:例如
fopen("example.txt", "w")
打开文件example.txt
并返回文件指针file
。FILE *file = fopen("example.txt", "w");
-
初始化缓冲区:标准I/O库为文件流分配一个缓冲区。默认缓冲区大小通常为 4KB 或 8KB,这取决于系统和库的实现。这个缓冲区在内部维护,程序员不需要显式分配。
-
调用
fputs()
写入数据:- 调用
fputs(buffer, file)
将buffer
中的字符串写入到文件流file
。 fputs()
会将字符串数据写入到文件流的缓冲区中,而不是直接写入磁盘。
fputs(buffer, file);
- 调用
-
填充缓冲区:
- 当
fputs()
被调用时,字符串数据首先被复制到文件流的缓冲区中。 - 如果缓冲区未满,数据会继续保留在缓冲区中,等待更多数据的写入。
- 当
-
写入缓冲区数据到磁盘:
- 当缓冲区被填满时,标准I/O库会将缓冲区中的数据一次性写入到磁盘,清空缓冲区以便后续写操作。
- 这种写入操作通常由低级系统调用(如 `write())完成。
- 即使缓冲区未满,如果程序调用
fflush()
或fclose()
,缓冲区中的数据也会被立即写入到磁盘。
-
刷新缓冲区:
- 如果调用
fflush(file)
,会强制将缓冲区中的数据写入到磁盘,而不论缓冲区是否已满。
fflush(file);
- 如果调用
-
关闭文件:完成写操作后,调用
fclose(file)
关闭文件,确保缓冲区中剩余的数据写入磁盘,并释放文件流和缓冲区。fclose(file);
缓冲机制的优势
- 减少系统调用:缓冲区机制减少了系统调用的次数。每次
fputs()
操作不会都直接导致系统调用,而是尽可能在缓冲区中积累数据,只有当缓冲区满时才进行一次实际的磁盘写操作。 - 提高性能:通过批量写入数据到磁盘,减少了频繁的磁盘I/O操作,提高了文件写入的效率。
- 方便使用:用户只需调用高级函数(如
fputs()
),不需要关心底层的缓冲区管理和系统调用。
fputs()
写入缓冲过程
- 第一次调用
fputs()
:文件指针在文件开头,缓冲区为空。 - 将数据写入缓冲区:
fputs()
将text
中的字符串复制到文件流的缓冲区中。 - 检查缓冲区:如果缓冲区未满,数据保留在缓冲区中,等待更多数据写入。
- 缓冲区满时:一旦缓冲区被填满,标准I/O库将缓冲区中的数据写入磁盘,并清空缓冲区。
- 关闭文件:调用
fclose(file)
时,缓冲区中的剩余数据(如果有)将被写入磁盘。
这种缓冲机制确保了即使数据量很大,fputs()
也能高效地将数据写入文件,而不会因为每次小量的数据写入导致频繁的磁盘I/O操作。