在C语言中,所有输入输出操作都建立在"流"这个概念之上。流本质上是一个字节序列的抽象接口,它屏蔽了底层设备的差异,为程序员提供了统一的I/O操作方式。
流的实现通常包含以下几个关键组件:
c复制#include <stdio.h>
int main() {
// 标准流的使用示例
printf("标准输出(stdout)演示\n");
fprintf(stderr, "标准错误(stderr)演示\n");
int ch;
printf("请输入一个字符:");
ch = getchar();
printf("你输入的字符是:%c\n", ch);
return 0;
}
注意:stdout和stderr虽然默认都输出到终端,但它们是独立的流,可以分别重定向到不同位置。
缓冲区是C语言I/O性能优化的关键机制,理解其工作原理能避免很多奇怪的问题:
全缓冲:
行缓冲:
无缓冲:
c复制#include <stdio.h>
#include <unistd.h>
void demonstrate_buffering() {
// 行缓冲演示
printf("这段文字不会立即显示");
sleep(2);
printf("直到遇到换行符\n");
// 强制刷新缓冲区
printf("这段文字会立即显示");
fflush(stdout);
sleep(2);
printf("因为调用了fflush\n");
}
标准流的强大之处在于可以灵活重定向:
bash复制# 将stdout重定向到文件
./program > output.txt 2> error.log
# 将stderr合并到stdout
./program > combined.log 2>&1
在程序中也可以使用freopen重定向流:
c复制#include <stdio.h>
void redirect_streams() {
// 将stdout重定向到文件
freopen("output.txt", "w", stdout);
printf("这行文字会写入文件\n");
// 恢复标准输出
freopen("/dev/tty", "w", stdout); // Linux/macOS
// freopen("CON", "w", stdout); // Windows
printf("这行文字会显示在终端\n");
}
printf的格式说明符完整语法:
%[flags][width][.precision][length]type
常用flags:
-:左对齐+:显示正负号0:用零填充 :正数前留空格#:替代形式(如十六进制加0x)宽度与精度:
c复制#include <stdio.h>
void advanced_printf() {
int num = 42;
double pi = 3.1415926535;
// 基本格式化
printf("十进制: %d, 十六进制: %x, 八进制: %o\n", num, num, num);
// 宽度与对齐
printf("右对齐: [%10d], 左对齐: [%-10d]\n", num, num);
// 浮点数精度
printf("默认: %f, 2位小数: %.2f, 科学计数法: %e\n", pi, pi, pi);
// 动态宽度
printf("动态宽度: %*d\n", 8, num);
}
scanf的问题根源在于它不进行边界检查,容易导致缓冲区溢出:
c复制#include <stdio.h>
void safe_input() {
char buffer[10];
// 危险示例
// scanf("%s", buffer); // 可能溢出
// 安全方案1:限制读取长度
scanf("%9s", buffer); // 最多读取9个字符
// 安全方案2:使用fgets
fgets(buffer, sizeof(buffer), stdin);
// 处理fgets留下的换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n')
buffer[len-1] = '\0';
}
常见scanf问题解决方案:
c复制int c;
while ((c = getchar()) != '\n' && c != EOF); // 清除缓冲区
c复制int num;
if (scanf("%d", &num) != 1) {
// 处理输入错误
}
使用终端控制序列可以实现更丰富的交互效果:
c复制#include <stdio.h>
#include <termios.h>
#include <unistd.h>
void terminal_control() {
// 获取当前终端设置
struct termios oldt, newt;
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
// 设置非规范模式(无缓冲)
newt.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
printf("按任意键继续(无回显)...");
getchar();
// 恢复原始设置
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
// 使用ANSI转义序列
printf("\033[2J"); // 清屏
printf("\033[1;31m红色文字\033[0m\n"); // 红色文本
}
文件操作必须考虑各种边界情况:
c复制#include <stdio.h>
#include <errno.h>
#include <string.h>
void safe_file_operations() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
fprintf(stderr, "打开文件失败: %s\n", strerror(errno));
return;
}
// 获取文件大小(可移植方法)
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
// 读取文件内容
char *buffer = malloc(size + 1);
if (buffer == NULL) {
fclose(fp);
fprintf(stderr, "内存分配失败\n");
return;
}
size_t read = fread(buffer, 1, size, fp);
if (read != size) {
fprintf(stderr, "读取错误: 期望%ld字节,实际读取%zu字节\n",
size, read);
}
buffer[read] = '\0';
printf("文件内容:\n%s\n", buffer);
// 清理资源
free(buffer);
if (fclose(fp) != 0) {
fprintf(stderr, "关闭文件时出错\n");
}
}
二进制I/O的关键是保持数据的一致性:
c复制#include <stdio.h>
#include <stdint.h>
typedef struct {
uint32_t id;
char name[32];
double score;
} Student;
void binary_io() {
// 写入二进制数据
Student s1 = {1, "张三", 95.5};
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) return;
fwrite(&s1, sizeof(Student), 1, fp);
fclose(fp);
// 读取二进制数据
Student s2;
fp = fopen("students.dat", "rb");
if (fp == NULL) return;
fread(&s2, sizeof(Student), 1, fp);
printf("学生ID: %u, 姓名: %s, 分数: %.1f\n",
s2.id, s2.name, s2.score);
fclose(fp);
}
二进制文件处理要点:
#pragma pack)在多进程环境中,文件锁定至关重要:
c复制#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
void file_locking() {
int fd = open("data.txt", O_RDWR);
if (fd == -1) {
perror("打开文件失败");
return;
}
// 获取独占锁
struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 0 // 锁定整个文件
};
if (fcntl(fd, F_SETLK, &lock) == -1) {
perror("获取文件锁失败");
close(fd);
return;
}
// 执行文件操作...
printf("获得文件锁,开始写入...\n");
write(fd, "测试数据", 12);
// 释放锁
lock.l_type = F_UNLCK;
fcntl(fd, F_SETLK, &lock);
close(fd);
}
对于大文件处理,内存映射能显著提高性能:
c复制#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
void memory_mapping() {
int fd = open("large_file.bin", O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
return;
}
struct stat sb;
if (fstat(fd, &sb) == -1) {
perror("获取文件信息失败");
close(fd);
return;
}
void *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
perror("内存映射失败");
close(fd);
return;
}
// 现在可以直接像访问内存一样访问文件内容
printf("文件前16字节: ");
for (int i = 0; i < 16; i++) {
printf("%02x ", ((unsigned char *)addr)[i]);
}
printf("\n");
// 清理
munmap(addr, sb.st_size);
close(fd);
}
处理多个I/O源时的经典模式:
c复制#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
#include <fcntl.h>
void non_blocking_io() {
// 设置stdin为非阻塞模式
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
fd_set readfds;
struct timeval timeout;
while (1) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select出错");
break;
} else if (ret == 0) {
printf("等待输入超时...\n");
continue;
}
if (FD_ISSET(STDIN_FILENO, &readfds)) {
char buf[128];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
if (n > 0) {
printf("收到输入: %.*s", (int)n, buf);
break;
}
}
}
}
c复制#include <stdio.h>
void optimize_buffer() {
FILE *fp = fopen("large_file.txt", "r");
if (fp == NULL) return;
// 设置8KB缓冲区
char buffer[8192];
setvbuf(fp, buffer, _IOFBF, sizeof(buffer));
// 文件操作...
fclose(fp);
}
批量读写:
内存对齐:
一个健壮的日志系统需要考虑多种因素:
c复制#include <stdio.h>
#include <time.h>
#include <stdarg.h>
#include <pthread.h>
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
void log_message(const char *format, ...) {
time_t now;
time(&now);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));
pthread_mutex_lock(&log_mutex);
FILE *log_file = fopen("app.log", "a");
if (log_file) {
fprintf(log_file, "[%s] ", timestamp);
va_list args;
va_start(args, format);
vfprintf(log_file, format, args);
va_end(args);
fprintf(log_file, "\n");
fclose(log_file);
}
// 同时输出到stderr
fprintf(stderr, "[%s] ", timestamp);
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, "\n");
pthread_mutex_unlock(&log_mutex);
}
文件描述符泄漏检测:
缓冲区问题诊断:
性能瓶颈分析:
处理不同平台的差异:
c复制#include <stdio.h>
void cross_platform_io() {
// 文件路径处理
const char *path = "data"
#ifdef _WIN32
"\\file.txt";
#else
"/file.txt";
#endif
// 换行符处理
FILE *fp = fopen(path, "w");
if (fp) {
// 写入时统一使用\n,在Windows上会自动转换为\r\n
fprintf(fp, "第一行\n第二行\n");
fclose(fp);
}
// 二进制模式的重要性(Windows)
fp = fopen(path, "rb"); // 总是使用二进制模式读取
if (fp) {
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 处理原始字节
}
fclose(fp);
}
}
c复制#include <stdio.h>
#include <errno.h>
#include <string.h>
void robust_io() {
FILE *src = NULL, *dst = NULL;
src = fopen("source.txt", "r");
if (src == NULL) {
fprintf(stderr, "无法打开源文件: %s\n", strerror(errno));
goto cleanup;
}
dst = fopen("destination.txt", "w");
if (dst == NULL) {
fprintf(stderr, "无法打开目标文件: %s\n", strerror(errno));
goto cleanup;
}
char buffer[1024];
size_t bytes;
while ((bytes = fread(buffer, 1, sizeof(buffer), src)) > 0) {
if (fwrite(buffer, 1, bytes, dst) != bytes) {
fprintf(stderr, "写入错误: %s\n", strerror(errno));
goto cleanup;
}
}
if (ferror(src)) {
fprintf(stderr, "读取错误: %s\n", strerror(errno));
}
cleanup:
if (src) fclose(src);
if (dst) fclose(dst);
}
c复制#include <stdio.h>
#include <string.h>
void memory_stream() {
char buffer[64];
FILE *stream = fmemopen(buffer, sizeof(buffer), "w");
if (stream) {
fprintf(stream, "当前温度: %.1f°C", 23.5);
fclose(stream);
printf("内存流内容: %s\n", buffer);
}
}
边界检查函数:
统一字符编码支持:
c复制#include <stdio.h>
#include <wchar.h>
void unicode_io() {
// 设置宽字符模式
FILE *fp = fopen("unicode.txt", "w");
if (fp) {
fwide(fp, 1); // 设置为宽字符模式
fwprintf(fp, L"宽字符字符串: %ls\n", L"中文测试");
fclose(fp);
}
// 读取宽字符文件
fp = fopen("unicode.txt", "r");
if (fp) {
fwide(fp, 1);
wchar_t line[256];
while (fgetws(line, sizeof(line)/sizeof(wchar_t), fp)) {
wprintf(L"读取到: %ls", line);
}
fclose(fp);
}
}
通过实际测试比较各种I/O方法的性能:
c复制#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define TEST_SIZE (100 * 1024 * 1024) // 100MB
void benchmark() {
clock_t start;
double duration;
// 测试1:单字节写入
start = clock();
FILE *fp = fopen("test1.bin", "wb");
if (fp) {
for (size_t i = 0; i < TEST_SIZE; i++) {
fputc('A', fp);
}
fclose(fp);
}
duration = (double)(clock() - start) / CLOCKS_PER_SEC;
printf("单字节写入: %.2f秒\n", duration);
// 测试2:块写入
start = clock();
fp = fopen("test2.bin", "wb");
if (fp) {
char *buffer = malloc(8192);
memset(buffer, 'A', 8192);
for (size_t i = 0; i < TEST_SIZE; i += 8192) {
fwrite(buffer, 1, 8192, fp);
}
free(buffer);
fclose(fp);
}
duration = (double)(clock() - start) / CLOCKS_PER_SEC;
printf("8KB块写入: %.2f秒\n", duration);
// 测试3:内存映射写入
start = clock();
fp = fopen("test3.bin", "wb");
if (fp) {
// 先扩展文件
fseek(fp, TEST_SIZE - 1, SEEK_SET);
fputc('\0', fp);
// 实际测试中应使用mmap...
fclose(fp);
}
duration = (double)(clock() - start) / CLOCKS_PER_SEC;
printf("内存映射: %.2f秒\n", duration);
}
根据场景选择最合适的I/O方式:
配置文件读写:
大数据处理:
网络通信:
标准I/O库的核心数据结构:
FILE结构体:
缓冲区的管理策略:
理解I/O操作的完整调用链:
性能优化的关键点:
文件描述符是操作系统层面的概念,而FILE*是C库的抽象:
c复制#include <stdio.h>
#include <unistd.h>
void fd_and_stream() {
// 从FILE*获取文件描述符
FILE *fp = fopen("test.txt", "w");
if (fp) {
int fd = fileno(fp);
printf("文件描述符: %d\n", fd);
// 直接使用系统调用写入
write(fd, "直接写入\n", 10);
// 同时使用stdio库写入
fprintf(fp, "通过FILE*写入\n");
fclose(fp);
}
// 从文件描述符创建FILE*
fd = open("another.txt", O_WRONLY | O_CREAT, 0644);
if (fd != -1) {
fp = fdopen(fd, "w");
if (fp) {
fprintf(fp, "通过fdopen创建的流\n");
fclose(fp); // 同时会关闭fd
} else {
close(fd);
}
}
}
路径注入:
竞争条件:
符号链接攻击:
c复制#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <stdlib.h>
void secure_file_operations(const char *user_input) {
// 1. 路径规范化检查
char resolved_path[PATH_MAX];
if (realpath(user_input, resolved_path) == NULL) {
perror("路径解析失败");
return;
}
// 2. 检查路径是否在允许的目录下
const char *allowed_dir = "/var/data/";
if (strncmp(resolved_path, allowed_dir, strlen(allowed_dir)) != 0) {
fprintf(stderr, "访问被拒绝: 路径不在允许的目录内\n");
return;
}
// 3. 安全打开文件
int fd = open(resolved_path, O_RDONLY | O_NOFOLLOW);
if (fd == -1) {
perror("无法安全打开文件");
return;
}
// 4. 验证文件属性
struct stat st;
if (fstat(fd, &st) == -1) {
perror("无法获取文件状态");
close(fd);
return;
}
if (!S_ISREG(st.st_mode)) { // 只允许普通文件
fprintf(stderr, "拒绝访问: 不是普通文件\n");
close(fd);
return;
}
// 5. 继续文件操作...
FILE *fp = fdopen(fd, "r");
if (fp) {
// 安全读取文件内容
char buffer[256];
while (fgets(buffer, sizeof(buffer), fp)) {
// 处理内容...
}
fclose(fp);
} else {
close(fd);
}
}
处理不可信输入时的防御措施:
长度检查:
内容过滤:
整数溢出防护:
c复制#include <stdio.h>
#include <ctype.h>
int safe_read_int(FILE *fp, int min, int max) {
char buffer[32];
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
return -1; // 读取失败
}
// 检查输入是否全是数字
for (char *p = buffer; *p && *p != '\n'; p++) {
if (!isdigit((unsigned char)*p)) {
return -1; // 非法输入
}
}
long val = strtol(buffer, NULL, 10);
if (val < min || val > max) {
return -1; // 超出范围
}
return (int)val;
}
C11标准引入的异步I/O支持:
c复制#include <stdio.h>
#include <threads.h>
void async_io_example() {
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) return;
// 启动异步读取
int result = fflush(fp); // 实际项目中应使用更完整的异步API
// 可以在这里执行其他工作...
// 等待I/O完成
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("读取到: %s", buffer);
}
fclose(fp);
}
构建可移植I/O层的常见模式:
c复制#include <stdio.h>
// 平台抽象接口
typedef struct {
int (*open)(const char *path, int mode);
int (*close)(int fd);
ssize_t (*read)(int fd, void *buf, size_t count);
ssize_t (*write)(int fd, const void *buf, size_t count);
} io_interface;
// 不同平台的实现
#ifdef _WIN32
#include <windows.h>
static int win_open(const char *path, int mode) {
// Windows实现...
}
#else
static int posix_open(const char *path, int mode) {
// POSIX实现...
}
#endif
// 初始化适当的接口
void init_io_interface(io_interface *io) {
#ifdef _WIN32
io->open = win_open;
#else
io->open = posix_open;
#endif
// 其他函数指针...
}
// 使用示例
void use_io_interface() {
io_interface io;
init_io_interface(&io);
int fd = io.open("file.txt", 0);
if (fd != -1) {
char buffer[128];
ssize_t bytes = io.read(fd, buffer, sizeof(buffer));
io.close(fd);
}
}
C语言I/O与其他语言的交互:
C++互操作:
Python扩展:
系统调用封装:
c复制// 示例:供Python调用的C扩展
#include <Python.h>
static PyObject *read_file(PyObject *self, PyObject *args) {
const char *filename;
if (!PyArg_ParseTuple(args, "s", &filename)) {
return NULL;
}
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
PyObject *content = PyBytes_FromString("");
char buffer[1024];
while (fgets(buffer, sizeof(buffer), fp)) {
PyBytes_ConcatAndDel(&content, PyBytes_FromString(buffer));
}
fclose(fp);
return content;
}
static PyMethodDef module_methods[] = {
{"read_file", read_file, METH_VARARGS, "Read a file"},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC PyInit_myio(void) {
return PyModule_Create(&(PyModuleDef){
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "myio",
.m_methods = module_methods
});
}