Linux系统C语言中与文件操作相关的核心库函数讲解
以下是 C 语言中文件操作相关库函数的表格总结,所有函数均来自 <stdio.h>
头文件:
C 语言文件操作库函数一览表
函数声明 | 功能描述 | 头文件 |
---|---|---|
文件打开与关闭 | ||
FILE *fopen(const char *filename, const char *mode); | 打开文件并返回文件指针 | <stdio.h> |
FILE *freopen(const char *filename, const char *mode, FILE *stream); | 重新定向文件流 | <stdio.h> |
int fclose(FILE *stream); | 关闭文件流 | <stdio.h> |
FILE *tmpfile(void); | 创建临时文件(自动删除) | <stdio.h> |
文件读写操作 | ||
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); | 从文件读取数据块 | <stdio.h> |
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); | 向文件写入数据块 | <stdio.h> |
int fgetc(FILE *stream); | 从文件读取单个字符 | <stdio.h> |
int getc(FILE *stream); | 从文件读取字符(通常为宏实现) | <stdio.h> |
int fputc(int char, FILE *stream); | 向文件写入单个字符 | <stdio.h> |
int putc(int char, FILE *stream); | 向文件写入字符(通常为宏实现) | <stdio.h> |
char *fgets(char *str, int n, FILE *stream); | 从文件读取一行字符串 | <stdio.h> |
int fputs(const char *str, FILE *stream); | 向文件写入字符串 | <stdio.h> |
int fprintf(FILE *stream, const char *format, ...); | 格式化输出到文件 | <stdio.h> |
int fscanf(FILE *stream, const char *format, ...); | 从文件格式化输入 | <stdio.h> |
int ungetc(int char, FILE *stream); | 将字符推回流中 | <stdio.h> |
文件定位与状态 | ||
int fseek(FILE *stream, long offset, int whence); | 设置文件位置 | <stdio.h> |
long ftell(FILE *stream); | 获取当前文件位置 | <stdio.h> |
void rewind(FILE *stream); | 重置文件位置到开头 | <stdio.h> |
int fgetpos(FILE *stream, fpos_t *pos); | 获取文件位置(大文件支持) | <stdio.h> |
int fsetpos(FILE *stream, const fpos_t *pos); | 设置文件位置(大文件支持) | <stdio.h> |
int feof(FILE *stream); | 检查文件结束标志 | <stdio.h> |
int ferror(FILE *stream); | 检查文件错误标志 | <stdio.h> |
void clearerr(FILE *stream); | 清除错误和结束标志 | <stdio.h> |
文件管理 | ||
int remove(const char *filename); | 删除文件 | <stdio.h> |
int rename(const char *old_filename, const char *new_filename); | 重命名文件 | <stdio.h> |
char *tmpnam(char *str); | 生成临时文件名(有安全风险) | <stdio.h> |
缓冲区控制 | ||
int fflush(FILE *stream); | 刷新输出缓冲区 | <stdio.h> |
void setbuf(FILE *stream, char *buffer); | 设置文件缓冲 | <stdio.h> |
int setvbuf(FILE *stream, char *buffer, int mode, size_t size); | 设置文件缓冲模式 | <stdio.h> |
fopen()
函数
1. 函数声明
FILE *fopen(const char *pathname, const char *mode);
- 作用:打开文件并创建文件流,用于后续读写操作
- 头文件:
#include <stdio.h>
2. 参数详解
参数 | 类型 | 说明 |
---|---|---|
pathname | const char * | 文件路径(绝对路径或相对路径) |
mode | const char * | 文件访问模式(见下方模式表) |
返回值 | FILE * | 成功返回文件指针,失败返回 NULL |
3. 打开模式详解(Linux 特注)
在 Linux 系统中,文本模式(无 “b”)和二进制模式(带 “b”)行为相同,因为 Linux 不转换换行符。
模式 | 描述 | 文件存在 | 文件不存在 | 指针位置 | 写入行为 |
---|---|---|---|---|---|
"r" | 只读 | 必须存在 | 失败 | 开头 | 禁止写入 |
"w" | 只写 | 清空内容 | 创建新文件 | 开头 | 覆盖写入 |
"a" | 追加 | 保留内容 | 创建新文件 | 末尾 | 尾部追加 |
"r+" | 读写 | 必须存在 | 失败 | 开头 | 覆盖写入 |
"w+" | 读写 | 清空内容 | 创建新文件 | 开头 | 覆盖写入 |
"a+" | 读写 | 保留内容 | 创建新文件 | 读:开头 写:末尾 | 尾部追加 |
📌 Linux 重要提示:
- 模式中的
b
(如"rb"
)在 Linux 中可忽略,但为跨平台兼容建议保留- 文件权限:新文件默认权限为
0666 & ~umask
4. 使用示例
#include <stdio.h>
#include <stdlib.h>int main() {// 示例1:安全打开文件(必须检查返回值!)FILE *fp = fopen("/home/user/data.txt", "r");if (fp == NULL) {perror("fopen failed"); // 输出具体错误exit(EXIT_FAILURE);}// 示例2:写入文件(自动创建新文件)FILE *log = fopen("app.log", "a"); // 追加模式if (log) {fprintf(log, "[INFO] Program started\n");fclose(log);}// 示例3:读写文件FILE *data = fopen("test.dat", "r+");if (data) {int value;fscanf(data, "%d", &value); // 读取fseek(data, 0, SEEK_SET); // 重置指针fprintf(data, "%d", value*2);// 写入fclose(data);}return 0;
}
5. 关键注意事项
-
必须检查返回值
FILE *fp = fopen("file.txt", "r"); if (fp == NULL) {// 处理错误:文件不存在/权限不足/路径错误perror("Error details"); }
-
文件路径规范
- 绝对路径:
/home/user/docs/file.txt
- 相对路径:
../data/config.cfg
- 当前目录:
./output.log
或output.log
- 绝对路径:
-
模式选择陷阱
"w"
和"w+"
会清空文件内容(误用会导致数据丢失!)"a"
模式写入时总是追加到末尾(无视文件指针位置)
-
读写位置管理
// 在读写模式中切换操作时需重置指针 fseek(fp, 0, SEEK_SET); // 回到文件开头 rewind(fp); // 同上(但不清除错误标志)
-
资源释放
fclose(fp); // 必须关闭文件!否则导致: // - 资源泄漏 // - 数据未写入磁盘 // - 文件锁定问题
6. Linux 特有细节
-
文件锁机制
- 使用
flock()
或fcntl()
实现文件锁定 fopen()
本身不提供锁,需额外处理并发访问
- 使用
-
权限控制
// 通过 umask 影响新文件权限 umask(0022); // 设置掩码 FILE *fp = fopen("new.txt", "w"); // 实际权限 0644
-
符号链接处理
- 默认跟随符号链接
- 需防符号链接攻击:
// 安全做法:先 lstat() 检查链接
-
错误代码
- 通过
errno
获取具体错误:
#include <errno.h> if (fp == NULL) {if (errno == EACCES) {printf("Permission denied\n");} }
- 通过
7. 最佳实践总结
-
始终检查返回值:避免 NULL 指针导致崩溃
-
明确路径处理:使用绝对路径或规范相对路径
-
模式选择原则:
- 只读 →
"r"
- 只写(覆盖)→
"w"
- 追加 →
"a"
- 读写 →
"r+"
(需文件存在)或"w+"
(清空)
- 只读 →
-
二进制文件处理:跨平台时显式使用
"b"
模式 -
资源管理:
FILE *fp = NULL; if ((fp = fopen(...)) != NULL) {// 操作文件fclose(fp); // 确保关闭 }
-
错误处理:使用
perror()
或strerror(errno)
输出详细信息
💡 性能提示:
- 频繁小量读写考虑使用
setvbuf()
调整缓冲区- 大文件操作使用
fseek()
替代多次fopen()
fclose()
函数
fclose()
是 C 语言文件操作中至关重要的函数,负责安全关闭文件并释放资源。在 Linux 系统编程中,正确使用 fclose()
是防止资源泄漏和确保数据完整性的关键。
一、函数声明与核心概念
1. 函数声明
int fclose(FILE *stream);
2. 作用
- 关闭打开的文件流
- 刷新缓冲区,确保所有数据写入磁盘
- 释放与文件流相关的系统资源
- 断开文件流与底层文件描述符的关联
3. 返回值
- 成功:返回
0
- 失败:返回
EOF
(通常为 -1)
二、参数详解与使用示例
FILE *stream
- 文件流指针
作用:指定要关闭的文件流
要求:
- 必须是通过
fopen()
,freopen()
或tmpfile()
打开的有效文件指针 - 关闭后文件指针变为无效(称为"悬挂指针")
- 多次关闭同一文件指针会导致未定义行为
示例:
FILE *file = fopen("data.txt", "w");
if (file) {// 文件操作...if (fclose(file) == EOF) {perror("关闭文件失败");}// file 指针现在无效,不应再使用
}
三、完整使用示例
示例1:基本文件操作流程
#include <stdio.h>
#include <stdlib.h>int main() {FILE *file = fopen("example.txt", "w");if (!file) {perror("文件打开失败");return EXIT_FAILURE;}// 写入数据fprintf(file, "Hello, Linux File System!\n");// 关闭文件if (fclose(file) != 0) {perror("文件关闭失败");return EXIT_FAILURE;}printf("文件操作成功完成\n");return EXIT_SUCCESS;
}
示例2:批量处理多个文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define MAX_FILES 5int main() {FILE *files[MAX_FILES];const char *filenames[] = {"file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"};// 打开所有文件for (int i = 0; i < MAX_FILES; i++) {files[i] = fopen(filenames[i], "w");if (!files[i]) {fprintf(stderr, "无法打开文件: %s\n", filenames[i]);// 关闭之前已打开的文件for (int j = 0; j < i; j++) {fclose(files[j]);}return EXIT_FAILURE;}}// 写入数据到所有文件for (int i = 0; i < MAX_FILES; i++) {fprintf(files[i], "这是文件 %d 的内容\n", i+1);}// 关闭所有文件并检查错误int error_occurred = 0;for (int i = 0; i < MAX_FILES; i++) {if (fclose(files[i]) != 0) {perror(filenames[i]);error_occurred = 1;}}return error_occurred ? EXIT_FAILURE : EXIT_SUCCESS;
}
示例3:使用临时文件
#include <stdio.h>
#include <stdlib.h>int main() {// 创建临时文件(自动删除)FILE *tmp = tmpfile();if (!tmp) {perror("无法创建临时文件");return EXIT_FAILURE;}// 写入临时数据fprintf(tmp, "临时数据\n");// 关闭临时文件(自动删除)if (fclose(tmp) != 0) {perror("关闭临时文件失败");return EXIT_FAILURE;}printf("临时文件已安全处理\n");return EXIT_SUCCESS;
}
四、关键注意事项与使用细节
1. 资源泄漏风险
危险做法:
void process_file() {FILE *file = fopen("data.txt", "r");// 处理文件...// 忘记 fclose(file)!// 函数返回后文件保持打开状态
}
正确做法:
void safe_process() {FILE *file = fopen("data.txt", "r");if (!file) return;// 使用 goto 确保关闭if (error_condition) goto cleanup;// 处理文件...cleanup:if (fclose(file) != 0) {// 处理关闭错误}
}
2. 文件描述符管理
在 Linux 中,每个文件流对应一个文件描述符:
FILE *file = fopen("test.txt", "w");
int fd = fileno(file); // 获取底层文件描述符// 关闭文件流
fclose(file);// 现在 fd 已无效,不应再使用
3. 缓冲区刷新机制
fclose()
会强制刷新缓冲区:
FILE *file = fopen("log.txt", "w");
fprintf(file, "重要数据...");// 此时数据可能在缓冲区,尚未写入磁盘
fclose(file); // 强制刷新缓冲区,确保数据写入
4. 错误处理重要性
FILE *file = fopen("critical.dat", "w");
// ...写入操作...
if (fclose(file) != 0) {// 关闭失败可能意味着数据未完全写入!// 需要紧急处理:尝试重新打开、备份等
}
5. 文件指针复用
FILE *file = fopen("first.txt", "w");
fclose(file);// 复用指针打开新文件
file = fopen("second.txt", "r");
if (file) {// 处理新文件...fclose(file);
}
五、高级应用场景
1. 使用 atexit()
确保关闭
#include <stdlib.h>static FILE *global_file;void cleanup_file(void) {if (global_file && fclose(global_file) == 0) {global_file = NULL;}
}int main() {global_file = fopen("persistent.log", "a");if (!global_file) {perror("打开文件失败");return EXIT_FAILURE;}// 注册退出处理函数atexit(cleanup_file);// 程序运行期间持续使用文件...// 正常退出时自动关闭文件return EXIT_SUCCESS;
}
2. 文件描述符传递
#include <stdio.h>
#include <unistd.h>int main() {FILE *file = fopen("data.bin", "wb");int fd = fileno(file);// 写入数据...write(fd, "RAW DATA", 8);// 混合使用标准IO和系统调用时需要小心fflush(file); // 确保缓冲区刷新// 关闭文件fclose(file);return 0;
}
3. 自定义流关闭
#include <stdio.h>
#include <stdlib.h>int custom_close(void *cookie) {printf("关闭自定义流\n");free(cookie);return 0;
}FILE *create_custom_stream() {char *buffer = malloc(1024);// 创建自定义文件流FILE *stream = fopencookie(buffer, "w", (cookie_io_functions_t){.close = custom_close});return stream;
}int main() {FILE *custom = create_custom_stream();fprintf(custom, "测试自定义流");fclose(custom); // 触发 custom_closereturn 0;
}
六、常见错误与解决方案
错误1:忽略返回值
// 危险:可能忽略关闭错误
fclose(file);
解决方案:
if (fclose(file) != 0) {perror("文件关闭失败");// 处理错误
}
错误2:重复关闭
fclose(file);
fclose(file); // 未定义行为!
解决方案:
fclose(file);
file = NULL; // 设置为 NULL 防止误用
错误3:关闭标准流
fclose(stdin); // 危险!可能导致后续输入函数失败
解决方案:
// 除非必要,否则不要关闭标准流
// 如需重定向,使用 freopen()
FILE *new_stdout = freopen("output.log", "w", stdout);
错误4:忘记关闭动态分配的文件指针数组
FILE **files = malloc(10 * sizeof(FILE*));
for (int i = 0; i < 10; i++) {files[i] = fopen(...);
}
// 使用后...
free(files); // 忘记关闭文件!
解决方案:
for (int i = 0; i < 10; i++) {if (files[i]) fclose(files[i]);
}
free(files);
七、性能与资源管理
1. 文件描述符限制
#include <sys/resource.h>// 检查文件描述符限制
struct rlimit rlim;
getrlimit(RLIMIT_NOFILE, &rlim);
printf("软限制: %ld, 硬限制: %ld\n", rlim.rlim_cur, rlim.rlim_max);
2. 监控打开文件
// 查看进程打开的文件
system("ls -l /proc/$PPID/fd");
3. 提高文件描述符限制
// 临时提高限制
struct rlimit new_limit = {10240, 10240};
setrlimit(RLIMIT_NOFILE, &new_limit);
八、替代方案比较
1. vs close()
系统调用
int fd = open("file.txt", O_WRONLY);
// ...操作...
close(fd); // 系统调用,不刷新缓冲区
2. vs fflush()
// fflush() 刷新但不关闭
fflush(file);
// 文件仍保持打开状态
3. 自动关闭技术(C++ RAII)
// C 中模拟 RAII
typedef struct {FILE *file;
} AutoFile;void auto_close(AutoFile *af) {if (af->file) fclose(af->file);
}int main() {AutoFile af = {fopen("auto.txt", "w")};// 使用 af.file...auto_close(&af); // 显式关闭return 0;
}
九、总结
fclose()
是 Linux C 文件操作的最后关键一步,核心要点:
- 资源释放:关闭文件释放文件描述符和内存资源
- 数据完整性:确保缓冲数据写入磁盘
- 错误处理:必须检查返回值
- 安全实践:关闭后置指针为 NULL,避免重复关闭
- 系统限制:了解文件描述符限制
- 特殊文件:正确处理临时文件和标准流
fread()
函数
一、函数声明与核心概念
1. 函数声明
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
2. 作用
fread()
用于从文件流中读取数据块,特别适合处理二进制文件和大数据块。它是 Linux 系统中高效文件操作的核心函数之一。
3. 返回值
- 成功:返回实际读取的完整数据项数量(不是字节数)
- 失败/EOF:返回值小于
nmemb
参数 - 0:当文件结束或发生错误时返回 0
二、参数详解与使用示例
1. void *ptr
- 数据存储缓冲区
作用:指向存放读取数据的内存地址
类型:通用指针(可指向任何数据类型)
注意事项:
- 必须确保缓冲区足够大(
size * nmemb
字节) - 缓冲区内存必须已分配(静态数组或动态分配)
示例:
// 读取整数数组
int numbers[100];
size_t items_read = fread(numbers, sizeof(int), 100, file_ptr);// 读取结构体
struct Employee {int id;char name[50];float salary;
};
struct Employee emp;
fread(&emp, sizeof(struct Employee), 1, file_ptr);
2. size_t size
- 单个数据项大小
作用:指定每个数据项的字节大小
最佳实践:
- 使用
sizeof
运算符确保准确性 - 对于基本类型:
sizeof(int)
,sizeof(double)
等 - 对于结构体:
sizeof(struct_name)
示例:
// 正确使用 sizeof
double values[50];
fread(values, sizeof(double), 50, file_ptr);// 错误示例(硬编码大小)
float data[100];
fread(data, 4, 100, file_ptr); // 错误!float 大小可能不是4字节
3. size_t nmemb
- 数据项数量
作用:指定要读取的数据项数量
注意事项:
- 与
size
参数共同决定总读取字节数(size * nmemb) - 实际读取量可能小于请求量(文件结束或错误)
示例:
// 读取100个结构体
struct Point { float x, y; };
struct Point points[100];
size_t read_count = fread(points, sizeof(struct Point), 100, file_ptr);if (read_count < 100) {// 处理未完全读取的情况
}
4. FILE *stream
- 文件流指针
作用:指定要读取的文件流
要求:
- 必须通过
fopen()
打开的文件指针 - 文件必须以可读模式打开(如 “r”, “rb”, “r+” 等)
示例:
FILE *file = fopen("data.bin", "rb"); // 二进制读取模式
if (file == NULL) {perror("文件打开失败");exit(EXIT_FAILURE);
}char buffer[1024];
size_t read_items = fread(buffer, 1, 1024, file); // 读取1024字节
三、完整使用示例
示例1:读取二进制文件
#include <stdio.h>
#include <stdlib.h>int main() {FILE *file = fopen("image.jpg", "rb");if (!file) {perror("无法打开文件");return 1;}// 获取文件大小fseek(file, 0, SEEK_END);long file_size = ftell(file);rewind(file);// 分配内存unsigned char *image_data = malloc(file_size);if (!image_data) {perror("内存分配失败");fclose(file);return 1;}// 读取整个文件size_t items_read = fread(image_data, 1, file_size, file);if (items_read != file_size) {fprintf(stderr, "读取错误:请求 %ld 字节,实际读取 %zu 字节\n",file_size, items_read);}// 处理图像数据...free(image_data);fclose(file);return 0;
}
示例2:读取结构化数据文件
#include <stdio.h>
#include <stdlib.h>typedef struct {int id;char name[30];double balance;
} Account;int main() {FILE *file = fopen("accounts.dat", "rb");if (!file) {perror("无法打开账户文件");return 1;}// 读取文件头(记录数量)int record_count;if (fread(&record_count, sizeof(int), 1, file) != 1) {perror("读取记录数量失败");fclose(file);return 1;}// 分配内存读取所有账户Account *accounts = malloc(record_count * sizeof(Account));if (!accounts) {perror("内存分配失败");fclose(file);return 1;}// 批量读取账户数据size_t read_count = fread(accounts, sizeof(Account), record_count, file);if (read_count != record_count) {fprintf(stderr, "警告:请求 %d 条记录,实际读取 %zu 条\n",record_count, read_count);}// 处理账户数据...free(accounts);fclose(file);return 0;
}
四、关键注意事项与使用细节
1. 二进制模式 vs 文本模式
- 二进制模式(“rb”):
- 数据按原样读取,无任何转换
- 适合图像、音频、压缩文件等
- 文本模式(“r”):
- Linux 中与二进制模式相同(无换行符转换)
- Windows 中会转换换行符(
\r\n
→\n
)
建议:处理非文本文件时始终使用二进制模式
2. 错误处理最佳实践
size_t items = fread(buffer, size, nmemb, file);if (items < nmemb) {if (feof(file)) {printf("到达文件末尾\n");}if (ferror(file)) {perror("读取错误");}
}
3. 缓冲区对齐优化
对于高性能应用,确保缓冲区对齐:
// 使用对齐分配
double *data = aligned_alloc(64, 1024 * sizeof(double)); // 64字节对齐
fread(data, sizeof(double), 1024, file);
4. 大文件处理技巧
分块读取大文件避免内存不足:
#define CHUNK_SIZE 65536 // 64KBunsigned char buffer[CHUNK_SIZE];
size_t total_read = 0;
size_t chunk_read;while ((chunk_read = fread(buffer, 1, CHUNK_SIZE, file)) > 0) {total_read += chunk_read;// 处理当前数据块...
}
5. 结构体读取的特殊考虑
struct Data {int a;double b;
};// 潜在问题:结构体可能有填充字节
fread(&data, sizeof(struct Data), 1, file);// 解决方案:逐字段读取
fread(&data.a, sizeof(int), 1, file);
fread(&data.b, sizeof(double), 1, file);
6. 与 fwrite()
配对使用
写入和读取应使用相同的参数:
// 写入
int numbers[] = {1, 2, 3, 4, 5};
fwrite(numbers, sizeof(int), 5, file);// 读取(必须匹配)
int read_nums[5];
fread(read_nums, sizeof(int), 5, file);
五、性能优化技巧
1. 设置缓冲区大小
FILE *file = fopen("large.dat", "rb");
char custom_buffer[131072]; // 128KB// 设置自定义缓冲区
setvbuf(file, custom_buffer, _IOFBF, sizeof(custom_buffer));
2. 直接 I/O(绕过内核缓存)
file = fopen("fast.dat", "rb");
posix_fadvise(fileno(file), 0, 0, POSIX_FADV_SEQUENTIAL);
3. 内存映射文件
对于超大文件,考虑使用 mmap
:
int fd = open("huge.dat", O_RDONLY);
void *map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);// 直接访问文件内容,无需fread
memcpy(buffer, map, buffer_size);
六、常见错误与解决方案
错误1:忽略返回值
// 错误做法
fread(buffer, size, nmemb, file);// 正确做法
size_t read = fread(buffer, size, nmemb, file);
if (read < nmemb) {// 处理部分读取或错误
}
错误2:大小计算错误
// 错误:混淆大小和数量
int data[100];
fread(data, 100, sizeof(int), file); // 参数顺序错误!// 正确
fread(data, sizeof(int), 100, file);
错误3:未处理字节序
// 跨平台问题
uint32_t value;
fread(&value, sizeof(uint32_t), 1, file);// 需要转换字节序
value = ntohl(value); // 网络字节序转主机字节序
七、总结
fread()
是 Linux C 程序中高效文件读取的核心函数,关键要点:
- 参数顺序:
ptr, size, nmemb, stream
- 返回值:实际读取的完整项数(非字节数)
- 错误处理:必须检查返回值并配合
feof()/ferror()
- 二进制模式:非文本文件必须使用 “rb”
- 缓冲区管理:确保足够空间和正确对齐
- 大文件处理:分块读取避免内存不足
fprintf()
函数
fprintf()
是 C 语言中用于格式化输出的核心函数,特别适合向文件流写入格式化的数据。在 Linux 系统编程中,它是日志记录、配置文件生成等任务的关键工具。
一、函数声明与核心概念
1. 函数声明
int fprintf(FILE *stream, const char *format, ...);
2. 作用
将格式化的数据写入指定的文件流中,支持多种数据类型和复杂的格式控制。
3. 返回值
- 成功:返回写入的字符总数(不包括字符串结尾的 ‘\0’)
- 失败:返回负数(通常表示错误)
二、参数详解与使用示例
1. FILE *stream
- 文件流指针
作用:指定要写入的文件流
要求:
- 必须通过
fopen()
打开的文件指针 - 文件必须以可写模式打开(如 “w”, “a”, “r+” 等)
- 可以是标准流:
stdout
(标准输出)、stderr
(标准错误)
示例:
// 写入普通文件
FILE *log = fopen("app.log", "a");
fprintf(log, "[INFO] Application started\n");// 写入标准输出
fprintf(stdout, "Hello, World!\n");// 写入标准错误
fprintf(stderr, "[ERROR] Invalid input detected\n");
2. const char *format
- 格式字符串
作用:控制输出格式的字符串,包含:
- 普通字符:直接输出
- 格式说明符:以
%
开头,指定后续参数的格式
格式说明符语法:
%[标志][宽度][.精度][长度]类型
常用类型说明符:
说明符 | 类型 | 示例输出 |
---|---|---|
%d | int | 42 |
%u | unsigned int | 42 |
%f | float/double | 3.141593 |
%e | 科学计数法 | 3.141593e+00 |
%g | 自动选择 %f/%e | 3.14159 |
%c | char | A |
%s | 字符串 | “Hello” |
%p | 指针地址 | 0x7ffd42 |
%% | 百分号字符 | % |
标志、宽度、精度示例:
int num = 42;
double pi = 3.1415926535;// 标志示例
fprintf(stdout, "%-10d\n", num); // 左对齐: "42 "
fprintf(stdout, "%+d\n", num); // 显示符号: "+42"
fprintf(stdout, "%#x\n", num); // 十六进制前缀: "0x2a"// 宽度和精度
fprintf(stdout, "%10d\n", num); // 宽度10: " 42"
fprintf(stdout, "%.3f\n", pi); // 精度3: "3.142"
fprintf(stdout, "%10.3f\n", pi); // 宽度10精度3: " 3.142"
3. ...
- 可变参数
作用:根据格式字符串提供要输出的数据
要求:
- 参数数量和类型必须与格式字符串中的说明符匹配
- 支持任意数量和类型的参数
示例:
// 多参数示例
char *name = "Alice";
int age = 30;
double score = 95.5;fprintf(stdout, "Name: %s, Age: %d, Score: %.1f\n", name, age, score);
// 输出: Name: Alice, Age: 30, Score: 95.5
三、完整使用示例
示例1:创建配置文件
#include <stdio.h>
#include <time.h>int main() {FILE *config = fopen("app.conf", "w");if (!config) {perror("无法创建配置文件");return 1;}time_t now = time(NULL);struct tm *t = localtime(&now);fprintf(config, "# Application Configuration\n");fprintf(config, "# Generated on: %04d-%02d-%02d %02d:%02d:%02d\n",t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,t->tm_hour, t->tm_min, t->tm_sec);fprintf(config, "\n[Settings]\n");fprintf(config, "log_level = INFO\n");fprintf(config, "max_connections = %d\n", 100);fprintf(config, "timeout = %.2f\n", 5.75);fclose(config);return 0;
}
示例2:带错误处理的日志系统
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <string.h>
#include <errno.h>void log_message(FILE *stream, const char *level, const char *format, ...) {time_t now = time(NULL);char timestamp[20];strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));// 打印时间戳和日志级别fprintf(stream, "[%s] [%s] ", timestamp, level);// 处理可变参数va_list args;va_start(args, format);vfprintf(stream, format, args);va_end(args);// 如果有系统错误,添加错误信息if (errno != 0) {fprintf(stream, " (%s)", strerror(errno));errno = 0; // 重置错误状态}fprintf(stream, "\n");fflush(stream); // 确保立即写入
}int main() {FILE *logfile = fopen("application.log", "a");if (!logfile) {perror("无法打开日志文件");return 1;}log_message(stdout, "INFO", "Application started");log_message(logfile, "DEBUG", "Initializing modules");// 模拟一个错误FILE *missing = fopen("nonexistent.txt", "r");if (!missing) {log_message(stderr, "ERROR", "Failed to open file");log_message(logfile, "ERROR", "File open failed");}log_message(logfile, "INFO", "Shutting down");fclose(logfile);return 0;
}
四、关键注意事项与使用细节
1. 格式字符串安全
危险做法:
char user_input[100];
fgets(user_input, sizeof(user_input), stdin);
fprintf(stdout, user_input); // 格式字符串漏洞!
安全做法:
fprintf(stdout, "%s", user_input); // 使用%s输出字符串
2. 缓冲区与刷新
// 默认情况下,文件流是缓冲的
fprintf(file, "This may not appear immediately");// 强制刷新缓冲区
fflush(file);// 设置无缓冲
setbuf(file, NULL);
3. 错误处理
int bytes_written = fprintf(file, "Important data");
if (bytes_written < 0) {perror("写入失败");if (ferror(file)) {// 处理具体错误}
}
4. 性能优化
对于频繁写入:
// 使用更大的缓冲区
char buffer[8192];
setvbuf(file, buffer, _IOFBF, sizeof(buffer));// 批量写入
for (int i = 0; i < 1000; i++) {fprintf(file, "Data %d\n", i);
}
// 而不是每次写入后刷新
5. 本地化支持
#include <locale.h>setlocale(LC_NUMERIC, "de_DE.UTF-8"); // 德国本地化
fprintf(stdout, "%.2f\n", 1234.56); // 输出 "1234,56"(使用逗号分隔)
6. 高级格式控制
// 动态宽度和精度
int width = 10;
int precision = 3;
double value = 3.1415926535;fprintf(stdout, "%*.*f\n", width, precision, value); // 输出 " 3.142"// 位置参数(GNU扩展)
fprintf(stdout, "%2$s %1$d\n", 42, "Answer"); // 输出 "Answer 42"
五、特殊文件处理
1. 写入 /dev/null
FILE *null = fopen("/dev/null", "w");
fprintf(null, "This will be discarded\n"); // 写入黑洞
2. 写入命名管道
mkfifo("my_pipe", 0666);
FILE *pipe = fopen("my_pipe", "w");
fprintf(pipe, "Data for another process\n");
3. 写入 syslog
#include <syslog.h>// 直接使用 syslog 函数更合适
syslog(LOG_INFO, "System event occurred");
六、常见错误与解决方案
错误1:格式说明符与参数不匹配
int num = 42;
fprintf(stdout, "Value: %f\n", num); // 错误!应为%d
解决方案:
- 使用编译器警告:
gcc -Wall -Werror
- 使用静态分析工具
错误2:忘记检查返回值
fprintf(file, "Critical data"); // 可能失败但未处理
解决方案:
if (fprintf(file, "Critical data") < 0) {// 错误处理
}
错误3:文件未正确打开
FILE *file = fopen("data.txt", "r"); // 只读模式
fprintf(file, "Trying to write"); // 会失败
解决方案:
FILE *file = fopen("data.txt", "a+"); // 读写/追加模式
if (!file) {perror("打开文件失败");return;
}
错误4:缓冲区溢出
char name[10];
fprintf(stdout, "Name: %s\n", name); // 如果name过长会溢出
解决方案:
fprintf(stdout, "Name: %.9s\n", name); // 限制输出长度
七、性能对比与替代方案
1. 与 fwrite()
对比
// 使用 fprintf 写入字符串
fprintf(file, "Hello, World!\n");// 使用 fwrite 效率更高
const char *msg = "Hello, World!\n";
fwrite(msg, 1, strlen(msg), file);
2. 与 snprintf()
+ fwrite()
组合
char buffer[256];
int len = snprintf(buffer, sizeof(buffer), "Value: %d", 42);
fwrite(buffer, 1, len, file);
3. 与 dprintf()
对比(Linux 特有)
// 直接使用文件描述符
int fd = open("file.txt", O_WRONLY);
dprintf(fd, "Value: %d\n", 42);
八、总结
fprintf()
是 Linux C 编程中格式化输出的核心工具,关键要点:
- 格式控制:使用
%
格式说明符控制输出格式 - 文件流:支持文件、标准输出、标准错误等
- 错误处理:必须检查返回值和错误状态
- 安全:避免格式字符串漏洞
- 性能:批量写入和缓冲区管理优化性能
- 本地化:考虑本地化设置对输出的影响
fgets()
函数
fgets()
是 C 语言中用于安全读取文本行的核心函数,特别适合处理用户输入和文本文件。在 Linux 系统编程中,它是避免缓冲区溢出的关键工具。
一、函数声明与核心概念
1. 函数声明
char *fgets(char *str, int n, FILE *stream);
2. 作用
从指定的文件流中读取一行文本(直到遇到换行符或文件结束),并确保缓冲区不会溢出。
3. 返回值
- 成功:返回
str
指针(传入的缓冲区地址) - 失败/EOF:返回
NULL
- 当读取到文件末尾且没有读取任何字符时返回
NULL
二、参数详解与使用示例
1. char *str
- 数据存储缓冲区
作用:存放读取到的字符串
要求:
- 必须是指向字符数组的指针
- 缓冲区大小必须足够容纳读取的数据
- 函数会自动添加字符串终止符
\0
示例:
// 静态缓冲区
char buffer[100];
fgets(buffer, sizeof(buffer), stdin);// 动态分配缓冲区
char *dyn_buffer = malloc(256);
if (dyn_buffer) {fgets(dyn_buffer, 256, stdin);
}
2. int n
- 最大读取字符数
作用:指定最多读取的字符数(包括结尾的 \0
)
关键细节:
- 实际读取字符数 =
n - 1
(因为要预留空间给\0
) - 如果遇到换行符,会包含在读取的字符串中
- 如果遇到 EOF,会提前结束读取
示例:
char buf[10];
fgets(buf, sizeof(buf), stdin);// 输入 "HelloWorld"(10字符)
// buf 内容: "HelloWorl" + \0(实际读取9字符)
3. FILE *stream
- 文件流指针
作用:指定要读取的文件流
常用值:
stdin
:标准输入(键盘)- 文件指针:通过
fopen()
打开的文件
示例:
// 从标准输入读取
fgets(buffer, 100, stdin);// 从文件读取
FILE *file = fopen("data.txt", "r");
if (file) {fgets(buffer, 100, file);
}
三、完整使用示例
示例1:安全读取用户输入
#include <stdio.h>
#include <string.h>int main() {char name[50];printf("请输入您的姓名:");if (fgets(name, sizeof(name), stdin) == NULL) {fprintf(stderr, "读取输入失败\n");return 1;}// 去除可能的换行符name[strcspn(name, "\n")] = '\0';printf("您好, %s!\n", name);return 0;
}
示例2:逐行读取文件
#include <stdio.h>
#include <stdlib.h>int main() {FILE *file = fopen("config.txt", "r");if (!file) {perror("无法打开文件");return 1;}char line[256];int line_num = 0;while (fgets(line, sizeof(line), file) != NULL) {line_num++;// 去除换行符size_t len = strlen(line);if (len > 0 && line[len-1] == '\n') {line[len-1] = '\0';}printf("%4d: %s\n", line_num, line);}if (ferror(file)) {perror("读取文件时出错");}fclose(file);return 0;
}
示例3:处理长行(分块读取)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>int main() {FILE *file = fopen("long_lines.txt", "r");if (!file) {perror("无法打开文件");return 1;}char chunk[100];char *full_line = NULL;size_t full_size = 0;bool complete_line = false;while (fgets(chunk, sizeof(chunk), file) != NULL) {// 检查是否读取了完整行size_t chunk_len = strlen(chunk);complete_line = (chunk_len > 0 && chunk[chunk_len-1] == '\n');// 如果存在换行符,替换为 \0if (complete_line) {chunk[chunk_len-1] = '\0';}// 扩展缓冲区size_t new_size = full_size + chunk_len + (complete_line ? 0 : 1);char *new_buffer = realloc(full_line, new_size);if (!new_buffer) {free(full_line);perror("内存分配失败");fclose(file);return 1;}full_line = new_buffer;strcpy(full_line + full_size, chunk);full_size = new_size - (complete_line ? 0 : 1);// 如果是完整行,处理并重置if (complete_line) {printf("完整行: %s\n", full_line);free(full_line);full_line = NULL;full_size = 0;}}// 处理文件末尾的不完整行if (full_line) {printf("不完整行: %s\n", full_line);free(full_line);}fclose(file);return 0;
}
四、关键注意事项与使用细节
1. 换行符处理
fgets()
会保留换行符:
// 输入 "Hello" + Enter
fgets(buf, 100, stdin);
// buf 内容: "Hello\n\0"// 安全去除换行符:
buf[strcspn(buf, "\n")] = '\0';
2. 缓冲区溢出保护
fgets()
是安全函数:
// 安全
char small_buf[5];
fgets(small_buf, sizeof(small_buf), stdin);// 危险替代方案(已弃用)
gets(small_buf); // 可能导致缓冲区溢出
3. EOF 处理
正确处理文件结束:
while (fgets(line, sizeof(line), file)) {// 处理行
}if (feof(file)) {printf("已到达文件末尾\n");
} else if (ferror(file)) {perror("读取错误");
}
4. 输入流残留数据
当读取部分行时:
char buf[5];
fgets(buf, sizeof(buf), stdin); // 输入 "HelloWorld"// 输入流中残留 "oWorld\n"
// 需要清空输入流:
int c;
while ((c = getchar()) != '\n' && c != EOF);
5. 二进制文件注意事项
fgets()
不适合二进制文件:
// 错误用法:读取二进制文件
FILE *bin = fopen("data.bin", "rb");
char buf[100];
fgets(buf, sizeof(buf), bin); // 可能提前停止(遇到0字节)// 正确做法:使用 fread()
fread(buf, 1, sizeof(buf), bin);
6. 性能优化
对于大文件:
// 设置更大的缓冲区
char io_buffer[8192];
setvbuf(file, io_buffer, _IOFBF, sizeof(io_buffer));while (fgets(line, sizeof(line), file)) {// 处理行
}
五、与 Linux 系统特性结合
1. 从管道读取
// Shell: echo "Hello" | ./program
fgets(buffer, 100, stdin);
2. 从设备文件读取
// 读取系统信息
FILE *meminfo = fopen("/proc/meminfo", "r");
if (meminfo) {fgets(buffer, 100, meminfo);fclose(meminfo);
}
3. 非阻塞 I/O
int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
FILE *file = fdopen(fd, "r");// 尝试读取
if (fgets(buffer, 100, file) == NULL) {if (errno == EAGAIN) {printf("数据暂不可用\n");}
}
六、常见错误与解决方案
错误1:忽略返回值
// 错误做法
fgets(buffer, size, file);// 正确做法
if (fgets(buffer, size, file) == NULL) {// 处理错误或EOF
}
错误2:缓冲区大小错误
char buf[10];
fgets(buf, 10, stdin); // 正确:最多读9字符+1个\0// 危险做法
fgets(buf, 11, stdin); // 缓冲区溢出风险!
错误3:未处理换行符
char name[50];
fgets(name, sizeof(name), stdin);
printf("你好, %s!", name); // 输出 "你好, Alice\n!"// 正确做法
name[strcspn(name, "\n")] = '\0';
错误4:错误处理 EOF
// 错误:使用feof()判断循环
while (!feof(file)) {fgets(line, size, file); // 最后一次读取会失败// ...
}// 正确做法
while (fgets(line, size, file)) {// ...
}
七、替代方案比较
1. vs scanf()
// scanf() 问题:
char buf[10];
scanf("%s", buf); // 可能溢出,不读取空格// fgets() 更安全:
fgets(buf, sizeof(buf), stdin);
2. vs getline()
(POSIX 标准)
// getline() 自动分配内存
char *line = NULL;
size_t len = 0;
ssize_t read;while ((read = getline(&line, &len, file)) != -1) {// 处理行
}
free(line);
3. vs read()
系统调用
// 低级读取
char buf[100];
ssize_t bytes = read(fd, buf, sizeof(buf)-1);
if (bytes > 0) {buf[bytes] = '\0'; // 需要手动添加终止符
}
八、总结
fgets()
是 Linux C 编程中安全读取文本的核心工具,关键要点:
- 安全第一:防止缓冲区溢出
- 换行符处理:保留换行符,需要时手动移除
- 错误处理:必须检查返回值和错误状态
- EOF 处理:正确区分错误和文件结束
- 流处理:支持标准输入、文件和管道
- 文本专用:不适合二进制文件
fwrite()
函数
fwrite()
是 C 语言中用于高效写入二进制数据的核心函数,特别适合处理结构化数据和大型文件。在 Linux 系统编程中,它是高性能文件操作的关键工具。
一、函数声明与核心概念
1. 函数声明
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
2. 作用
将内存中的数据块直接写入文件流,特别适合处理:
- 二进制文件(图像、音频、视频)
- 结构化数据(数组、结构体)
- 大数据集(数据库、日志文件)
3. 返回值
- 成功:返回实际写入的数据项数量(不是字节数)
- 失败:返回值小于
nmemb
参数 - 0:当写入失败时返回 0
二、参数详解与使用示例
1. const void *ptr
- 数据源指针
作用:指向要写入数据的内存地址
要求:
- 可以是任何类型的数据指针
- 数据在内存中必须是连续的
- 对于非连续数据,需要先组合到缓冲区
示例:
// 写入整数数组
int numbers[] = {1, 2, 3, 4, 5};
fwrite(numbers, sizeof(int), 5, file);// 写入结构体
struct Point { double x, y; };
struct Point p = {3.14, 2.71};
fwrite(&p, sizeof(struct Point), 1, file);
2. size_t size
- 单个数据项大小
作用:指定每个数据项的字节大小
最佳实践:
- 使用
sizeof
运算符确保准确性 - 对于基本类型:
sizeof(int)
,sizeof(double)
等 - 对于结构体:
sizeof(struct_name)
示例:
// 正确使用 sizeof
float data[100];
fwrite(data, sizeof(float), 100, file);// 错误示例(硬编码大小)
double values[50];
fwrite(values, 8, 50, file); // 错误!double 大小可能不是8字节
3. size_t nmemb
- 数据项数量
作用:指定要写入的数据项数量
注意事项:
- 与
size
参数共同决定总写入字节数(size * nmemb) - 实际写入量可能小于请求量(磁盘空间不足或错误)
示例:
// 写入100个结构体
struct Record {int id;char name[50];float balance;
};
struct Record records[100];
size_t written = fwrite(records, sizeof(struct Record), 100, file);if (written < 100) {// 处理未完全写入的情况
}
4. FILE *stream
- 文件流指针
作用:指定要写入的文件流
要求:
- 必须通过
fopen()
打开的文件指针 - 文件必须以可写模式打开(如 “w”, “wb”, “a”, “r+” 等)
示例:
FILE *file = fopen("data.bin", "wb"); // 二进制写入模式
if (file == NULL) {perror("文件打开失败");exit(EXIT_FAILURE);
}char buffer[1024];
size_t written = fwrite(buffer, 1, 1024, file);
三、完整使用示例
示例1:创建二进制数据文件
#include <stdio.h>
#include <stdlib.h>int main() {FILE *file = fopen("dataset.bin", "wb");if (!file) {perror("无法创建文件");return 1;}// 生成并写入数据const int num_points = 1000;double *points = malloc(num_points * sizeof(double));if (!points) {perror("内存分配失败");fclose(file);return 1;}for (int i = 0; i < num_points; i++) {points[i] = i * 0.1;}size_t written = fwrite(points, sizeof(double), num_points, file);if (written != num_points) {fprintf(stderr, "写入错误:请求 %d 项,实际写入 %zu 项\n",num_points, written);}free(points);fclose(file);return 0;
}
示例2:写入结构化数据文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>typedef struct {int id;char name[50];double salary;char department[30];
} Employee;int main() {Employee employees[] = {{101, "Alice Smith", 85000.0, "Engineering"},{102, "Bob Johnson", 75000.0, "Marketing"},{103, "Charlie Brown", 90000.0, "Engineering"}};FILE *file = fopen("employees.dat", "wb");if (!file) {perror("无法打开员工文件");return 1;}// 写入记录数量int count = sizeof(employees) / sizeof(Employee);if (fwrite(&count, sizeof(int), 1, file) != 1) {perror("写入记录数量失败");fclose(file);return 1;}// 写入所有员工记录size_t written = fwrite(employees, sizeof(Employee), count, file);if (written != count) {fprintf(stderr, "警告:请求 %d 条记录,实际写入 %zu 条\n",count, written);}fclose(file);printf("成功写入 %zu 条员工记录\n", written);return 0;
}
示例3:高效写入大文件
#include <stdio.h>
#include <stdlib.h>
#include <time.h>#define CHUNK_SIZE (1024 * 1024) // 1MBint main() {FILE *file = fopen("largefile.bin", "wb");if (!file) {perror("无法创建大文件");return 1;}// 设置大缓冲区提高性能setvbuf(file, NULL, _IOFBF, 16 * 1024 * 1024); // 16MB缓冲区unsigned char *buffer = malloc(CHUNK_SIZE);if (!buffer) {perror("内存分配失败");fclose(file);return 1;}srand(time(NULL));size_t total_written = 0;const size_t target_size = 1024 * 1024 * 100; // 100MBwhile (total_written < target_size) {// 生成随机数据for (size_t i = 0; i < CHUNK_SIZE; i++) {buffer[i] = rand() % 256;}size_t to_write = (target_size - total_written < CHUNK_SIZE) ? (target_size - total_written) : CHUNK_SIZE;size_t written = fwrite(buffer, 1, to_write, file);total_written += written;if (written < to_write) {perror("写入不完整");break;}printf("已写入: %.2f MB\r", (double)total_written / (1024 * 1024));fflush(stdout);}free(buffer);fclose(file);printf("\n写入完成,总计 %.2f MB\n", (double)total_written / (1024 * 1024));return 0;
}
四、关键注意事项与使用细节
1. 二进制模式 vs 文本模式
- 二进制模式(“wb”):
- 数据按原样写入,无任何转换
- 适合图像、音频、视频等二进制文件
- 文本模式(“w”):
- Linux 中与二进制模式相同(无换行符转换)
- Windows 中会转换换行符(
\n
→\r\n
)
建议:处理非文本文件时始终使用二进制模式
2. 错误处理最佳实践
size_t items = fwrite(data, size, nmemb, file);if (items < nmemb) {if (ferror(file)) {perror("写入错误");// 处理具体错误}if (feof(file)) {// 在写入模式下通常不会发生,但应检查}
}
3. 数据对齐优化
对于高性能应用,确保数据对齐:
// 使用对齐分配
double *data = aligned_alloc(64, 1024 * sizeof(double)); // 64字节对齐
fwrite(data, sizeof(double), 1024, file);
4. 结构体写入的特殊考虑
struct Data {int a;double b;
};// 潜在问题:结构体可能有填充字节
fwrite(&data, sizeof(struct Data), 1, file);// 解决方案:序列化每个字段
fwrite(&data.a, sizeof(int), 1, file);
fwrite(&data.b, sizeof(double), 1, file);
5. 与 fread()
配对使用
写入和读取应使用相同的参数:
// 写入
int numbers[] = {1, 2, 3, 4, 5};
fwrite(numbers, sizeof(int), 5, file);// 读取(必须匹配)
int read_nums[5];
fread(read_nums, sizeof(int), 5, file);
6. 大文件处理技巧
使用 ftell()
和 fseek()
处理大文件:
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
rewind(file);// 计算需要写入的块数
size_t chunks = file_size / CHUNK_SIZE;
size_t remainder = file_size % CHUNK_SIZE;
五、性能优化技巧
1. 设置缓冲区大小
FILE *file = fopen("data.bin", "wb");
char custom_buffer[16 * 1024 * 1024]; // 16MB// 设置自定义缓冲区
setvbuf(file, custom_buffer, _IOFBF, sizeof(custom_buffer));
2. 直接 I/O(绕过内核缓存)
file = fopen("fast.dat", "wb");
posix_fadvise(fileno(file), 0, 0, POSIX_FADV_SEQUENTIAL);
3. 内存映射文件
对于超大文件写入,考虑使用 mmap
:
int fd = open("huge.dat", O_RDWR | O_CREAT, 0644);
ftruncate(fd, file_size); // 设置文件大小
void *map = mmap(NULL, file_size, PROT_WRITE, MAP_SHARED, fd, 0);// 直接写入内存
memcpy(map, data, data_size);
msync(map, data_size, MS_SYNC); // 确保写入磁盘
六、常见错误与解决方案
错误1:忽略返回值
// 错误做法
fwrite(data, size, nmemb, file);// 正确做法
size_t written = fwrite(data, size, nmemb, file);
if (written < nmemb) {// 处理部分写入或错误
}
错误2:大小计算错误
// 错误:混淆大小和数量
int data[100];
fwrite(data, 100, sizeof(int), file); // 参数顺序错误!// 正确
fwrite(data, sizeof(int), 100, file);
错误3:字节序问题
// 跨平台问题
uint32_t value = 0x12345678;
fwrite(&value, sizeof(uint32_t), 1, file);// 需要转换字节序
value = htonl(value); // 主机字节序转网络字节序
错误4:未刷新缓冲区
fwrite(data, size, nmemb, file);
// 程序崩溃或断电可能导致数据丢失// 解决方案
fflush(file); // 强制刷新缓冲区
fsync(fileno(file)); // 强制写入磁盘(Linux)
七、高级应用场景
1. 写入管道
FILE *pipe = popen("gzip > data.gz", "w");
if (pipe) {fwrite(data, 1, data_size, pipe);pclose(pipe);
}
2. 写入共享内存
int shm_fd = shm_open("/myshm", O_CREAT | O_RDWR, 0644);
ftruncate(shm_fd, SHM_SIZE);
void *shm_ptr = mmap(NULL, SHM_SIZE, PROT_WRITE, MAP_SHARED, shm_fd, 0);// 使用fwrite写入共享内存
FILE *shm_file = fdopen(shm_fd, "wb");
fwrite(data, 1, data_size, shm_file);
3. 加密写入
FILE *file = fopen("encrypted.dat", "wb");
unsigned char buffer[4096];while (data_remaining) {// 加密数据块encrypt_chunk(buffer, current_data);if (fwrite(buffer, 1, sizeof(buffer), file) != sizeof(buffer)) {perror("加密写入失败");break;}
}
八、总结
fwrite()
是 Linux C 程序中高效文件写入的核心函数,关键要点:
- 参数顺序:
ptr, size, nmemb, stream
- 返回值:实际写入的完整项数(非字节数)
- 错误处理:必须检查返回值并配合
ferror()
- 二进制模式:非文本文件必须使用 “wb”
- 缓冲区管理:使用大缓冲区提高性能
- 大文件处理:分块写入避免内存不足
- 数据对齐:优化对齐提高写入速度
- 字节序:跨平台时注意字节序转换
fputs()
函数
fputs()
是 C 语言中用于高效写入字符串的核心函数,特别适合处理文本数据和日志记录。在 Linux 系统编程中,它是比 fprintf()
更轻量级的字符串输出工具。
一、函数声明与核心概念
1. 函数声明
int fputs(const char *s, FILE *stream);
2. 作用
- 将字符串写入文件流
- 不添加换行符(与
puts()
不同) - 比
fprintf()
更高效(无格式解析开销)
3. 返回值
- 成功:返回非负整数(通常为 0 或正数)
- 失败:返回
EOF
(-1)
二、参数详解与使用示例
1. const char *s
- 字符串指针
作用:要写入的以 \0
结尾的字符串
要求:
- 必须是有效的 C 字符串(以 null 结尾)
- 可以是字符串字面量或字符数组
- 内容中可包含换行符(需手动添加)
示例:
// 字符串字面量
fputs("Hello, Linux!\n", stdout);// 字符数组
char message[] = "System message\n";
fputs(message, logfile);// 动态字符串
char *dynamic_str = malloc(100);
strcpy(dynamic_str, "Dynamic content\n");
fputs(dynamic_str, file);
2. FILE *stream
- 文件流指针
作用:指定写入目标
常用值:
stdout
:标准输出stderr
:标准错误- 文件指针:通过
fopen()
打开的文件
示例:
// 写入标准输出
fputs("Console output\n", stdout);// 写入错误流
fputs("Critical error!\n", stderr);// 写入文件
FILE *log = fopen("app.log", "a");
fputs("Log entry\n", log);
三、完整使用示例
示例1:基本字符串写入
#include <stdio.h>int main() {// 写入标准输出fputs("=== System Information ===\n", stdout);// 写入文件FILE *info = fopen("system.txt", "w");if (!info) {fputs("无法创建系统信息文件\n", stderr);return 1;}fputs("Operating System: Linux\n", info);fputs("Kernel Version: 5.15.0\n", info);fclose(info);return 0;
}
示例2:高效日志系统
#include <stdio.h>
#include <time.h>
#include <string.h>void log_message(FILE *stream, const char *level, const char *msg) {time_t now = time(NULL);char timestamp[20];strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&now));// 高效拼接日志信息fputs("[", stream);fputs(timestamp, stream);fputs("] [", stream);fputs(level, stream);fputs("] ", stream);fputs(msg, stream);fputs("\n", stream);// 立即刷新确保日志实时写入fflush(stream);
}int main() {FILE *logfile = fopen("application.log", "a");if (!logfile) {fputs("无法打开日志文件\n", stderr);return 1;}log_message(stdout, "INFO", "应用程序启动");log_message(logfile, "DEBUG", "初始化模块");// 模拟错误if (1) { // 假设发生错误log_message(stderr, "ERROR", "配置文件损坏");log_message(logfile, "ERROR", "配置文件损坏");}log_message(logfile, "INFO", "应用程序关闭");fclose(logfile);return 0;
}
示例3:文件复制工具
#include <stdio.h>
#include <stdlib.h>#define BUFFER_SIZE 4096int main(int argc, char *argv[]) {if (argc != 3) {fputs("用法: fcopy <源文件> <目标文件>\n", stderr);return 1;}FILE *src = fopen(argv[1], "r");if (!src) {fputs("无法打开源文件\n", stderr);return 1;}FILE *dest = fopen(argv[2], "w");if (!dest) {fputs("无法创建目标文件\n", stderr);fclose(src);return 1;}char buffer[BUFFER_SIZE];while (fgets(buffer, sizeof(buffer), src)) {// 使用fputs写入读取的行if (fputs(buffer, dest) == EOF) {fputs("写入文件失败\n", stderr);break;}}if (ferror(src)) {fputs("读取源文件时出错\n", stderr);}fclose(src);fclose(dest);return 0;
}
四、关键注意事项与使用细节
1. 换行符处理
fputs()
不会自动添加换行符:
// 错误:输出将连在一起
fputs("Line 1", file);
fputs("Line 2", file);
// 结果: "Line1Line2"// 正确:手动添加换行
fputs("Line 1\n", file);
fputs("Line 2\n", file);
2. 错误处理
必须检查返回值:
if (fputs("重要数据", file) == EOF) {if (ferror(file)) {perror("写入失败");// 处理具体错误}
}
3. 性能优化
对于大量字符串输出:
// 设置大缓冲区
setvbuf(file, NULL, _IOFBF, 16384); // 16KB缓冲区// 批量写入
const char *lines[] = {"Line1\n", "Line2\n", "Line3\n"};
for (int i = 0; i < 3; i++) {fputs(lines[i], file);
}
// 而不是多次调用fflush
4. 二进制模式注意事项
在 Linux 中,文本模式和二进制模式无区别:
// Linux下两者行为相同
fputs("Text\n", file); // 文本模式
fputs("Binary\n", file); // 二进制模式
5. 特殊字符处理
fputs()
会原样输出所有字符:
fputs("包含\0空字符的文本", file);
// 文件内容: "包含" + 空字符 + "空字符的文本"
// 注意:空字符会截断字符串显示,但实际数据完整
五、与 Linux 系统特性结合
1. 写入系统日志
#include <syslog.h>// 替代方案:直接使用syslog
openlog("myapp", LOG_PID, LOG_USER);
syslog(LOG_INFO, "系统事件发生");
closelog();
2. 写入命名管道
mkfifo("my_pipe", 0666);
FILE *pipe = fopen("my_pipe", "w");
fputs("管道数据\n", pipe);
fclose(pipe);
3. 写入 /dev/null
FILE *null = fopen("/dev/null", "w");
fputs("这些内容会被丢弃\n", null);
fclose(null);
六、常见错误与解决方案
错误1:忘记换行符
fputs("没有换行", stdout);
fputs("的文字会连在一起", stdout);
// 输出: "没有换行的文字会连在一起"
解决方案:
fputs("正确添加换行\n", stdout);
错误2:使用未初始化的指针
char *uninit_ptr; // 未初始化
fputs(uninit_ptr, stdout); // 未定义行为!
解决方案:
char *safe_ptr = "有效字符串";
fputs(safe_ptr, stdout);
错误3:写入只读文件
FILE *file = fopen("/etc/passwd", "w"); // 权限不足
if (file) {fputs("尝试写入", file); // 会失败
}
解决方案:
FILE *file = fopen("userfile.txt", "w"); // 用户可写位置
if (file) {if (fputs("内容", file) == EOF) {perror("写入失败");}
}
错误4:缓冲区未刷新
fputs("重要日志", logfile);
// 程序崩溃,日志可能丢失
解决方案:
fputs("安全日志", logfile);
fflush(logfile); // 确保立即写入
七、与相关函数对比
1. vs fprintf()
// fputs更高效
fputs("简单字符串\n", file);// fprintf更灵活但开销大
fprintf(file, "格式化: %d %.2f\n", 42, 3.14);
2. vs puts()
// puts自动添加换行
puts("自动换行"); // 输出后加\n// fputs需手动控制
fputs("手动换行\n", stdout);
3. vs fwrite()
// fwrite处理二进制数据
fwrite(data, 1, size, file);// fputs处理文本字符串
fputs("文本内容", file);
八、高级应用场景
1. 构建简单 HTTP 响应
void send_http_response(FILE *client) {fputs("HTTP/1.1 200 OK\r\n", client);fputs("Content-Type: text/html\r\n", client);fputs("\r\n", client); // 空行分隔头部和主体fputs("<html><body>Hello World</body></html>", client);
}
2. 生成 CSV 文件
void write_csv_row(FILE *file, const char *col1, const char *col2, int col3) {fputs("\"", file);fputs(col1, file);fputs("\",\"", file);fputs(col2, file);fputs("\",", file);// 转换数字char num_buf[20];snprintf(num_buf, sizeof(num_buf), "%d", col3);fputs(num_buf, file);fputs("\n", file);
}
3. 实现简单命令行界面
void display_menu() {fputs("\n==== 系统菜单 ====\n", stdout);fputs("1. 查看状态\n", stdout);fputs("2. 配置设置\n", stdout);fputs("3. 退出系统\n", stdout);fputs("选择: ", stdout);fflush(stdout);
}
九、总结
fputs()
是 Linux C 编程中高效字符串输出的核心工具,关键要点:
- 轻量高效:无格式解析开销,性能优于
fprintf
- 精确控制:不自动添加换行符,需手动管理
- 错误处理:必须检查返回值和错误状态
- 适用场景:纯文本输出、日志系统、协议实现
- 性能优化:配合缓冲区设置提高吞吐量
- 系统集成:支持标准流、文件和特殊设备
fscanf()
函数
fscanf()
是 C 语言中用于从文件流读取格式化数据的核心函数,特别适合处理结构化文本数据。在 Linux 系统编程中,它是解析配置文件、日志文件和用户输入的强大工具。
一、函数声明与核心概念
1. 函数声明
int fscanf(FILE *stream, const char *format, ...);
2. 作用
- 从文件流中读取格式化输入
- 解析文本数据为指定类型(整数、浮点数、字符串等)
- 支持复杂格式匹配和跳过不需要的数据
3. 返回值
- 成功:返回成功匹配和赋值的输入项数量
- 失败:返回
EOF
(-1)或匹配项数量小于预期 - 0:当没有项匹配时返回 0
二、参数详解与使用示例
1. FILE *stream
- 文件流指针
作用:指定要读取的文件流
要求:
- 必须通过
fopen()
打开的文件指针 - 文件必须以可读模式打开(如 “r”, “r+”, “rb” 等)
- 可以是标准输入流
stdin
示例:
// 从标准输入读取
int age;
fscanf(stdin, "%d", &age);// 从文件读取
FILE *config = fopen("settings.cfg", "r");
float value;
fscanf(config, "%f", &value);
2. const char *format
- 格式字符串
作用:控制输入解析的格式,包含:
- 普通字符:必须精确匹配输入中的字符
- 格式说明符:以
%
开头,指定数据类型 - 空白字符:匹配任意空白(空格、制表符、换行)
格式说明符语法:
%[*][宽度][长度]类型
常用类型说明符:
说明符 | 类型 | 示例输入 |
---|---|---|
%d | int | “42” → 42 |
%i | int(自动检测进制) | “0x2A” → 42 |
%u | unsigned int | “42” → 42 |
%f | float | “3.14” → 3.14 |
%lf | double | “3.14” → 3.14 |
%c | char | “A” → ‘A’ |
%s | 字符串 | “Hello” |
%[...] | 字符集匹配 | “[A-Z]” |
%n | 已读取字符数 | 不消耗输入 |
%% | 百分号字符 | “%” |
特殊修饰符:
*
:赋值抑制(读取但不存储)宽度
:指定最大读取字符数长度
:h
(short)、l
(long)、ll
(long long)
三、完整使用示例
示例1:基本数据类型读取
#include <stdio.h>int main() {FILE *file = fopen("data.txt", "r");if (!file) {perror("无法打开文件");return 1;}int id;char name[50];float score;// 解析格式:ID 姓名 分数int result = fscanf(file, "%d %49s %f", &id, name, &score);if (result == 3) {printf("ID: %d, 姓名: %s, 分数: %.2f\n", id, name, score);} else if (result == EOF) {printf("文件结束\n");} else {printf("解析失败,只匹配了 %d 项\n", result);}fclose(file);return 0;
}
示例2:复杂格式解析(日志文件)
#include <stdio.h>
#include <string.h>int main() {FILE *log = fopen("access.log", "r");if (!log) {perror("无法打开日志文件");return 1;}char ip[16];char timestamp[20];char method[10];char url[100];int status;int size;while (1) {// 解析格式:IP [时间] "方法 URL" 状态 大小int result = fscanf(log, "%15s - - [%19[^]]] \"%9s %99[^\"]\" %d %d",ip, timestamp, method, url, &status, &size);if (result == EOF) break;if (result == 6) {printf("%s %s %s %s %d %d\n", ip, timestamp, method, url, status, size);} else {// 跳过无效行char buffer[256];fgets(buffer, sizeof(buffer), log);}}fclose(log);return 0;
}
示例3:高级格式控制
#include <stdio.h>int main() {// 输入示例: "Product: ABC-123, Price: $19.99, Stock: 50"char product[10];int code;float price;int stock;char currency;// 使用 * 跳过不需要的部分int result = scanf("Product: %9[^-]-%d, Price: %*c%f, Stock: %d",product, &code, &price, &stock);if (result == 4) {printf("产品: %s-%d, 价格: %.2f, 库存: %d\n", product, code, price, stock);}return 0;
}
四、关键注意事项与使用细节
1. 缓冲区溢出防护
// 危险:无长度限制
char name[20];
fscanf(file, "%s", name); // 可能溢出!// 安全:指定最大宽度
fscanf(file, "%19s", name); // 读取最多19字符
2. 空白字符处理
fscanf()
会跳过前导空白字符:
// 输入 " 42" 也能正确解析
int num;
fscanf(stdin, "%d", &num);
3. 输入流残留问题
当部分匹配失败时:
// 输入 "abc123"
int num;
if (fscanf(stdin, "%d", &num) != 1) {// 清除残留输入int c;while ((c = getchar()) != '\n' && c != EOF);
}
4. 文件结束检测
while (1) {int value;int result = fscanf(file, "%d", &value);if (result == EOF) {if (feof(file)) {printf("文件结束\n");} else if (ferror(file)) {perror("读取错误");}break;} else if (result == 1) {// 处理值} else {// 跳过无效数据fgetc(file);}
}
5. 字符集匹配
// 只读取大写字母
char uppercase[20];
fscanf(file, "%19[A-Z]", uppercase);// 读取除逗号外的所有字符
char field[50];
fscanf(file, "%49[^,]", field);
6. 使用 %n
跟踪位置
int pos1, pos2;
char str[100];fscanf(file, "%s%n %*d%n", str, &pos1, &pos2);
printf("字符串结束位置: %d, 数字结束位置: %d\n", pos1, pos2);
五、高级应用场景
1. 解析 CSV 文件
#include <stdio.h>
#include <string.h>#define MAX_LINE 1024int main() {FILE *csv = fopen("data.csv", "r");if (!csv) return 1;char line[MAX_LINE];while (fgets(line, sizeof(line), csv)) {// 替换换行符line[strcspn(line, "\n")] = '\0';char *ptr = line;char field[100];int field_count = 0;while (*ptr) {// 处理引号包围的字段if (*ptr == '"') {int len;ptr++; // 跳过开头的引号sscanf(ptr, "%99[^\"]%n", field, &len);ptr += len;if (*ptr == '"') ptr++; // 跳过结尾的引号} else {sscanf(ptr, "%99[^,\n]%n", field, &len);ptr += len;}printf("字段 %d: %s\n", ++field_count, field);if (*ptr == ',') ptr++;}}fclose(csv);return 0;
}
2. 读取配置文件
#include <stdio.h>
#include <string.h>
#include <ctype.h>int main() {FILE *config = fopen("app.conf", "r");if (!config) return 1;char line[256];while (fgets(line, sizeof(line), config)) {// 跳过注释和空行if (line[0] == '#' || line[0] == '\n') continue;char key[50], value[100];if (sscanf(line, "%49s = %99[^\n]", key, value) == 2) {printf("配置项: %s = %s\n", key, value);}}fclose(config);return 0;
}
3. 解析二进制文件头
#include <stdio.h>
#include <stdint.h>typedef struct {char magic[4];uint32_t version;uint64_t timestamp;uint16_t checksum;
} FileHeader;int main() {FILE *bin = fopen("data.bin", "rb");if (!bin) return 1;FileHeader header;if (fread(&header, sizeof(FileHeader), 1, bin) != 1) {perror("读取文件头失败");fclose(bin);return 1;}// 验证魔数if (memcmp(header.magic, "BIN", 3) != 0) {fprintf(stderr, "无效文件格式\n");fclose(bin);return 1;}printf("版本: %u\n", header.version);printf("时间戳: %lu\n", header.timestamp);printf("校验和: %u\n", header.checksum);fclose(bin);return 0;
}
六、常见错误与解决方案
错误1:忘记地址运算符 &
int num;
fscanf(file, "%d", num); // 错误!缺少 &
解决方案:
fscanf(file, "%d", &num); // 正确
错误2:缓冲区溢出
char city[20];
fscanf(file, "%s", city); // 可能溢出
解决方案:
fscanf(file, "%19s", city); // 安全
错误3:格式字符串不匹配
// 输入 "42.5"
int num;
fscanf(file, "%d", &num); // 匹配失败
解决方案:
float value;
fscanf(file, "%f", &value); // 正确
错误4:未处理返回值
fscanf(file, "%d", &num); // 忽略返回值
解决方案:
if (fscanf(file, "%d", &num) != 1) {// 处理错误
}
七、性能优化技巧
1. 批量读取
// 一次读取多个值
int values[100];
for (int i = 0; i < 100; i++) {if (fscanf(file, "%d", &values[i]) != 1) {break;}
}
2. 使用 fgets()
+ sscanf()
char line[256];
while (fgets(line, sizeof(line), file)) {int a, b;if (sscanf(line, "%d %d", &a, &b) == 2) {// 处理值}
}
3. 内存映射文件
#include <sys/mman.h>
#include <fcntl.h>int fd = open("large.txt", O_RDONLY);
size_t size = lseek(fd, 0, SEEK_END);
char *map = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);char *ptr = map;
int count = 0;
while (ptr < map + size) {int num, n;if (sscanf(ptr, "%d%n", &num, &n) == 1) {count++;ptr += n;} else {ptr++;}
}munmap(map, size);
close(fd);
八、替代方案比较
1. vs fgets()
+ sscanf()
// fscanf 直接读取
int num;
fscanf(file, "%d", &num);// fgets + sscanf
char line[100];
fgets(line, sizeof(line), file);
sscanf(line, "%d", &num);
2. vs getline()
+ sscanf()
// POSIX getline
char *line = NULL;
size_t len = 0;
ssize_t read;while ((read = getline(&line, &len, file)) != -1) {int a, b;sscanf(line, "%d %d", &a, &b);
}
free(line);
3. vs 正则表达式
// 使用正则表达式库(更强大但更重)
#include <regex.h>regex_t regex;
regcomp(®ex, "([0-9]+) ([A-Z]+)", REG_EXTENDED);
regmatch_t matches[3];if (regexec(®ex, line, 3, matches, 0) == 0) {char num_str[20];strncpy(num_str, line + matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so);int num = atoi(num_str);
}
九、总结
fscanf()
是 Linux C 编程中强大的格式化输入工具,关键要点:
- 格式控制:使用
%
格式说明符精确控制输入解析 - 安全防护:始终指定字符串读取的最大宽度
- 错误处理:必须检查返回值并处理部分匹配
- 高级特性:利用
%[]
、%n
和*
实现复杂解析 - 性能优化:对于大文件,考虑
fgets()
+sscanf()
组合 - 二进制文件:使用
fread()
替代二进制数据读取