强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 程序启动时自动打开三个标准流:

类型说明默认关联
stdinFILE *标准输入键盘
stdoutFILE *标准输出终端(行缓冲)
stderrFILE *标准错误终端(无缓冲)
#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);
方式优点缺点
#ifndefC 标准,可移植需要唯一宏名
#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;
}

14. 扩展阅读