# 叫人頭昏眼花的 stdio library 查閱 [Linux manual page](https://man7.org/linux/man-pages/man0/stdio.h.0p.html) 時會發現: 在 stdio.h 中有密密麻麻的函式/變數定義。有趣的是,筆者發現許多 C 語言的入門教材都沒有將**標準輸入輸出**交代仔細。讓大多數的人讀完文章還是對 stdio.h 內函式的設計感到一頭霧水,因此,筆者希望透過撰寫文章為大家解惑,也順便複習一下生疏的 C 語言技能。 ## 先備知識 在學習 stdio library 之前,需要先了解 Unix 作業系統的檔案觀念,在掌握檔案觀念後,可以幫助我們更快的理解 stdio 的設計巧思。 ## File descriptor ![](https://pic3.zhimg.com/80/v2-2d6dae0603b82fc2dce46fd1000a5182_720w.jpg) [stdio.h](https://man7.org/linux/man-pages/man0/stdio.h.0p.html) 中的函式幾乎都跟 **File** 的操作脫不了關係。 在 Unix 或是 Unix-like 中,作業系統抽象了一層資料結構用來存取 File, input / output 資源,該資料結構稱為 **File descriptor** 。並且, File descriptor 是屬於 POSIX API 的一部份,在符合 POSIX 的作業系統上,每一個 Process (除了守護程序)都應該具備至少三個 File descriptor ,分別是: | Integer value (>=0) | Name | <stdio.h> file stream | | -------- | -------- | -------- | | 0 | Standard input | stdin | | 1 | Standard output | stdout | | 2 | Standard error | stderr | ### 串流的概念 ![](https://miro.medium.com/max/1380/1*2jhvm-SF0qRQwg7V5-ac1Q.png) > Unix 提供許多開創產的進步,其中之一是提供抽象裝置 :它免除了程式須要知道或在意它正與哪個裝置溝通。 Unix 藉由資料串流的概念來消除這種複雜:一種資料位元組的有序序列,直到讀到檔案結尾。程式員亦可依需求寫入而無須宣告寫入多少或如何組織。 > -- [wikipedia](https://zh.wikipedia.org/wiki/%E6%A8%99%E6%BA%96%E4%B8%B2%E6%B5%81) ### 檔案結尾: EOF > 檔案結尾(英語:End of File,縮寫為 EOF ),是作業系統無法從資料來源讀取更多資料的情形。資料源通常為檔案或串流。 在 C 標準函式庫中,像 getchar 這樣的資料讀取函式返回一個與符號(巨集) EOF 相等的值來指明檔案結束的情況發生, EOF 的真實值與不同的平台有關(但通常是-1,比如在 glibc 中),並且不等於任何有效的字元代碼。塊讀取函式返回讀取的位元組數,如果它小於要求讀取的位元組數,就會出現一個檔案結束符。 -- [wikipedia](https://zh.wikipedia.org/zh-tw/%E6%AA%94%E6%A1%88%E7%B5%90%E5%B0%BE) ## 進入正題 建立起基礎觀念後,我們直接透過 Linux manual page 學習並掌握 stdlib library 的使用方法與技巧。 ### 字元的輸出與輸入 ```c= /* fgetc() and getc() function read a single character from the current stream position and advances the stream position to the next character. */ int fgetc(FILE *); int getc(FILE *); ``` `fgetc()` 與 `getc()` 可以從指定的 file descriptor 讀入一個字元,或是搭配 EOF 做讀檔: ```c= FILE *fp; fp = fopen(file_name, "r"); while((ch = fgetc(fp)) != EOF) // ... fclose(fp); ``` 使用 `fgetc()` 或是 `getc()` 後,讀取 file descriptor 的指標會往下一格移動: | status: | Before | | -------- | -------- | | 47 | <- pointer | | 50 | | | EOF | | | status: | After | | -------- | -------- | | 47 | | | 50 | <- pointer | | EOF | | :::info 在使用上, `getc()` 與 `fgetc()` 並無差異,他們最大的差別是實作方式,前者是使用 macro 定義,後者則是由函式實作。 ::: 此外,在 stdio.h 中還有一個函式有類似的功能: `getchar()` ,讓我們來看看它的定義: ```c= /* In Linux Man page: getchar() is equivalent to getc(stdin). */ int getchar(void); ``` 如果已經掌握 getc 以及 fgetc 的行為,看到 `getchar()` 的說明就可以知道該函式會從 stdin stream 中讀入一個字元。 談完輸入,讓我們接著看字元輸出的操作方法: ```c= int putc(int, FILE *); int fputc(int, FILE *); int putchar(int); ``` 字元輸出與字元輸入的用法基本上有 87% 像,唯一的差別是在每個函式中都多了一個 int 型別的參數,其目的也不難猜到: 將字元以 [ASCII Code](https://www.ascii-code.com/) 的形式寫到指定的 File descriptor 內。 至於 `putchar(int)` 則是將字元寫到 stdout 上。 ### 字串的輸入與輸出 在 C 語言中並沒有字串型別,若要存放字串,我們可以將其視為連續的字元做儲存: ```c= char *str = "ABC"; char str[] = "ABC"; char str[]={'A','B','C','\0'}; ``` 在讀檔時,如果使用者原本就打算讀取連續的字元,除了在執行 `getc()` 的迴圈判斷 EOF 和 `'\0'` 外, stdio.h 也定義了讀取字串的方法: ```c= char *gets(char *); char *fgets(char *restrict, int buf_size, FILE *restrict); ``` `gets()` 會連續讀取 stdin 的資料到 buffer 中直至讀到 EOF 或是換行符號,讀取結束時會在尾端加上 `'\0'` 做為字串的結尾。 `gets()` 可以無止盡的做讀取,它並不會檢查 buffer 的空間是否足夠。在設計不良的程式中, `gets()` 讀取的資料如果超過 buffer 大小,就會複寫到 stack 上,造成其他變數或是資料損壞。 雖然 `gets()` 與 `fgets()` 都無法得知 buffer 的大小,不過後者可以指定要讀取多大的資料。此外 `fgets()` 可以指定 File descriptor ,所以也可以被用來讀取檔案內容: ```c= /* https://stackoverflow.com/questions/3919009/how-to-read-from-stdin-with-fgets */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define BUFFERSIZE 10 int main() { char *text = calloc(1,1), buffer[BUFFERSIZE]; printf("Enter a message: \n"); while( fgets(buffer, BUFFERSIZE , stdin) ) /* break with ^D or ^Z */ { text = realloc( text, strlen(text) + 1 + strlen(buffer) ); if( !text ) ... /* error handling */ strcat( text, buffer ); /* note a '\n' is appended here everytime */ printf("%s\n", buffer); } printf("\ntext:\n%s",text); return 0; } ``` > 需要注意的是, buffer 的最後一個空間會被用來儲存 `'\0'` ,因此,假設我們要讀入 n 個字元, buffer 的大小應該為 n + 1 。 ### 資料的輸入與輸出 除了字元, C 語言還支援多種型態的資料,所以在 stdio.h 中除了定義了字元字串的輸入輸出方法,還定義了其他型態資料的輸入與輸出: ```c= int scanf(const char *restrict, ...); int fscanf(FILE *restrict, const char *restrict, ...); ``` `scanf()` 會從 stdin 讀取資料,並根據指定的 Format 存入 Buffer: ```c= scanf ("%d",&i); ``` 讓我們來分析上面的範例,第一個參數為資料的 Format ,上面所使用的 Format 為 **%d** ,關於它的說明也可以在 [Linux manual page](https://man7.org/linux/man-pages/man3/scanf.3.html) 上找到: > 1. Matches a literal '%'. That is, %% in the format string matches a single input '%' character. No conversion is done (but initial white space characters are discarded), and assignment does not occur. > 2. Matches an optionally signed decimal integer; the next pointer must be a pointer to int. 此外, `scanf()` 也允許我們一次讀取多筆資料: ```c= scanf("%d,%d,%d",&i,&j,&k); ``` 經過長時間的洗禮,相信各位看到 `fscanf()` 的參數就能猜到它的作用: > The fscanf() function shall read from the named input stream. 在介紹輸入後,相對的,肯定還有輸出的方法: ```c= int printf(const char *restrict, ...); int fprintf(FILE *restrict, const char *restrict, ...); ``` 對應前面的 `scanf()` 與 `fsacnf()` , `printf()` 會將緩衝區內的資料以指定的 Format 輸出到 stdout 上。而 `fprintf()` 可以讓使用者將 buffer 的資料輸出到指定的 File descriptor 。 ### Format 與 Modifier 除了剛剛提到的 `%d` , stdlib.h 還定義了許多 Format 以及 Modifier : ![](https://doembeddedprogram.files.wordpress.com/2018/12/typesizerangeformatspecifierexamplechar1byte-128to127c.jpg?w=900) > 圖片來源: [doembeddedprogram.wordpress.com](#) 。 > BTW: 比起看別人整理好的表格,筆者更推薦大家去查閱 [Linux manual page](https://man7.org/linux/man-pages/man3/scanf.3.html) 。 ### 沒有換行符號的 printf 一般來說,我們使用 `printf()` 時都會搭配換行字元 `'\n'` : ```c= int main(){ fprintf(stdout, "Hello World\n"); return 0; } ``` 如果不想要使用換行字元, stdio lib 提供了一個特別的函式: ```c= int fflush(FILE *stream); ``` - 對於輸出流來說, `fflush()` 會強制將 buffer 的內容輸出到目標 file descriptor 上並清出 buffer 。 - 對於輸入流來說, `fflush()` 會清空輸入的 buffer ,常被應用在連續讀取字串的情境。 - 如果傳入的參數為 NULL , `fflush()` 會清除所有輸出流。 看完 `fflush()` 的介紹,直接改寫先前的程式碼: ```c= int main(){ fprintf(stdout, "Hello World"); fflush(stdout); return 0; } ``` ### 小總結: 此 Printf 非彼 Printf stdio.h 定義了非常多的方法,本文介紹的僅僅是冰山一角,從 Linux manual page 上就可以得知: printf 有 10 種變形,而 scanf 也多達 6 種變形! 因為篇幅關係,本文就不一一介紹,如果讀者有興趣可以參考以下文章: - [printf、fprintf、dprintf、sprintf、snprintf、vprintf 系列](https://blog.csdn.net/feather_wch/article/details/50709141) - [程式人雜誌 -- 2013 年 12 月號](http://programmermagazine.github.io/201312/htm/article2.html) ## 總結 仔細閱讀 stdlib.h 內的定義後,筆者發現: 日常生活中我們所使用的 printf 看似簡單,背後也藏有大學問。如果沒有清楚掌握 file stream 的概念,可能會造成程式碼出現非預期的情況,例如: 由 CMU 舉辦的 picoCTF 就曾出過利用 `gets` 弱點的[題目](https://ithelp.ithome.com.tw/articles/10227440)。不只如此,筆者前陣子在滑臉書時也看到某字備資工系的老師在抱怨大多數的資工所考生竟然連 `scanf()` 要傳入的是位址這種送分題都答不出來。 因此,我想: 如果連資工系學生都不太會使用 `scanf()` ,那讀完這篇文章應該就可以幹掉一大半學生了吧!(誤) ## Reference - [Linux manual page](https://man7.org/linux/man-pages/man0/stdio.h.0p.html) - [stack overflow](https://stackoverflow.com/questions/22367920/is-it-possible-that-linux-file-descriptor-0-1-2-not-for-stdin-stdout-and-stderr) - [System programming](https://github.com/angrave/SystemProgramming/wiki/C-Programming%2C-Part-2%3A-Text-Input-And-Output#printing-to-streams) - [putchar、getchar、puts、fgets](https://openhome.cc/Gossip/CGossip/PutcharGetcharPutsGets.html) - [stdin、stdout、stderr串流](https://welkinchen.pixnet.net/blog/post/41649481-stdin%E3%80%81stdout%E3%80%81stderr%E4%B8%B2%E6%B5%81) - [關於 fflush 函式(stdin, stdout)](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/103492/#outline__1_2)