狭义上的“文件”主要指存储在磁盘上的数据集合。具体包括:
fopen
或fwrite
函数最终会转化为对外设的输入输出请求。磁盘作为计算机外设的一种,既是输入设备(读取数据)也是输出设备(写入数据)。例如:
所有对磁盘文件的操作(创建、读取、修改、删除)本质上都是通过操作系统提供的输入输出(IO)接口完成的。
总结:狭义文件强调磁盘存储的永久性和硬件交互的IO本质,所有操作都围绕外设进行。
在Linux/Unix操作系统中,有一个重要的设计理念:"一切皆文件"。这意味着:
• 硬件设备抽象为文件:
• 特殊文件类型:
例如,在Linux中:
这种抽象极大简化了系统编程接口,开发者可以使用统一的文件操作API来访问各种资源。
即使是0KB的空文件也会占用磁盘空间,原因包括:
• 文件元数据存储:每个文件都需要存储文件名、创建时间、权限等属性信息
• 文件系统开销:大多数文件系统有最小分配单元(如4KB的块大小)
• 目录条目:文件名需要在父目录中建立对应的条目
文件由两个主要部分组成:
文件属性(元数据):
文件内容:
例如,使用ls -l
命令可以查看文件的元数据,使用cat
命令可以查看文件内容。
所有文件操作都可以归类为:
• 内容操作:
• 属性操作:
在操作系统中:
• 文件操作的主体是进程
• 每个进程维护一个文件描述符表
• 通过文件描述符来引用打开的文件
例如:
对文件的操作本质是进程对文件的操作:文件操作由运行中的进程发起,进程通过系统调用请求操作系统执行文件任务(如打开、读写)。磁盘作为硬件资源,由操作系统统一管理,进程不能直接访问磁盘。
操作系统作为磁盘的管理者,负责:
• 文件系统的实现(如ext4, NTFS)
• 磁盘空间分配与管理
• 文件缓存与IO调度
• 权限控制与安全机制
磁盘的管理者是操作系统:操作系统(如Linux内核)负责磁盘的底层管理,包括文件系统组织、数据存储和安全控制。用户程序不能绕过操作系统直接操作磁盘。
文件操作的实际执行路径:
例如,C语言的fopen函数底层会调用open系统调用,fread会调用read系统调用。这些系统调用才是真正与操作系统内核交互的接口。
文件读写通过系统调用接口实现:C语言或C++的库函数(如fopen
、fprintf
)只是用户层封装,提供便利性;底层实现依赖于文件相关的系统调用接口(如open
打开文件、read
读取数据、write
写入数据)。这些系统调用涉及模式切换(用户态到内核态),确保操作安全和高效。所有语言(包括C)的文件操作最终都调用系统调用,因为硬件访问必须通过操作系统。
fopen()
- 打开文件FILE*
)。filename
:文件路径(绝对或相对路径)。mode
:打开模式(如 "r"
只读、"w"
只写、"a"
追加等)。NULL
。fclose()
- 文件关闭FILE*
文件指针。0
,失败返回 EOF
(-1)。#include
int main() {
// 参数1: 文件名,参数2: 模式("r"读/"w"写/"a"追加)
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) { // 检查是否成功打开
printf("文件打开失败\n");
return 1;
}
printf("文件打开成功\n");
fclose(fp); // 关闭文件
return 0;
}
fgetc()
/ getc()
- 读取单个字符FILE* stream
。int
),文件结束或失败返回 EOF
。示例代码:
FILE *fp = fopen("file.txt", "r");
if (fp) {
char c = fgetc(fp); // 从文件读取一个字符
while (c != EOF) { // EOF 表示文件结束
printf("%c", c);
c = fgetc(fp); // 继续读取下一个字符
}
fclose(fp);
}
fgetc
和 getc
功能相同,但 getc
可能被实现为宏。int
类型(可容纳 EOF
)。fputc()
/ putc()
- 写入单个字符char c
:待写入字符。FILE* stream
:文件指针。EOF
。FILE *fp = fopen("output.txt", "w");
if (fp) {
fputc('A', fp); // 写入字符 'A'
fclose(fp);
}
fgets()
- 读取一行字符串char* str
:存储读取数据的缓冲区。int n
:最大读取长度(含结尾 \0
)。FILE* stream
。str
,失败或文件结束返回 NULL
。FILE *fp = fopen("data.txt", "r");
char buf[1024];
if (fp) {
// 读取一行(最多 sizeof(buf)-1 个字符)
while (fgets(buf, sizeof(buf), fp) != NULL) {
printf("%s", buf);
}
fclose(fp);
}
\0
。feof()
或 ferror()
检查结束/错误。fputs()
- 写入字符串const char* str
:字符串指针。FILE* stream
。FILE *fp = fopen("log.txt", "a");
if (fp) {
fputs("Hello, World!\n", fp); // 写入字符串(不自动加换行)
fclose(fp);
}
EOF
。fprintf()
- 格式化写入printf()
类似,增加文件指针参数。FILE *fp = fopen("report.txt", "w");
if (fp) {
int num = 100;
fprintf(fp, "数值: %d\n", num); // 类似 printf,但输出到文件
fclose(fp);
}
fscanf()
- 格式化读取scanf()
类似,增加文件指针参数。FILE *fp = fopen("data.txt", "r");
int a;
char str[20];
if (fp) {
fscanf(fp, "%d %s", &a, str); // 从文件读取整数和字符串
fclose(fp);
}
fread()
- 读取数据块const void* ptr
:存储目标地址。size_t size
:每个数据块大小(字节)。size_t nmemb
:数据块数量。FILE* stream
。struct Item
{
char name[20];
int size;
};
struct Item items[3];
FILE *fp = fopen("/tmp/data.bin", "rb");
if (fp) {
// 参数:缓冲区, 每个元素大小, 元素数量, 文件指针
size_t count = fread(items, sizeof(struct Item), 3, fp);
if (count != 3) { // 检查实际读取数量
printf("读取不完整\n");
}
fclose(fp);
}
"b"
)。fwrite()
- 写入数据块const void* ptr
:数据源地址。size_t size
:每个数据块大小(字节)。size_t nmemb
:数据块数量。FILE* stream
。struct Item items[3] = {{"Linux", 5}, {"C", 1}};
FILE *fp = fopen("/tmp/data.bin", "wb");
if (fp) {
fwrite(items, sizeof(struct Item), 2, fp); // 写入2个结构体
fclose(fp);
}
fseek()
- 移动文件指针FILE* stream
。long offset
:偏移量(字节)。int whence
:起始位置(SEEK_SET
文件头、SEEK_CUR
当前位置、SEEK_END
文件尾)。0
,失败返回非零值FILE *fp = fopen("large.bin", "rb");
if (fp) {
fseek(fp, 100L, SEEK_SET); // 从文件头偏移100字节
long pos = ftell(fp); // 获取当前位置
printf("当前位置: %ld\n", pos);
fclose(fp);
}
rewind()
- 重置到文件头rewind(fp); // 等价于 fseek(fp, 0L, SEEK_SET)
feof()
/ ferror()
- 检查状态ferror()
:检查文件操作是否出错(非零值表示错误)。feof()
:检查是否到达文件末尾(非零值表示结束)。FILE *fp = fopen("file.txt", "r");
if (fp) {
char buf[100];
fgets(buf, sizeof(buf), fp);
if (feof(fp)) {
printf("已到文件末尾\n");
}
if (ferror(fp)) {
printf("读取错误\n");
}
fclose(fp);
}
feof()
在读到末尾后返回真,ferror()
检查错误标志。C语言通过标准I/O库提供多种输出方式,均基于stdout
(标准输出流)实现:
printf()
:最常用的格式化输出函数,自动追加换行符。
printf("Hello World\n"); // 输出字符串并换行
fprintf()
:指定输出流(如stdout
)的格式化输出。
fprintf(stdout, "Value: %d\n", 42); // 等同于printf
puts()
:输出字符串并自动换行。
puts("Hello Linux"); // 输出后自动换行
fputs()
:输出字符串但不自动换行。
fputs("No newline", stdout); // 需手动添加\n
putchar()
/fputc()
:单字符输出。
putchar('A'); // 输出字符'A'
fputc('B', stdout); // 等同putchar
fwrite()
:二进制数据块输出(也可用于文本)。
const char *msg = "Binary write\n";
fwrite(msg, strlen(msg), 1, stdout); // 直接写入字节流
关键点:所有方法均通过
stdout
(文件指针)指向显示器。
在C语言中,stdin
、stdout
和stderr
是三个预定义的标准I/O流,它们在程序启动时由系统自动打开。这些流是C标准库的核心组成部分,提供了一种标准化的输入/输出处理方式。
流名称 | 文件描述符 | 默认设备 | 用途 | 缓冲类型 |
---|---|---|---|---|
stdin |
0 | 键盘 | 标准输入 | 行缓冲(终端) |
stdout |
1 | 显示器 | 标准输出 | 行缓冲(终端) |
stderr |
2 | 显示器 | 标准错误输出 | 无缓冲 |
关键特性:
- 类型均为
FILE*
(文件指针)- 定义在
中
- 生命周期与程序相同(自动打开/关闭)
// stdio.h 中的声明
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
#include
// 验证文件描述符
printf("stdin fd: %d\n", fileno(stdin)); // 输出 0
printf("stdout fd: %d\n", fileno(stdout)); // 输出 1
printf("stderr fd: %d\n", fileno(stderr)); // 输出 2
流 | 缓冲行为 | 典型场景 |
---|---|---|
stdout |
行缓冲(遇到\n 或满缓冲才输出) |
printf("Hello\n") |
stderr |
无缓冲(立即输出) | perror("Error") |
stdin |
行缓冲(等待回车键) | scanf("%s", buf) |
缓冲验证代码:
#include
#include
int main() {
// stdout 有缓冲(可能不会立即显示)
fprintf(stdout, "This is stdout");
sleep(2); // 等待2秒
// stderr 无缓冲(立即显示)
fprintf(stderr, "\nThis is stderr");
return 0;
}
运行结果:
先等待2秒,然后同时显示:
This is stderr
This is stdout
#include
int main() {
char buf[100];
// 从stdin读取(可以是键盘或重定向文件)
while (fgets(buf, sizeof(buf), stdin) != NULL) {
fprintf(stdout, "Read: %s", buf);
}
return 0;
}
使用方式:
# 键盘输入
$ ./program
Hello
Read: Hello
# 文件重定向
$ ./program < input.txt
#include
int main() {
// 正常输出到stdout(可被重定向)
fprintf(stdout, "Program started\n");
// 错误输出到stderr(始终显示在终端)
fprintf(stderr, "[ERROR] Invalid operation\n");
return 0;
}
重定向效果:
$ ./program > output.txt # stdout重定向到文件
[ERROR] Invalid operation # stderr仍在终端显示
$ cat output.txt
Program started
#include
#include
int main() {
FILE *fp = fopen("nonexist.txt", "r");
if (fp == NULL) {
// 输出到stderr(无缓冲确保及时显示)
perror("fopen failed");
fprintf(stderr, "Error code: %d\n", errno);
}
return 0;
}
输出:
fopen failed: No such file or directory
Error code: 2
#include
int main() {
// 临时重定向stdout到文件
FILE *log = fopen("log.txt", "w");
stdout = log; // 重定向
printf("This goes to log.txt"); // 写入文件
fclose(log);
stdout = fdopen(1, "w"); // 恢复默认
return 0;
}
#include
int main() {
int value;
// 提示用户输入(stderr确保即时显示)
fprintf(stderr, "Enter a number: ");
// 从stdin读取
scanf("%d", &value);
// 结果输出到stdout
printf("Square: %d\n", value * value);
return 0;
}
不要手动关闭
这三个流会在程序结束时自动关闭,显式关闭可能导致未定义行为:
fclose(stdin); // ❌ 危险操作!
缓冲同步
在混合使用stdout
和stderr
时,使用fflush
强制同步:
printf("Processing...");
fflush(stdout); // 确保先显示
fprintf(stderr, "[WARN] Low memory");
重定向安全
需要即时显示的提示信息应使用stderr
:
// 正确方式(重定向时仍显示提示)
fprintf(stderr, "Enter password: ");
二进制模式
在Windows系统中,使用_setmode
切换二进制模式以避免换行符转换:
#include
#include
_setmode(_fileno(stdout), _O_BINARY); // Windows专用
r Open text file for reading.
The stream is positioned at the beginning of the file.
r+ Open for reading and writing.
The stream is positioned at the beginning of the file.
w Truncate(缩短) file to zero length or create text file for writing.
The stream is positioned at the beginning of the file.
w+ Open for reading and writing.
The file is created if it does not exist, otherwise it is truncated.
The stream is positioned at the beginning of the file.
a Open for appending (writing at end of file).
The file is created if it does not exist.
The stream is positioned at the end of the file.
a+ Open for reading and appending (writing at end of file).
The file is created if it does not exist. The initial file position
for reading is at the beginning of the file,
but output is always appended to the end of the file.
1. 基本模式
模式 | 描述 | 文件存在 | 文件不存在 | 缓冲区行为 |
---|---|---|---|---|
"r" |
只读(文本文件) | 打开文件 | 返回 NULL |
输入缓冲区 |
"w" |
只写(文本文件) | 清空内容 | 创建新文件 | 输出缓冲区 |
"a" |
追加(文本文件) | 保留内容,追加写入 | 创建新文件 | 输出缓冲区 |
"rb" |
只读(二进制文件) | 打开文件 | 返回 NULL |
输入缓冲区 |
"wb" |
只写(二进制文件) | 清空内容 | 创建新文件 | 输出缓冲区 |
"ab" |
追加(二进制文件) | 保留内容,追加写入 | 创建新文件 | 输出缓冲区 |
关键说明:
"r"
和"rb"
要求文件必须存在,否则失败 。"w"
/"wb"
会无条件清空文件(易错点!)。"a"
/"ab"
的写入位置始终在文件末尾 。
2. 扩展读写模式(+
号组合)
模式 | 描述 | 读操作位置 | 写操作位置 |
---|---|---|---|
"r+" |
读写(文本文件) | 文件开头 | 当前指针位置 |
"w+" |
读写(文本文件) | 文件开头 | 先清空内容 |
"a+" |
读写(文本文件) | 文件开头 | 始终在文件末尾 |
"rb+" |
读写(二进制文件) | 文件开头 | 当前指针位置 |
"wb+" |
读写(二进制文件) | 文件开头 | 先清空内容 |
"ab+" |
读写(二进制文件) | 文件开头 | 始终在文件末尾 |
核心特性:
"w+"
和"wb+"
会先清空文件再打开 。"a+"
/"ab+"
的写操作不受fseek()
影响,永远追加到末尾 。- 读写切换时需用
fseek()
或rewind()
重定位指针 。
换行符差异:
b
)在 Windows 中自动转换 \r\n
↔ \n
,Linux/macOS 无此转换 。b
)在所有平台直接读写原始字节 。权限问题:
"w"
和 "a"
模式需文件可写权限,否则返回 NULL
。chmod()
提前设置权限。总结
核心口诀:
r
读,w
写(清空!),a
追加,b
二进制,+
读写。
最佳实践:
"w"
导致数据清空)。b
(避免换行符问题)。fseek()
同步指针位置 。