从 0 开始学习 Linux 系列之「13.标准 IO 库」

StdIO.png

版权声明:本文为 cdeveloper 原创文章,可以随意转载,但必须在明确位置注明出处!

标准 IO 库

上一篇文章我们学习了 5 个底层的 IO 函数,这次我们来学习标准的 IO 函数,既然是标准那肯定在多个平台都有使用到,例如 Linux,Windows...,上层应用用标准库开发还是很常见的,因为它们可以跨平台。

标准 IO 库是由 Dennis Ritchie 在 1975 年左右写的,后来被美国国际标准化组织(ISO)制定成了 C 语言的标准库,称为 ANSI C。因为现在所有的 UNIX 系统都提供 C 标准中定义的库函数,所以学习标准 C 库非常重要。

这篇文章主要介绍一些常用的 C 库函数,例如 fopen, fclose, fread, fwrite...,不可能将全部的库函数介绍完毕,你需要根据那本经典的Unix 环境高级编程来深入的学习,一定掌握学习的方法:

  1. man 手册
  2. glibc 官网
  3. Google

流和 FILE 对象

在学习标准库之前,有两个概念需要理解,我们的开发都是以它们为基础

1. 文件流

当用标准库打开或操作文件时,我们可以看作用一根水管与这个文件连接,里面流淌的是文件数据。文件流又分为下面 2 种:

  1. 文本流:传输文本
  2. 字节流:传输字节

他们主要有 3 点不同:

  1. 从文本流中读取的数据被划分成以换行 \n 字符终止的行,而二进制流只是一系列长的字符
  2. 在某些系统上,文本文件只能包含打印字符,水平制表符和换行符,因此文本流可能不支持其他字符,而二进制流可以处理任何字符值。
  3. 当文件再次读入时,在文本流中的换行符之前立即写入的空格字符可能会消失。更一般地说,不需要在从文本流中读取或写入文本流的字符与实际文件中的字符之间进行一对一映射。

注意:在 GNU C 库和所有 POSIX 系统中,文本流和二进制流之间没有区别,当您打开流时,无论是否要求二进制,都可以获得相同的流,此流可以处理任何文件内容,并且没有文本流有时具有的限制。

文本流和字符流更加详细的区别请参考这里

2. FILE 对象

我们用 FILE 对象来在用户空间表示一个打开的文件,该对象是一个结构体,包含了标准 IO 库管理当前文件流的信息。

例如,fopen 函数成功打开文件的操作:

FILE *fp = fopen("1.txt", "r");

fopen & fclose

使用 fopenfclose 来打开和关闭一个文件,来分别看看它们的声明:

fopen

使用 fopen 来打开一个文件:

#include <stdio.h>

/*
 * path: 要打开的文件名称,必须时包含路径
 * mode: 文件的打开模式
 * return: 成功返回 FILE 指针,失败返回 NULL,并设置 errno
 */
FILE *fopen (const char *path, const char *mode);

其中文件的打开模式分为 6 种:

  1. r:只读,文件必须存在
  2. r+: 可读可写
  3. w: 可读可写,会清空文件
  4. w+: 可读可写,文件不存在就创建
  5. a: 在文件尾部追加内容,如果文件不存在就创建
  6. a+:可读可写

其中 r = readw = writea = append,意思分别是:读,写,追加

fclose

使用 fclose 来关闭一个文件:

#include <stdio.h>

/*
 * stream: 要关闭的文件指针
 * return: 成功返回 0,失败返回 EOF,并设置 errno
 */
int fclose(FILE *stream);

来看一个打开和关闭文件的例子。

实例 1: fopen_close.c

这个例子使用 fopenfclose 来打开和关闭一个文件 1.txt

/*
 * fopen_close.c
 * fun: test fopen and fclose function.
 */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: ./fopen_close 1.txt\n");
        exit(1);
    } 

    FILE *fp = fopen(argv[1], "r");

    if (NULL == fp) {
        printf("file open fail.\n");
        exit(1);
    } 

    printf("file open success.\n");

    fclose(fp);

    printf("file close success.\n");
    return 0;
}


gcc 编译:

gcc fopen_close.c -o fopen_close

运行,因为我们r 打开文件,所以必须先建立 1.txt 文件

./fopen_close 1.txt

file open success.
file close success.

文本流:单字符读写

在标准 IO 库中有下面 3 组对应的输入输出字符函数:

#include <stdio.h>

// 从文件中读取或写入一个字符
int getc(FILE *fp);
int putc(int c, FILE *fp);

// 从文件中读取或写入一个字符
int fgetc(FILE *fp);
int fputc(int c, FILE *fp);

// 从标准输入读取,或者输出到标准输出
int getchar(void);
int putchar(int c);

这些函数的参数都很好理解,我们以 fgetcfputc 为例:

实例 2:fget_putc.c

我们用 fgetcfputc拷贝一个文件:

/*
 * fgetc_putc.c
 * fun: using fgetc and fputc to copy file.
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 3)
    {
        printf("Usage: ./fgetc_putc 1.txt 1.txt.bak.\n");
        exit(1);
    }

    FILE *fp_in = fopen(argv[1], "r");
    FILE *fp_out= fopen(argv[2], "w+");

    if (NULL == fp_in || NULL == fp_out) {
        printf("file open failed\n");
        exit(1);
    }   

    char ch = 0;
    while ((ch = fgetc(fp_in)) != EOF)
        fputc(ch, fp_out);

    fclose(fp_in);
    fclose(fp_out);

    return 0;
}

编译:

gcc fgetc_putc.c -o copy_file

运行:

./copy_file 1.txt 1.txt.bak

想必你也发现了这样一个一个字符的读取和写入效率肯定不高,那有没有一次读取或者写入一行的函数呢?我们来看看行 IO。

文本流:行读写 IO

行 IO 每次读取或者写入文件中的一行,比单个字符的输入输出更加有效率,常用的函数有 2 组:

#include <stdio.h>

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

// gets 有缓存区溢出漏洞,建议弃用。
char *gets(char *buf);
int puts(const char *s);

实例 3:fgets_puts.c

这里例子中程序输出我们输入的每一行

/*
 * fgets_puts.c
 * fun: print stdin to stdout using fgets and fputs.
 */

#include <stdio.h>
#include <stdlib.h>

#define MAXSIZE 100

int main(void) {
    char buf[MAXSIZE] = { 0 };

    while (fgets(buf, MAXSIZE, stdin) != NULL)
        fputs(buf, stdout);

    return 0;
}

编译:

gcc fgets_puts.c -o fgets_puts 

运行:

./fgets_puts
hello world
hello world

记住,不要使用 gets 它很危险

字节流:二进制 IO

前面我们每次传输的都是一个个的字符,如果我们要读写指定大小的结构,则我们需要知道一个结构用字符表示的大小等信息,非常麻烦。因此标准库提供了二进制 IO 来读写指定数量的字节,因为我们可以使用 sizeof(type) 来得到一个类型的字节大小,所以可以使用字节流方便的读取和写入数据:

#include <stdio.h>

/* 
 * ptr: 存储读取内容的指针
 * size: 每次读取的数据大小
 * nmemb: 要读取的数据个数或者次数
 * stream: 读取的文件指针
 */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

/*
 * ptr: 要写入的数据指针
 * size: 每次写入的数据大小
 * nmmeb: 写入的数据个数或者次数
 * stream: 要写入的文件指针
 */
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

实例 4:fread_write.c

/*
 * fread_write.c
 * fun: write bin data to file and read, print.
 */

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: ./fread_write 1.txt.\n");
        exit(1);
    }

    // write
    FILE *fp = fopen(argv[1], "w+");
    int data[5] = { 1, 2, 3, 4, 5 };
    fwrite(data, sizeof(int), 5, fp);
    fclose(fp);

    // read
    fp = fopen(argv[1], "r");
    int data_bak[5] = { 0 };
    fread(data_bak, sizeof(int), 5, fp);
    fclose(fp);

    // show
    for (int i = 0; i < 5; ++i)
        printf("%d ", data_bak[i]);

    printf("\n");
    return 0;
}

使用二进制 IO 可以也可以将文件读入内存或者写入内存文件到磁盘上。

文件流的定位

在底层 IO 我们可以使用 lseek 来移动文件指针,在标准 C 中我们也有 3 个非常常用的移动文件指针的函数

#include <stdio.h>

// 与 lseek 参数相同
int fseek(FILE *stream, long offset, int whence);

// 返回当前文件指针的位置
long ftell(FILE *stream);

// 移动文件指针到开头
void rewind(FILE *stream);

实例 5:file_length.c

我们来使用标准库的文件定位函数来求一个文件的长度:

#include <stdio.h>

int main(int argc, char *argv[]) {

    FILE *fp = fopen(argv[1], "r");
    
    // 移动文件指针到文件末尾
    fseek(fp, 0, SEEK_END);

    // 求出文件长度
    long file_length = ftell(fp);

    printf("file length = %ld\n", file_length);

    // 定位到文件开头
    rewind(fp);

    // 查看当前文件指针位置
    printf("cur pos = %ld\n", ftell(fp));

    return 0;
}

编译:

gcc file_length.c -o file_length

运行,1.txt 文件的内容是 hello,加上末尾的 '\0' 一共 6 个字节:

./file_length 1.txt

file length = 6
cur pos = 0

成功求出了文件的长度 = 6 。

格式化 IO

在标准库中,我们可以使用 printf 的许多衍生函数来格式化输出到文件,buf 中等:

#include <stdio.h>

// 输出到 stdout
int printf(const char *format, ...);

// 输出到文件
int fprintf(FILE *stream, const char *format, ...);

// 输出到缓存 str 中
int sprintf(char *str, const char *format, ...);

实例 6:test_printf.c

我们来格式化打印一段文本,来练习上面的 3 个函数:

#include <stdio.h>

int main(int argc, char *argv[]) {
    // stdout
    printf("Hello World\n");

    // file
    FILE *fp = fopen(argv[1], "w+");
    fprintf(fp, "%s", "Hello World");
    fclose(fp);

    // buffer
    char buf[20] = { 0 };
    sprintf(buf, "%s", "Hello World");

    puts(buf);

    return 0;
}

编译:

gcc test_printf.c -o test_printf

运行:

./test_printf 1.txt
Hello World
Hello World

# 查看 1.txt
cat 1.txt
Hello World

可以看到我们成功在 stdoutbuf1.txt 中写入了 Hello World 文本,实现了格式化输出的功能。

结语

这次主要介绍了 C 标准库的一些常用的函数,掌握这些常用的函数即可应付大多数的工作,如果遇到不能实现的功能,一定要搜索 Google,因为 API 那么多不可能全部介绍完啊。就算我全部介绍完,你也不一定有耐心看下去是不,所以啊,最重要的是形成自己的学习方法,则比什么都重要。

最后,感谢你的阅读,我们下次再见 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,774评论 0 27
  • C/C++输入输出流总结 前两天写C++实习作业,突然发现I/O是那么的陌生,打了好长时间的文件都没有打开,今天终...
    LuckTime阅读 1,710评论 0 6
  • 文件操作 (Linux文件操作)) [文件|目录] Linux文件操作:为了对文件和目录进程处理,你需要用到系统...
    JamesPeng阅读 1,457评论 1 5
  • 2016-02-01 标准io 标准io处理了很多细节,例如缓存分配,优化长度执行io等。 流和file对象 之前...
    千里山南阅读 1,171评论 0 0
  • 我想我是自卑的,我总是尽可能的只表现自己最灿烂的笑容给所有见到我的人,以至于所有朋友都误以为我是个非常乐天非常开朗...
    MISS言甚阅读 314评论 3 2