一、 核心概念:什么是标准 IO 与“流”?

1. I/O 与内存参考点

  • I/O (Input/Output):输入输出,是以计算机的内存为参考点的。

    • 如果从内存中把数据发送出去(写到文件、显示到屏幕),称为输出 (Output)。

    • 如果从外部把数据写入内存(读键盘、读文件),称为输入 (Input)。

  • 在 Linux 操作系统当中,一切皆文件,I/O 都是对文件的操作。键盘是标准输入设备(/dev/input,对应文件描述符 0),显示器是标准输出设备(对应 1)。

2. 标准 IO 是什么?

  • 标准 IO 是 C 标准库的一部分。任何支持标准 C 的系统都可以使用标准 IO 来实现文件存储,具有极好的跨平台兼容性。

  • 底层关系:标准 IO 在 UNIX/Linux 上,其实是对文件 IO(系统调用,如 open, read, write)的封装。它通过内部维护一个结构体(即 FILE 结构体)来管理计算机上所有的文件,并提供了缓冲机制,效率更高。

3. 关键概念:流 (Stream)

在 C 语言中,操作文件用的都是 FILE *fp(文件流指针)。为什么叫“流”?

  • 流:数据从文件当中流入和流出所体现出来的字节流,就叫做流。

  • 文本流 (Text Stream):数据以字符的形式组成,通常带有换行符处理(如在某些系统中会将 \n 自动转换为 \r\n)。

  • 二进制流 (Binary Stream):数据按原样(字节)传输,不需要任何转换。图片、音频、视频等必须使用二进制流。


二、 文件操作的“三步曲”与核心函数

文件操作必须遵循:打开文件 -> 读写操作 -> 关闭文件 (释放资源)。

1. 打开与关闭

c

FILE *fopen(const char *path, const char *mode); // 成功返回指针,失败返回 NULL
int fclose(FILE *stream);                        // 成功返回 0,失败返回 EOF
  • 常用 mode:

    • r:只读(文件不存在报错)。

    • w:只写(不存在则创建,存在则清 0 覆盖)。

    • a:追加写(在文件末尾追加)。

    • 加上 + 表示可读可写,加上 b 表示二进制模式(Linux 下 b 和 r/w 无区别,但在 Windows 下不同)。

2. 按字符读写 fgetc / fputc

c

int fputc(int c, FILE *stream); // 成功返回写入的字符 ASCII,失败返回 EOF
int fgetc(FILE *stream);        // 成功返回读到的 ASCII,读到末尾返回 EOF

【注意】:fgetc 的返回值必须是 int,千万别用 char,否则在判断 EOF(通常是 -1)时容易引发死循环!

3. 按字符串读写 fgets / fputs

c

int fputs(const char *s, FILE *stream);
char *fgets(char *s, int size, FILE *stream);

【注意】:fgets 遇到换行符 \n 或读够 size-1 个字符就会停止,并且它会把换行符 \n 也存入你的字符串中!

4. 自定义大小读写(二进制安全)fread / fwrite

处理结构体或大段二进制数据时专用:

c

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

三、 实战代码演练

以下是我今天跑通的代码实战,按难度递增整理:

实战1:体验命令行参数 (argc 与 argv)

c

#include <stdio.h>
// 运行方式: ./a.out 1.txt 2.txt 3 4 5 (此时 argc = 6)
int main(int argc, char *argv[])
{
    printf("argc:%d\n", argc);
    for(int i = 0; i < argc; i++) {
        printf("i:%d %s\n", i, argv[i]);
    }
    return 0;
}

实战2:最基础的打开、写入与关闭

c

#include <stdio.h>

int main(int argc, char **argv)
{
    FILE* fp = fopen("1.txt", "w"); // "w"模式会清空原文件
    if(NULL == fp) {
        printf("fopen error\n");
        return 1;
    }
    fputc('o', fp);
    fputc('k', fp);
    fclose(fp);
    return 0;
}

实战3:单字符读取文本

c

#include <stdio.h>

int main(int argc, char **argv)
{
    FILE* fp = fopen("1.txt", "r");
    if(NULL == fp) return 1;

    while(1) {
        int c = fgetc(fp);
        if(EOF == c) break; // 判断是否读到文件末尾
        printf("%c", c);
    }
    printf("\n");
    fclose(fp);
    return 0;
}

实战4:单字符实现文件拷贝(文本与二进制通用)

c

#include <stdio.h>

int main(int argc, char **argv)
{
    FILE* srcfp = fopen("/home/linux/1.png", "r");
    FILE* dstfp = fopen("2.png", "w");
    if(NULL == srcfp || NULL == dstfp) {
        printf("fopen src or dst error\n");
        return 1;
    }

    while(1) {
        int c = fgetc(srcfp);
        if(EOF == c) break;
        fputc(c, dstfp);
    }
    fclose(srcfp);
    fclose(dstfp);
    return 0;
}

实战5:按字符串读写(体会中文字符截断)

c

#include <stdio.h>
int main(int argc, char **argv)
{
    FILE* fp = fopen("1.txt", "w");
    if(NULL == fp) return 1;

    fputs("how are you\n", fp);
    char buf[256] = "你好\n"; // UTF-8编码下,一个汉字通常占3个字节
    fputs(buf, fp);
    fclose(fp);
    return 0;
}

【测试读取】:

c

#include <stdio.h>
int main(int argc, char **argv)
{
    FILE *fp = fopen("1.txt", "r");
    if (NULL == fp) return 1;

    while (1) {
        char s[256] = {0}; // 每次循环都初始化为0,防止脏数据
        // 注意这里 size = 5,说明每次最多读 4 个字符(留1个给 '\0')
        char *ret = fgets(s, 5, fp); 
        if (NULL == ret) break;
        printf("%s", s);
    }
    fclose(fp);
    return 0;
}

实战6:综合应用 - 手写一个简易的 Linux cat 命令

c

#include <stdio.h>

int main(int argc, char *argv[])
{
    if(argc < 2) {
        printf("usage: ./a.out filename\n");
        return 1;
    }
    FILE* fp = fopen(argv[1], "r");
    if(NULL == fp) return 1;

    while(1) {
        char buf[256] = {0};
        // 一次性读一整行(最多255字符)
        if(NULL == fgets(buf, sizeof(buf), fp)) break;
        printf("%s", buf);
    }
    fclose(fp);
    return 0;
}

四、 新手易错点

在练习编写一个“统计文件中字母出现次数”的代码时,我踩了无数个雷。大家引以为戒:

[错误1] sizeof(指针) 误以为是 malloc 的大小

c

// 错误代码
char *n = malloc(256 * sizeof(char));
ret = fgets(n, sizeof(n), fp); 

崩溃现场:我以为 sizeof(n) 会返回 256,结果 n 是个指针,64位系统上 sizeof(n) 永远是 8!导致 fgets 每次最多只能读 7 个字符,读取大文件时直接乱套。
正确做法:宏定义容量,如 #define BUF_SIZE 256,明确传参。

[错误2] 逻辑判断 || 写成了全选

c

// 错误代码
if('a' <= ret || ret <= 'z') {
    i = ret - 'a';
    s[i]++; // 数组越界!
}

崩溃现场:数学里表示区间是 a <= x <= z,但在 C 语言里必须用 &&。我写成了 ||,导致不管读进来的是大写字母、数字还是空格,条件永远为真!传入空格的 ASCII 码时,数组下标直接变成负数,程序报 Segmentation fault。
正确做法:if (ret >= 'a' && ret <= 'z')。

[错误3] malloc 放在了 while 循环里

c

// 错误代码
while(1) {
    char *s = malloc(5 * sizeof(char)); 
    // 下次循环又重新 malloc,上一块内存永久丢失!
}
free(s); // 循环外根本找不到 s 了

崩溃现场:内存泄漏。如果文件很大,程序跑着跑着内存就爆了。而且作用域错误,循环外根本 free 不掉。
正确做法:在循环外申请内存,循环内复用(或者在循环内用完立即 free)。

[错误4] 循环内数组没清空导致的“脏数据”

如果在 while 循环里使用了 char buf[256] = {0}; 没问题。但如果你把数组定义在循环外,读取较短的字符串后,残留的旧数据可能会影响下一次打印。
正确做法:每次读取前,使用 memset(buf, 0, sizeof(buf)) 清空缓冲区。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐