Linux 软件编程 - IO 编程

一、IO 核心概念:理解 "一切皆文件"

在 Linux 中,IO 操作的本质是对 "文件" 的操作 ------ 这里的 "文件" 不仅包括我们日常接触的文本 / 二进制文件,还涵盖了设备(键盘、鼠标、磁盘)、通信对象(管道、套接字)等。所有这些 "文件" 都通过统一的文件描述符或流进行管理,实现了 "屏蔽底层差异,统一操作接口" 的目标。

1.1 常见文件类型分类

不同类型的 "文件" 对应不同的 IO 场景,其标识和用途如下表所示:

文件标识 类型名称 核心用途 典型示例
b 块设备文件 按 "块" 读写,用于存储设备 硬盘、U 盘、分区
c 字符设备文件 按 "字符" 读写,用于交互设备 键盘、鼠标、终端(tty)
d 目录文件 存储文件 / 子目录的索引信息 /home、/etc 目录
- 普通文件 存储用户数据 文本文件(.txt)、二进制文件(.exe)
l 链接文件 指向其他文件的 "快捷方式" 软链接(ln -s 创建)
s 套接字文件 进程间网络通信 网络服务端 / 客户端通信
p 管道文件 本地进程间通信 匿名管道( )、命名管道

二、IO 接口分类:按需选择合适的操作方式

根据操作对象的不同,Linux IO 接口分为三类,核心差异在于是否有缓存 和适用场景:

接口类型 操作对象 缓存特性 核心用途
标准 IO 普通文件(文本 / 二进制) 有缓存(高效) 日常文件读写(如配置文件、日志)
文件 IO 设备文件、通信文件(管道) 无缓存(实时) 硬件操作、实时数据交互
目录 IO 目录文件 无缓存 目录创建、删除、遍历(如 ls 功能)

三、标准 IO 详解:从基础到核心接口

标准 IO 由 C 标准库(<stdio.h>)提供,基于 "流(FILE*)" 实现对文件的操作,支持缓存管理、格式化读写等功能,适用于绝大多数普通文件场景。

3.1 标准 IO 的核心前提

1. 头文件

使用标准 IO 必须包含头文件:

复制代码
#include <stdio.h>
2. 普通文件的两种形式
  • ASCII 码文件 :内容由可显示的 ASCII 字符组成(如代码、文本),可通过cat直接查看;
  • 二进制文件:内容是二进制数据(如图片、音视频、压缩包),直接查看会显示乱码;
  • 注意:ASCII 码文件是特殊的二进制文件(仅包含 0~127 的 ASCII 值)。
3. 默认打开的 3 个流

程序启动时,系统会自动打开 3 个标准流,无需手动fopen:

  • stdin:标准输入流(对应键盘),行缓存;
  • stdout:标准输出流(对应终端),行缓存;
  • stderr:标准错误流(对应终端),无缓存(错误信息需实时输出)。

3.2 标准 IO 的缓存机制

缓存是标准 IO 的核心优化 ------ 通过在内存中开辟缓存区,批量读写数据,减少与磁盘 / 终端的直接交互次数。标准 IO 的缓存分为三类:

缓存类型 缓存大小 刷新条件(数据写入目标) 适用场景
全缓存 4KB(默认) 1. 缓存区满;2. 调用fclose/ 程序结束;3. 手动fflush 普通文件(如.log)
行缓存 1KB(默认) 1. 缓存区满;2. 遇到\n;3. 调用fclose/ 程序结束;4. 手动fflush 终端交互(stdin/stdout)
不缓存 0KB 无缓存,数据直接写入目标 错误输出(stderr)

示例 :用printf("Hello")打印时,若未加\n,stdout(行缓存)不会立即输出;加上\n或调用fflush(stdout),数据才会显示到终端。

3.3 标准 IO 核心函数接口

标准 IO 提供了一套完整的文件操作函数,从 "打开 - 读写 - 关闭" 到 "定位 - 刷新",覆盖所有常见场景。以下是最常用的函数详解:

1. 文件打开:fopen
  • 原型 :FILE *fopen(const char *pathname, const char *mode);
  • 功能:打开指定路径的文件,并创建一个 "流(FILE*)" 用于后续操作;
  • 参数 :
    • pathname:文件路径(如./test.txt、/home/user/data.bin);
    • mode:打开模式(决定读写权限和文件不存在时的行为),常见模式如下:
mode 读写权限 文件不存在时 文件存在时 适用场景
r 只读 报错(NULL) 打开文件 读取配置文件
r+ 读写 报错(NULL) 打开文件(不清空) 读写已存在的文件
w 只写 创建文件 清空文件内容 新建日志文件(覆盖旧内容)
w+ 读写 创建文件 清空文件内容 新建并读写文件
a 追加只写 创建文件 指针定位到文件尾 追加日志(不覆盖旧内容)
a+ 追加读写 创建文件 指针定位到文件尾 追加并读取日志
  • 返回值 :成功返回FILE*流指针;失败返回NULL(需用perror查看错误原因)。
2. 文件关闭:fclose
  • 原型 :int fclose(FILE *stream);
  • 功能 :关闭流,释放缓存和文件资源(必须调用,避免内存泄漏和数据丢失);
  • 参数 :stream:fopen返回的流指针;
  • 返回值 :成功返回 0;失败返回EOF(-1)。
3. 字符读写:fgetc / fputc

适用于逐字符读写(如统计文件字符数)。

  • fgetc(读字符):

    • 原型:int fgetc(FILE *stream);
    • 功能:从流中读取一个字符(返回 ASCII 码值);
    • 返回值:成功返回字符 ASCII 码;失败 / 文件末尾返回EOF(-1);
    • 等价:getchar() ≡ fgetc(stdin)(从键盘读字符)。
  • fputc(写字符):

    • 原型:int fputc(int c, FILE *stream);
    • 功能:将字符c(ASCII 码)写入流;
    • 返回值:成功返回字符c;失败返回EOF;
    • 等价:putchar(c) ≡ fputc(c, stdout)(向终端写字符)。
4. 字符串读写:fgets / fputs

适用于逐行读写(如读取配置文件的行内容)。

  • fgets(读字符串):

    • 原型:char *fgets(char *s, int size, FILE *stream);

    • 功能:从流中读取最多size-1个字符(留 1 个位置存\0),遇到\n或EOF停止;

    • 关键特性:会保留输入中的\n(若一行未读完,下次继续读);

    • 示例(模拟gets功能,去掉\n):

      复制代码
      char buf[128];
      fgets(buf, sizeof(buf), stdin);
      buf[strlen(buf)-1] = '\0'; // 替换末尾的\n为字符串结束符
  • fputs(写字符串):

    • 原型:int fputs(const char *s, FILE *stream);
    • 功能:将字符串s写入流(不自动添加\n);
    • 对比:puts(s)会自动在末尾加\n,而fputs不会。
5. 格式化读写:fscanf / fprintf

适用于结构化数据读写(如读写 "姓名 + 年龄" 这类格式化信息)。

  • fprintf(格式化写):

    • 原型:int fprintf(FILE *stream, const char *format, ...);

    • 功能:按format格式将数据写入流(类似printf,但输出到文件而非终端);

    • 示例:向test.txt写入用户信息:

      复制代码
      FILE *fp = fopen("test.txt", "w");
      fprintf(fp, "Name: %s, Age: %d\n", "ZhangSan", 20); // 写入文件
      fclose(fp);
  • fscanf(格式化读):

    • 原型:int fscanf(FILE *stream, const char *format, ...);

    • 功能:按format格式从流中读取数据(类似scanf,但输入来自文件而非键盘);

    • 返回值:成功返回匹配的参数个数;失败 / EOF 返回EOF;

    • 示例:从test.txt读取用户信息:

      复制代码
      char name[32];
      int age;
      FILE *fp = fopen("test.txt", "r");
      fscanf(fp, "Name: %s, Age: %d", name, &age); // 读取数据
      printf("Read: %s, %d\n", name, age); // 输出:ZhangSan, 20
      fclose(fp);
6. 文件定位:fseek / rewind / ftell

用于调整文件读写指针的位置(如 "跳转到文件第 100 字节处读写")。

  • fseek:设置指针位置,原型:int fseek(FILE *stream, long offset, int whence);

    • offset:偏移量(正数向后移,负数向前移);
    • whence:基准位置:SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾);
    • 示例:跳转到文件开头向后 10 字节处:fseek(fp, 10, SEEK_SET);
  • rewind:将指针重置到文件开头,等价于fseek(fp, 0, SEEK_SET);

  • ftell:获取当前指针相对于文件开头的偏移量(字节数),原型:long ftell(FILE *stream);

四、标准 IO 实践案例

案例 1:文件拷贝(将 A 文件内容复制到 B 文件)

需求:从终端接收两个文件路径(源文件 A、目标文件 B),将 A 的内容拷贝到 B。

复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char src_path[128] = {0}; // 源文件路径
    char dest_path[128] = {0}; // 目标文件路径
    FILE *src_fp = NULL, *dest_fp = NULL;
    int ch;

    // 1. 从终端获取文件路径
    printf("请输入源文件路径:");
    fgets(src_path, sizeof(src_path), stdin);
    src_path[strlen(src_path)-1] = '\0'; // 去掉fgets保留的\n

    printf("请输入目标文件路径:");
    fgets(dest_path, sizeof(dest_path), stdin);
    dest_path[strlen(dest_path)-1] = '\0';

    // 2. 打开源文件(只读)和目标文件(只写,不存在则创建)
    src_fp = fopen(src_path, "r");
    if (src_fp == NULL) {
        perror("打开源文件失败");
        return -1;
    }

    dest_fp = fopen(dest_path, "w");
    if (dest_fp == NULL) {
        perror("打开目标文件失败");
        fclose(src_fp); // 避免内存泄漏
        return -1;
    }

    // 3. 逐字符拷贝:读源文件→写目标文件
    while ((ch = fgetc(src_fp)) != EOF) {
        fputc(ch, dest_fp);
    }

    // 4. 关闭文件
    fclose(src_fp);
    fclose(dest_fp);
    printf("文件拷贝完成!\n");

    return 0;
}

案例 2:统计文件中出现次数最多的字符

需求:从终端接收文件路径,统计文件中所有 ASCII 字符的出现次数,并输出出现最多的字符及其次数。

复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char file_path[128] = {0};
    FILE *fp = NULL;
    int ch;
    int count[128] = {0}; // 索引0~127对应ASCII码,值为出现次数
    int max_count = 0;
    char max_char = 0;
    int i;

    // 1. 获取文件路径
    printf("请输入文件路径:");
    fgets(file_path, sizeof(file_path), stdin);
    file_path[strlen(file_path)-1] = '\0';

    // 2. 打开文件
    fp = fopen(file_path, "r");
    if (fp == NULL) {
        perror("打开文件失败");
        return -1;
    }

    // 3. 统计字符出现次数
    while ((ch = fgetc(fp)) != EOF) {
        if (ch >= 0 && ch < 128) { // 只统计标准ASCII字符
            count[ch]++;
        }
    }

    // 4. 找到出现次数最多的字符
    for (i = 0; i < 128; i++) {
        if (count[i] > max_count) {
            max_count = count[i];
            max_char = (char)i;
        }
    }

    // 5. 输出结果
    printf("出现次数最多的字符:'%c'(ASCII:%d)\n", max_char, (int)max_char);
    printf("出现次数:%d\n", max_count);

    // 6. 关闭文件
    fclose(fp);
    return 0;
}
相关推荐
@兽血沸腾17 分钟前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
且从容.3 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达6 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.7 小时前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落8 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨8 小时前
WSL2的安装与使用
linux·wsl
微小的鱼8 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng9 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮9 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频