21、标准流与重定向:stdin/stdout/stderr、freopen重定向、管道与文件描述符概念
说到标准流,这其实是每个C程序员每天都在打交道的东西。你可能没意识到,你写的每个printf、scanf、fprintf,背后都在操作这三个默认打开的流。我个人习惯把它们叫做「三剑客」——stdin、stdout、stderr。
21.1 标准流到底是什么?
程序启动时,操作系统会自动打开三个文件流。说白了,就是三个默认的通信管道:
- stdin(标准输入)——默认从键盘读取数据
- stdout(标准输出)——默认往屏幕写数据
- stderr(标准错误)——默认也往屏幕写,但独立于stdout
你想想看,为什么printf能直接往屏幕打印?因为它内部用的就是stdout。为什么scanf能从键盘读?因为它读的是stdin。这三个流在stdio.h里被声明为FILE*类型的全局变量。
核心要点:stdin、stdout、stderr是程序启动时自动打开的,不需要你手动fopen。它们对应的文件描述符分别是0、1、2。
21.2 文件描述符——内核视角的「编号」
嗯,这里要注意。文件描述符是操作系统层面的概念,不是C语言标准库独有的。每个进程都有一个文件描述符表,说白了就是一个整数数组。0、1、2这三个位置被预留给标准流了。
// 文件描述符与标准流的对应关系
// 0 → stdin → 标准输入
// 1 → stdout → 标准输出
// 2 → stderr → 标准错误
// 你可以直接用文件描述符来读写
#include <unistd.h>
char buffer[100];
read(0, buffer, sizeof(buffer)); // 从stdin读,等价于fgets
write(1, buffer, strlen(buffer)); // 写到stdout,等价于printf
write(2, "error\n", 6); // 写到stderr
我在项目中遇到过一个问题:有人用fprintf(stdout, ...)和fprintf(stderr, ...)混合输出,结果在重定向时发现错误信息和正常输出混在一起了。原因就是stdout和stderr虽然都指向屏幕,但它们是两个不同的流。
21.3 freopen重定向——把流「拐」到文件里
freopen这个函数很有意思。它可以把一个已经打开的流重新绑定到另一个文件。我经常用它来做日志重定向或者测试数据注入。
#include <stdio.h>
int main() {
// 把stdout重定向到文件
if (freopen("output.log", "w", stdout) == NULL) {
perror("freopen failed");
return 1;
}
// 现在printf不会打印到屏幕,而是写入output.log
printf("这条信息写入了文件,而不是屏幕\n");
printf("标准输出已经被重定向了\n");
// 恢复stdout到终端(需要保存原始fd)
// 注意:freopen后原来的流被关闭了
return 0;
}
我的经验:freopen有个坑——它会关闭原来的流。如果你之后想恢复,最好先dup保存原始文件描述符。我曾经在写一个命令行工具时,重定向后忘了恢复,结果后续所有输出都丢了。
再看一个stderr重定向的例子:
// 把错误信息单独记录到文件
freopen("error.log", "a", stderr);
fprintf(stderr, "这条错误写入了error.log\n");
// 标准输出仍然正常打印
printf("屏幕上看得到我\n");
21.4 管道——进程间的「数据水管」
管道这个概念,说白了就是让一个进程的输出变成另一个进程的输入。你在shell里用的|符号,背后就是管道机制。
// 在shell中:
// ls -l | grep ".c" | wc -l
// 这条命令创建了两个管道,连接了三个进程
// C语言中创建管道:
#include <unistd.h>
int main() {
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
return 1;
}
// pipefd[0] → 读端
// pipefd[1] → 写端
pid_t pid = fork();
if (pid == 0) {
// 子进程:关闭读端,重定向stdout到写端
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
execlp("ls", "ls", "-l", NULL);
} else {
// 父进程:关闭写端,从读端读取
close(pipefd[1]);
char buffer[1024];
read(pipefd[0], buffer, sizeof(buffer));
close(pipefd[0]);
printf("从子进程读到:\n%s", buffer);
}
return 0;
}
注意:管道是单向的。如果你想双向通信,需要创建两个管道。另外,管道有缓冲区大小限制(通常65536字节),写满时会阻塞。
21.5 重定向的几种常见场景
我整理了一张表,把日常开发中常见的重定向操作列出来:
| 操作 | Shell写法 | C语言实现 | 说明 |
|---|---|---|---|
| 输出重定向 | ./a.out > out.txt | freopen("out.txt","w",stdout) | 覆盖写入 |
| 追加输出 | ./a.out >> out.txt | freopen("out.txt","a",stdout) | 追加到文件末尾 |
| 输入重定向 | ./a.out < in.txt | freopen("in.txt","r",stdin) | 从文件读取输入 |
| 错误重定向 | ./a.out 2> err.txt | freopen("err.txt","w",stderr) | 单独记录错误信息 |
| 合并输出 | ./a.out > all.txt 2>&1 | dup2(fd, STDERR_FILENO) | stdout和stderr写入同一文件 |
21.6 知识体系图
下面这张图展示了标准流、文件描述符、重定向和管道之间的关系:
21.7 避坑指南
我曾经在一个线上服务中踩过一个坑:程序用freopen把stdout重定向到了日志文件,但后来fork了子进程。子进程继承了重定向后的stdout,结果父子进程同时写同一个日志文件,导致日志内容交错混乱。
解决方案其实很简单:fork之后,子进程应该先关闭继承来的文件描述符,再重新打开。或者用dup2配合O_CLOEXEC标志,让子进程执行exec时自动关闭。
几个实用建议:
- 调试时多用stderr输出错误信息,因为它不受stdout重定向影响
- 重定向后记得检查返回值,freopen失败会返回NULL
- 管道通信时,记得关闭不需要的端,否则可能造成死锁
- 文件描述符是有限资源,用完要close
嗯,标准流和重定向这块内容,说白了就是理解「数据从哪里来,到哪里去」。你掌握了freopen和管道,就能灵活控制程序的输入输出方向。这在写命令行工具、日志系统、进程间通信时特别有用。
公众号:蓝海资料掘金营,微信deep3321