C/C++ Linux 开发教程(GCC + CMake) / 10 — 文件 I/O 与预处理器
文件 I/O 与预处理器
1. 文件操作基础
C 标准库通过 <stdio.h> 提供了一套完整的文件 I/O 函数,以 FILE * 为核心抽象。
1.1 fopen / fclose
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
// 打开文件用于写入
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("fopen");
return 1;
}
fprintf(fp, "Hello, File I/O!\n");
fprintf(fp, "Line 2: %d + %d = %d\n", 3, 5, 3 + 5);
fclose(fp);
// 打开文件用于读取
fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
1.2 文件打开模式
| 模式 | 说明 | 文件不存在 | 已有文件 |
|---|---|---|---|
"r" | 只读 | 失败 | 从头读 |
"w" | 只写 | 创建 | 清空 |
"a" | 追加 | 创建 | 追加到末尾 |
"r+" | 读写 | 失败 | 从头读写 |
"w+" | 读写 | 创建 | 清空 |
"a+" | 读追加 | 创建 | 从头读,追加写 |
"rb" | 二进制只读 | 失败 | 从头读 |
"wb" | 二进制只写 | 创建 | 清空 |
⚠️ 注意: "w" 模式会清空已有文件内容。如果只想追加数据,使用 "a" 模式。
2. 标准流
每个 C 程序启动时自动打开三个标准流:
| 流 | 类型 | 说明 | 默认关联 |
|---|---|---|---|
stdin | FILE * | 标准输入 | 键盘 |
stdout | FILE * | 标准输出 | 终端(行缓冲) |
stderr | FILE * | 标准错误 | 终端(无缓冲) |
#include <stdio.h>
int main(void)
{
// printf 等价于 fprintf(stdout, ...)
fprintf(stdout, "这是标准输出\n");
// 错误信息输出到 stderr
fprintf(stderr, "这是错误输出\n");
// 从 stdin 读取
char name[64];
fprintf(stdout, "请输入名字: ");
if (fgets(name, sizeof(name), stdin)) {
fprintf(stdout, "你好, %s", name);
}
// 输出重定向演示
// ./prog > output.txt ← stdout 重定向到文件
// ./prog 2> error.txt ← stderr 重定向到文件
// ./prog > out.txt 2>&1 ← stdout 和 stderr 都重定向
return 0;
}
3. 读写文本文件
3.1 fprintf / fscanf
#include <stdio.h>
typedef struct {
char name[32];
int age;
double height;
} Person;
int main(void)
{
// 写入
FILE *fp = fopen("people.txt", "w");
if (!fp) return 1;
Person people[] = {
{"Alice", 30, 1.65},
{"Bob", 25, 1.80},
{"Charlie", 35, 1.72}
};
int n = sizeof(people) / sizeof(people[0]);
fprintf(fp, "%d\n", n);
for (int i = 0; i < n; i++) {
fprintf(fp, "%s %d %.2f\n", people[i].name, people[i].age, people[i].height);
}
fclose(fp);
// 读取
fp = fopen("people.txt", "r");
if (!fp) return 1;
int count;
fscanf(fp, "%d", &count);
for (int i = 0; i < count; i++) {
Person p;
fscanf(fp, "%31s %d %lf", p.name, &p.age, &p.height);
printf("%-10s %3d岁 %.2f米\n", p.name, p.age, p.height);
}
fclose(fp);
return 0;
}
3.2 fgets / fputs
#include <stdio.h>
int main(void)
{
// fgets —— 安全地读取一行
FILE *fp = fopen("test.txt", "r");
if (!fp) return 1;
char line[256];
int line_num = 1;
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%3d: %s", line_num++, line);
// fgets 保留换行符,如果需要可以手动去除
}
fclose(fp);
return 0;
}
| 函数 | 说明 | 安全性 |
|---|---|---|
fgets(buf, n, fp) | 最多读 n-1 个字符 | ✅ 安全 |
gets(buf) | 读取一行(已废弃) | ❌ 缓冲区溢出风险 |
fputs(str, fp) | 写入字符串(不含换行) | ✅ 安全 |
puts(str) | 写入字符串并添加换行到 stdout | ✅ 安全 |
💡 提示: 永远不要使用
gets(),它已被 C11 标准移除。
4. 二进制文件读写
4.1 fread / fwrite
#include <stdio.h>
#include <string.h>
typedef struct {
char name[32];
int id;
float score;
} Student;
int main(void)
{
// 写入二进制文件
Student students[] = {
{"Alice", 1001, 85.5f},
{"Bob", 1002, 92.0f},
{"Charlie", 1003, 78.5f}
};
int count = sizeof(students) / sizeof(students[0]);
FILE *fp = fopen("students.dat", "wb");
if (!fp) return 1;
// 写入记录数量
fwrite(&count, sizeof(int), 1, fp);
// 写入所有记录
fwrite(students, sizeof(Student), count, fp);
fclose(fp);
// 读取二进制文件
fp = fopen("students.dat", "rb");
if (!fp) return 1;
int read_count;
fread(&read_count, sizeof(int), 1, fp);
Student *buf = malloc(read_count * sizeof(Student));
fread(buf, sizeof(Student), read_count, fp);
for (int i = 0; i < read_count; i++) {
printf("%-10s ID:%-5d %.1f\n", buf[i].name, buf[i].id, buf[i].score);
}
free(buf);
fclose(fp);
return 0;
}
| 函数 | 原型 | 说明 |
|---|---|---|
fread(buf, size, count, fp) | size_t fread(...) | 读取 count 个 size 字节的元素 |
fwrite(buf, size, count, fp) | size_t fwrite(...) | 写入 count 个 size 字节的元素 |
5. 文件定位(fseek / ftell)
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("test.txt", "w+");
if (!fp) return 1;
// 写入数据
fprintf(fp, "ABCDEFGHIJ");
// 获取当前位置
long pos = ftell(fp);
printf("写入后位置: %ld\n", pos); // 10
// 回到文件开头
fseek(fp, 0, SEEK_SET);
pos = ftell(fp);
printf("SEEK_SET 后: %ld\n", pos); // 0
// 移到第 5 个字节
fseek(fp, 5, SEEK_SET);
char c = fgetc(fp);
printf("第 5 个字节: '%c'\n", c); // 'F'
// 从当前位置偏移 2
fseek(fp, 2, SEEK_CUR);
c = fgetc(fp);
printf("偏移 2 后: '%c'\n", c); // 'I'
// 从末尾往前 3 个字节
fseek(fp, -3, SEEK_END);
c = fgetc(fp);
printf("末尾前 3: '%c'\n", c); // 'H'
// 获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
printf("文件大小: %ld 字节\n", size); // 10
fclose(fp);
return 0;
}
| 函数 | 说明 |
|---|---|
fseek(fp, offset, origin) | 移动文件位置指示器 |
ftell(fp) | 返回当前位置 |
rewind(fp) | 等价于 fseek(fp, 0, SEEK_SET) |
SEEK_SET | 文件开头 |
SEEK_CUR | 当前位置 |
SEEK_END | 文件末尾 |
6. 缓冲区控制
6.1 缓冲模式
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 全缓冲 | 缓冲区满时才刷新 | 文件 I/O |
| 行缓冲 | 遇到换行符刷新 | stdout(连接终端时) |
| 无缓冲 | 立即写入 | stderr |
6.2 setvbuf / fflush
#include <stdio.h>
int main(void)
{
FILE *fp = fopen("buffered.txt", "w");
if (!fp) return 1;
// 设置全缓冲,8KB 缓冲区
char buf[8192];
setvbuf(fp, buf, _IOFBF, sizeof(buf));
fprintf(fp, "This goes to buffer\n");
// 手动刷新缓冲区
fflush(fp); // 数据从缓冲区写入文件
fprintf(fp, "More data\n");
fclose(fp); // fclose 也会自动刷新
return 0;
}
| 函数 | 说明 |
|---|---|
setvbuf(fp, buf, mode, size) | 设置缓冲区和模式 |
fflush(fp) | 刷新缓冲区(fflush(NULL) 刷新所有流) |
_IOFBF | 全缓冲 |
_IOLBF | 行缓冲 |
_IONBF | 无缓冲 |
7. 预处理器
预处理器在编译前对源代码进行文本替换和条件选择。
7.1 #define 宏定义
#include <stdio.h>
// 对象宏
#define PI 3.14159265358979
#define MAX_SIZE 100
#define TRUE 1
#define FALSE 0
// 函数宏
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
// 多行宏
#define PRINT_ARRAY(arr, n) do { \
for (int _i = 0; _i < (n); _i++) \
printf("%d ", (arr)[_i]); \
printf("\n"); \
} while (0)
int main(void)
{
printf("PI = %f\n", PI);
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("MAX(3, 7) = %d\n", MAX(3, 7));
int arr[] = {1, 2, 3, 4, 5};
PRINT_ARRAY(arr, 5);
return 0;
}
⚠️ 注意: 函数宏的每个参数都要加括号,整个表达式也要加括号,否则可能因运算符优先级导致错误:
// 错误示例
#define BAD_SQUARE(x) x * x
// BAD_SQUARE(2+3) 展开为 2+3*2+3 = 11,而非 25
7.2 字符串化 # 与连接 ##
#include <stdio.h>
// 字符串化运算符 #
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)
// 标记连接运算符 ##
#define MAKE_VAR(name, id) name##_##id
#define DECLARE_PAIR(type, name) \
type name##_x; \
type name##_y
int main(void)
{
// 字符串化
printf("字符串化: %s\n", STRINGIFY(Hello World));
int age = 25;
PRINT_VAR(age); // 展开为 printf("age" " = %d\n", age);
// 标记连接
int MAKE_VAR(counter, 1) = 10;
int MAKE_VAR(counter, 2) = 20;
printf("counter_1 = %d, counter_2 = %d\n", counter_1, counter_2);
// 声明一对变量
DECLARE_PAIR(double, point);
point_x = 1.0;
point_y = 2.0;
printf("point = (%.1f, %.1f)\n", point_x, point_y);
return 0;
}
8. 条件编译
8.1 #ifdef / #ifndef / #endif
#include <stdio.h>
#define DEBUG 1
#define PLATFORM_LINUX
int main(void)
{
#ifdef DEBUG
printf("[DEBUG] 调试模式已启用\n");
#endif
#ifdef PLATFORM_LINUX
printf("运行在 Linux 平台\n");
#elif defined(PLATFORM_WINDOWS)
printf("运行在 Windows 平台\n");
#else
printf("未知平台\n");
#endif
#ifndef RELEASE
printf("非发布版本\n");
#endif
return 0;
}
8.2 #if / #elif / #else
#include <stdio.h>
#define VERSION 3
int main(void)
{
#if VERSION >= 3
printf("版本 3 或更高\n");
#elif VERSION >= 2
printf("版本 2\n");
#else
printf("版本 1\n");
#endif
// 检查编译器标准
#if __STDC_VERSION__ >= 201710L
printf("C17 或更高\n");
#elif __STDC_VERSION__ >= 201112L
printf("C11\n");
#elif __STDC_VERSION__ >= 199901L
printf("C99\n");
#else
printf("C89/C90\n");
#endif
return 0;
}
8.3 预定义宏
| 宏 | 说明 |
|---|---|
__FILE__ | 当前源文件名 |
__LINE__ | 当前行号 |
__func__ | 当前函数名(C99) |
__DATE__ | 编译日期 |
__TIME__ | 编译时间 |
__STDC__ | 是否符合 C 标准 |
__STDC_VERSION__ | C 标准版本号 |
#include <stdio.h>
#define DEBUG_LOG(fmt, ...) \
fprintf(stderr, "[%s:%d %s] " fmt "\n", \
__FILE__, __LINE__, __func__, ##__VA_ARGS__)
int main(void)
{
DEBUG_LOG("程序启动");
int x = 42;
DEBUG_LOG("x = %d", x);
return 0;
}
9. 头文件保护
9.1 传统方式(#ifndef)
myheader.h:
#ifndef MYHEADER_H
#define MYHEADER_H
typedef struct {
double x, y;
} Point;
double point_distance(Point a, Point b);
#endif
9.2 #pragma once
#pragma once
typedef struct {
double x, y;
} Point;
double point_distance(Point a, Point b);
| 方式 | 优点 | 缺点 |
|---|---|---|
#ifndef | C 标准,可移植 | 需要唯一宏名 |
#pragma once | 简洁,不易出错 | 非标准(但几乎所有编译器支持) |
💡 提示: 两者都很常用。在大型项目中,
#pragma once更简洁;需要严格标准兼容时用#ifndef。
10. #pragma 指令
// 控制对齐
#pragma pack(push, 1)
struct Packed {
char a;
int b;
};
#pragma pack(pop)
// 禁用特定警告(GCC)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused = 42;
#pragma GCC diagnostic pop
11. 实际场景:配置文件解析器
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_ENTRIES 64
#define MAX_LINE 256
typedef struct {
char key[64];
char value[128];
} ConfigEntry;
typedef struct {
ConfigEntry entries[MAX_ENTRIES];
int count;
} Config;
void config_init(Config *cfg) { cfg->count = 0; }
int config_load(Config *cfg, const char *filename)
{
FILE *fp = fopen(filename, "r");
if (!fp) return -1;
char line[MAX_LINE];
while (fgets(line, sizeof(line), fp) && cfg->count < MAX_ENTRIES) {
// 跳过空行和注释
if (line[0] == '#' || line[0] == '\n' || line[0] == '\0')
continue;
// 去除换行符
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
// 查找等号
char *eq = strchr(line, '=');
if (!eq) continue;
*eq = '\0';
const char *key = line;
const char *value = eq + 1;
// 去除首尾空格
while (*key == ' ' || *key == '\t') key++;
while (*value == ' ' || *value == '\t') value++;
strncpy(cfg->entries[cfg->count].key, key, 63);
strncpy(cfg->entries[cfg->count].value, value, 127);
cfg->count++;
}
fclose(fp);
return 0;
}
const char *config_get(const Config *cfg, const char *key)
{
for (int i = 0; i < cfg->count; i++) {
if (strcmp(cfg->entries[i].key, key) == 0) {
return cfg->entries[i].value;
}
}
return NULL;
}
int main(void)
{
// 创建示例配置文件
FILE *fp = fopen("app.conf", "w");
if (fp) {
fprintf(fp, "# 应用配置\n");
fprintf(fp, "host = localhost\n");
fprintf(fp, "port = 8080\n");
fprintf(fp, "debug = true\n");
fprintf(fp, "database = /var/lib/app/db.sqlite\n");
fclose(fp);
}
// 解析配置
Config cfg;
config_init(&cfg);
if (config_load(&cfg, "app.conf") != 0) {
fprintf(stderr, "加载配置失败\n");
return 1;
}
printf("已加载 %d 个配置项:\n", cfg.count);
for (int i = 0; i < cfg.count; i++) {
printf(" %s = %s\n", cfg.entries[i].key, cfg.entries[i].value);
}
// 查询配置
const char *host = config_get(&cfg, "host");
const char *port = config_get(&cfg, "port");
if (host && port) {
printf("\n服务器: %s:%s\n", host, port);
}
return 0;
}
12. 实际场景:简单日志系统
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <string.h>
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARN,
LOG_ERROR
} LogLevel;
static LogLevel g_level = LOG_INFO;
static FILE *g_logfile = NULL;
const char *log_level_str(LogLevel level)
{
switch (level) {
case LOG_DEBUG: return "DEBUG";
case LOG_INFO: return "INFO ";
case LOG_WARN: return "WARN ";
case LOG_ERROR: return "ERROR";
default: return "?????";
}
}
void log_set_level(LogLevel level) { g_level = level; }
void log_set_file(const char *path)
{
if (g_logfile) fclose(g_logfile);
g_logfile = fopen(path, "a");
}
void log_message(LogLevel level, const char *file, int line,
const char *fmt, ...)
{
if (level < g_level) return;
time_t now = time(NULL);
struct tm *t = localtime(&now);
char timebuf[32];
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", t);
// 格式化消息
char msg[512];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
// 输出到 stderr
fprintf(stderr, "[%s] [%s] %s:%d: %s\n",
timebuf, log_level_str(level), file, line, msg);
// 同时写入日志文件
if (g_logfile) {
fprintf(g_logfile, "[%s] [%s] %s:%d: %s\n",
timebuf, log_level_str(level), file, line, msg);
fflush(g_logfile);
}
}
// 便捷宏
#define LOG_DEBUG(fmt, ...) log_message(LOG_DEBUG, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) log_message(LOG_INFO, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) log_message(LOG_WARN, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) log_message(LOG_ERROR, __FILE__, __LINE__, fmt, ##__VA_ARGS__)
int main(void)
{
log_set_level(LOG_DEBUG);
log_set_file("app.log");
LOG_INFO("程序启动");
LOG_DEBUG("调试信息: x = %d", 42);
LOG_WARN("警告: 内存使用率 %d%%", 85);
LOG_ERROR("错误: 文件未找到 '%s'", "config.txt");
LOG_INFO("程序结束");
return 0;
}
编译运行:
gcc -Wall -std=c17 -o logger logger.c && ./logger
cat app.log
13. 实际场景:文件复制工具
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 8192
int copy_file(const char *src, const char *dst)
{
FILE *fin = fopen(src, "rb");
if (!fin) {
perror(src);
return -1;
}
FILE *fout = fopen(dst, "wb");
if (!fout) {
perror(dst);
fclose(fin);
return -1;
}
char buffer[BUFFER_SIZE];
size_t bytes;
long total = 0;
while ((bytes = fread(buffer, 1, BUFFER_SIZE, fin)) > 0) {
if (fwrite(buffer, 1, bytes, fout) != bytes) {
perror("fwrite");
fclose(fin);
fclose(fout);
return -1;
}
total += (long)bytes;
}
fclose(fin);
fclose(fout);
printf("已复制 %ld 字节: %s -> %s\n", total, src, dst);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "用法: %s <源文件> <目标文件>\n", argv[0]);
return 1;
}
return copy_file(argv[1], argv[2]) == 0 ? 0 : 1;
}