從 0 開始學習 Linux 系列之「13.標準 IO 庫」

StdIO.png

版權聲明:本文為 cdeveloper 原創(chuàng)文章,可以隨意轉載,但必須在明確位置注明出處!

標準 IO 庫

上一篇文章我們學習了 5 個底層的 IO 函數(shù),這次我們來學習標準的 IO 函數(shù),既然是標準那肯定在多個平臺都有使用到,例如 Linux,Windows...,上層應用用標準庫開發(fā)還是很常見的,因為它們可以跨平臺。

標準 IO 庫是由 Dennis Ritchie 在 1975 年左右寫的,后來被美國國際標準化組織(ISO)制定成了 C 語言的標準庫,稱為 ANSI C。因為現(xiàn)在所有的 UNIX 系統(tǒng)都提供 C 標準中定義的庫函數(shù),所以學習標準 C 庫非常重要。

這篇文章主要介紹一些常用的 C 庫函數(shù),例如 fopen, fclose, fread, fwrite...,不可能將全部的庫函數(shù)介紹完畢,你需要根據(jù)那本經(jīng)典的Unix 環(huán)境高級編程來深入的學習,一定掌握學習的方法:

  1. man 手冊
  2. glibc 官網(wǎng)
  3. Google

流和 FILE 對象

在學習標準庫之前,有兩個概念需要理解,我們的開發(fā)都是以它們?yōu)榛A

1. 文件流

當用標準庫打開或操作文件時,我們可以看作用一根水管與這個文件連接,里面流淌的是文件數(shù)據(jù)。文件流又分為下面 2 種:

  1. 文本流:傳輸文本
  2. 字節(jié)流:傳輸字節(jié)

他們主要有 3 點不同:

  1. 從文本流中讀取的數(shù)據(jù)被劃分成以換行 \n 字符終止的行,而二進制流只是一系列長的字符
  2. 在某些系統(tǒng)上,文本文件只能包含打印字符,水平制表符和換行符,因此文本流可能不支持其他字符,而二進制流可以處理任何字符值。
  3. 當文件再次讀入時,在文本流中的換行符之前立即寫入的空格字符可能會消失。更一般地說,不需要在從文本流中讀取或寫入文本流的字符與實際文件中的字符之間進行一對一映射。

注意:在 GNU C 庫和所有 POSIX 系統(tǒng)中,文本流和二進制流之間沒有區(qū)別,當您打開流時,無論是否要求二進制,都可以獲得相同的流,此流可以處理任何文件內(nèi)容,并且沒有文本流有時具有的限制。

文本流和字符流更加詳細的區(qū)別請參考這里

2. FILE 對象

我們用 FILE 對象來在用戶空間表示一個打開的文件,該對象是一個結構體,包含了標準 IO 庫管理當前文件流的信息。

例如,fopen 函數(shù)成功打開文件的操作:

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+: 可讀可寫,文件不存在就創(chuàng)建
  5. a: 在文件尾部追加內(nèi)容,如果文件不存在就創(chuàng)建
  6. a+:可讀可寫

其中 r = read,w = 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 組對應的輸入輸出字符函數(shù):

#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);

這些函數(shù)的參數(shù)都很好理解,我們以 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

想必你也發(fā)現(xiàn)了這樣一個一個字符的讀取和寫入效率肯定不高,那有沒有一次讀取或者寫入一行的函數(shù)呢?我們來看看行 IO。

文本流:行讀寫 IO

行 IO 每次讀取或者寫入文件中的一行,比單個字符的輸入輸出更加有效率,常用的函數(shù)有 2 組:

#include <stdio.h>

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

// gets 有緩存區(qū)溢出漏洞,建議棄用。
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 它很危險。

字節(jié)流:二進制 IO

前面我們每次傳輸?shù)亩际且粋€個的字符,如果我們要讀寫指定大小的結構,則我們需要知道一個結構用字符表示的大小等信息,非常麻煩。因此標準庫提供了二進制 IO 來讀寫指定數(shù)量的字節(jié),因為我們可以使用 sizeof(type) 來得到一個類型的字節(jié)大小,所以可以使用字節(jié)流方便的讀取和寫入數(shù)據(jù):

#include <stdio.h>

/* 
 * ptr: 存儲讀取內(nèi)容的指針
 * size: 每次讀取的數(shù)據(jù)大小
 * nmemb: 要讀取的數(shù)據(jù)個數(shù)或者次數(shù)
 * stream: 讀取的文件指針
 */
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

/*
 * ptr: 要寫入的數(shù)據(jù)指針
 * size: 每次寫入的數(shù)據(jù)大小
 * nmmeb: 寫入的數(shù)據(jù)個數(shù)或者次數(shù)
 * 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 可以也可以將文件讀入內(nèi)存或者寫入內(nèi)存文件到磁盤上。

文件流的定位

在底層 IO 我們可以使用 lseek 來移動文件指針,在標準 C 中我們也有 3 個非常常用的移動文件指針的函數(shù)

#include <stdio.h>

// 與 lseek 參數(shù)相同
int fseek(FILE *stream, long offset, int whence);

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

// 移動文件指針到開頭
void rewind(FILE *stream);

實例 5:file_length.c

我們來使用標準庫的文件定位函數(shù)來求一個文件的長度:

#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 文件的內(nèi)容是 hello,加上末尾的 '\0' 一共 6 個字節(jié):

./file_length 1.txt

file length = 6
cur pos = 0

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

格式化 IO

在標準庫中,我們可以使用 printf 的許多衍生函數(shù)來格式化輸出到文件,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 個函數(shù):

#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

可以看到我們成功在 stdout,buf,1.txt 中寫入了 Hello World 文本,實現(xiàn)了格式化輸出的功能。

結語

這次主要介紹了 C 標準庫的一些常用的函數(shù),掌握這些常用的函數(shù)即可應付大多數(shù)的工作,如果遇到不能實現(xiàn)的功能,一定要搜索 Google,因為 API 那么多不可能全部介紹完啊。就算我全部介紹完,你也不一定有耐心看下去是不,所以啊,最重要的是形成自己的學習方法,則比什么都重要。

最后,感謝你的閱讀,我們下次再見 :)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 一、溫故而知新 1. 內(nèi)存不夠怎么辦 內(nèi)存簡單分配策略的問題地址空間不隔離內(nèi)存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 8,123評論 0 27
  • C/C++輸入輸出流總結 前兩天寫C++實習作業(yè),突然發(fā)現(xiàn)I/O是那么的陌生,打了好長時間的文件都沒有打開,今天終...
    LuckTime閱讀 1,807評論 0 6
  • 文件操作 (Linux文件操作)) [文件|目錄] Linux文件操作:為了對文件和目錄進程處理,你需要用到系統(tǒng)...
    JamesPeng閱讀 1,606評論 1 5
  • 我想我是自卑的,我總是盡可能的只表現(xiàn)自己最燦爛的笑容給所有見到我的人,以至于所有朋友都誤以為我是個非常樂天非常開朗...
    MISS言甚閱讀 341評論 3 2
  • 2017年3月31日14:42 晴 ...
    心靈安歇處即為家閱讀 164評論 0 0

友情鏈接更多精彩內(nèi)容