标準IO
文章目錄
- 标準IO
- 前言
- 一、為什麼需要标準IO
-
-
-
-
- "檔案": 普通的文本檔案和二進制檔案
- 文本檔案: 無組織、無格式的檔案,以字元的ASCII碼等其他編碼來解析的檔案。
- 二進制檔案: 有特定格式的檔案。
- 還提供了對"檔案"操作的接口函數:
- 使用标準IO的實作過程:
- 标準IO帶緩存的IO,IO流,它的效率要比系統IO要高,why?
- beacase
- 标準IO庫,會自動為每個程序,打開三個标準IO流(檔案):
- 标準輸入流:FILE *stdin
- 标準輸出流: FILE *stdout
- 标準出錯:FILE *stderr
-
-
-
- 二、标準IO的函數接口
-
- (1)标準IO打開或關閉一個檔案流
-
- fopen
- fclose:用來關閉指定的檔案流
- (2)讀寫流:一旦讀寫成功啦,光标會自動往後移n個位置(n就是你讀寫成功的位元組數)
-
- (2.1)每次一個字元讀寫
- (2.2)每次一行讀寫
- (2.3)直接讀寫 fread/ fwrite
-
- fread
- fwrite
- (3)沖洗流 fflush,同步
- (4)定位流
- (5)檔案出錯/結束标記
- (6)格式化IO
-
- (6.1)格式化輸入
- 總結
前言
一、為什麼需要标準IO
因為每個作業系統下面,對檔案的管理和接口是不一樣的。
linux:open/read/write/close…,struct file, struct inode
windows:winopen,winclose,…
同一個檔案,在不同的作業系統下面,我們的操作檔案的代碼都不一樣。
c語言标準委員會,就統一了檔案操作的接口
–》标準IO庫: 主要統一對檔案操作的接口
“檔案”: 普通的文本檔案和二進制檔案
文本檔案: 無組織、無格式的檔案,以字元的ASCII碼等其他編碼來解析的檔案。
如:.txt .c .h .s .cpp .java …
二進制檔案: 有特定格式的檔案。
如:.jpg .bmp .gif .mp3 .mp4
在标準IO庫,用結構體 FILE結構體來描述或表示一個檔案,然後在這個結構體中建立了兩個緩沖區(一段記憶體),一個讀緩沖區,一個寫緩沖區
FILE
{
char *in; //指向讀的緩沖區
char *out //指向寫的緩沖區
};
還提供了對"檔案"操作的接口函數:
fopen/fclose/fread/fwrite/fseek/…
puts/gets/fputs/fgets/scanf/printf…
使用标準IO的實作過程:
Your Code —>标準IO庫(如:fopen/fclose…) -->系統IO -->核心–>Hardware
FILE 有兩個緩沖區(标準IO庫中開辟的一段記憶體)
*in —> 讀的緩沖區
*out --> 寫的緩沖區
标準IO帶緩存的IO,IO流,它的效率要比系統IO要高,why?
beacase
系統IO: read 1byte 從硬碟中讀一個位元組出來
标準IO:
read 1byte,它會從硬碟上讀一塊(如:512byte)出來,放到 标準IO緩沖區。(記憶體條)
标準IO:IO流,stream跟水流一樣,流走了就沒有了,讀走了就沒有了
緩沖: 同步的問題。
緩沖區中的資料,何時同步到外設上去呢?
緩存區開多大呢? 标準IO緩沖區有三種類型:行緩沖、全緩沖、無緩沖
行緩沖:緩沖區的資料達到一行,同步到外設上去。
假設你設定一行頂多80個位元組。遇到\n(稱換行符,一行結束的标志)就會把緩沖區中的資料同步到外設上去
printf —> 行緩存
main()
{
printf("hello");
while(1);
}
此時在螢幕上沒有“hello”列印出來,因為沒有hello後面沒有‘\n’,hello一直在緩沖區中,并沒有把緩沖區中的資料同步到外設上去。
全緩沖: 緩沖區中資料要填滿整個緩沖區,才同步到外設上去。
無緩沖: 緩沖區中有一個位元組,就同步到外設上去。
perror --> 無緩沖
标準IO庫,會自動為每個程序,打開三個标準IO流(檔案):
标準輸入流:FILE *stdin
stdin 是定義在<stdio.h>中的一個全局變量,它指向标準輸入裝置(一般為終端或鍵盤)
scanf() <-----stdin
标準輸出流: FILE *stdout
stdout是定義在<stdio.h>中的一個全局變量,它指向标準輸出裝置(控制台console,終端或螢幕)
printf() <----stdout
标準出錯:FILE *stderr
stderr是定義在<stdio.h>中的一個全局變量,它指向标準出錯裝置(一般為終端)
perror() <---- stdout
二、标準IO的函數接口
(1)标準IO打開或關閉一個檔案流
fopen
頭檔案
#include <stdio.h>
函數功能
用來打開一個普通檔案(文本檔案/二進制檔案)
函數原型
FILE *fopen(const char *pathname, const char *mode);
函數參數
const char *pathname //要打開的那個檔案的檔案名
const char *mode //打開檔案的方式,有如下集中:
“r”: 隻讀打開。檔案不存在,則報錯; 打開後,光标在開頭。
"r+": 讀寫打開。檔案不存在,則報錯;打開後,光标在開頭。
"w": 隻寫打開。檔案不存在,則建立; 打開後,檔案内容截短(檔案内容被清掉)。
"w+": 讀寫打開。檔案不存在,則建立; 打開後,檔案内容截短(檔案内容被清掉)
“a”: append 追加打開,檔案不存在,則建立; 打開後,光标在末尾。檔案内容不會被截短。
“a+”:讀寫打開。檔案不存在,則建立。 原始讀的位置在開頭,原始寫的位置在末尾
傳回值
成功:傳回打開檔案指針。 FILE *
在标準IO中,FILE *代表一個打開的檔案,後續标準IO庫的函數都需要要到它。
失敗:傳回NULL,同時errno被設定
fclose:用來關閉指定的檔案流
頭檔案
#include <stdio.h>
函數功能
用來關閉stream指定的檔案流
函數原型
int fclose(FILE *stream);
函數參數
FILE *stream //要關閉的檔案的FILE *
傳回值
成功: 傳回0
失敗: 傳回-1,同時errno被設定
(2)讀寫流:一旦讀寫成功啦,光标會自動往後移n個位置(n就是你讀寫成功的位元組數)
a:每次讀一個字元讀寫
fgetc/getc/getchar
fput/putc/putchar
b:每次一行讀寫
fgets/gets
fputs/puts
c:直接讀寫,你想要讀多少個對象都可以
fread
rwrite
(2.1)每次一個字元讀寫
fgetc/getc/getchar
fput/putc/putchar
頭檔案
#include <stdio.h>
函數功能
用來從stream指定的檔案流中,讀取一個字元。
函數原型
int fgetc(FILE *stream);
函數參數
FILE *stream //指定要從哪個檔案流中讀取字元
傳回值
成功: 傳回讀到的那個字元的ASCII碼
失敗:傳回-1,同時errno被設定。
get和fget一樣,也是用來從stream指定的檔案流中,讀取一個字元。getc和fget的差別在哪裡?
fgetc是一個函數;
getc可能是用用來實作的
函數原型
int getc(FILE *stream);
getcha是用來從标準輸入流stdin中擷取一個字元,并把讀取到的字元的ASCII碼傳回。
函數原型
int getchar(void); 《==》 fgetc(stdin);
頭檔案
#include <stdio.h>
函數功能
用來把c指定的字元,輸出到stream指定的檔案流中去,
函數原型
int fputc(int c, FILE *stream);
函數參數
int c //要輸出的字元的ASCII碼
FILE *stream //檔案流,表示輸出到哪個檔案中去。
傳回值
成功:傳回實際寫入到檔案流中的字元的ASCII碼(c值)
失敗:傳回-1,同時errno被設定。
putc與fputc功能與傳回值 一樣,隻不過putc的實作可能是由宏來實作的
函數原型
int putc(int c, FILE *stream);
putchar用來把c指定的字元,輸出到"标準輸出流檔案中"
函數原型
int putchar(int c); 《==》 fputc(c,stdout);
(2.2)每次一行讀寫
fgets/gets
fputs/puts
頭檔案
#include <stdio.h>
函數功能
用來從标準輸入流(stdin)擷取一行字元,存儲到s指向的記憶體空間中去。
函數原型
char *gets(char *s);
函數參數
char *s //指向的空間,用來存儲從輸入緩沖區擷取到的多個字元
傳回值
成功:非空 NULL
失敗:傳回NULL,同時errno被設定
注意:
gets有一巨大的bug,你懂的!!!
gets沒有考慮到s指向的空間的大小問題,存在越界的可能。 是以從今天開始,gets你們就不要用啦。
fgets修正了gets的這個bug
函數功能
從stream所指向的檔案中讀取size個位元組大小的資料到s指向的空間中
函數原型
char *fgets(char *s, int size, FILE *stream);
函數參數
char *s //指向的空間用來儲存從檔案流中讀取的資料
int size //表示你要最多擷取size個位元組,size一般為s指向的
空間的可用長度。
fgets輸入結束有兩種情況:
(1)遇到\n或檔案結束
(2)已經讀取到size-1個位元組啦(後面留一個\0的位置)
FILE *stream //FILE *指針,表示從哪個檔案流中讀取資料
傳回值
成功:傳回s的首位址
失敗:傳回NULL,同時errno被設定
頭檔案
#include <stdio.h>
函數功能
fputs用來把s指向的字元串,輸出到stream指定的檔案流中去
函數原型
int fputs(const char *s, FILE *stream);
函數參數
const char *s //指向要輸出的字元串的首位址
FILE *stream //表示要輸出到哪個檔案流中去
傳回值
成功:傳回一個非負數
失敗:傳回-1,同時errno被設定。
(2.3)直接讀寫 fread/ fwrite
fread
頭檔案
#include <stdio.h>
函數功能
從stream指定的檔案流中,讀取nmemb個對象且每個對象size位元組,讀取到ptr指向的記憶體空間中去。
函數原型
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函數參數
void *ptr //指向的記憶體空間,用來儲存從檔案流讀取到的"數組"
size_t size //每個元素占的位元組大小
size_t nmemb //要讀取的n個元素
FILE *stream //表示要從哪個檔案流中讀取資料
傳回值
成功:傳回實際讀取到的元素個數 <= nmemb
失敗:傳回-1,同時errno被設定。
fwrite
頭檔案
#include <stdio.h>
函數功能
用來把ptr指向的nmemb個元素(每個元素占size位元組)寫入
到stream指向的檔案流中去。
函數原型
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
函數參數
const void *ptr //要寫入的數組的首位址
size_t size //每個元素所占的大小
size_t nmemb //元素的個數
FILE *stream //表示要寫到哪個檔案流中去
傳回值
成功:傳回實際寫入到檔案流中去的元素個數, <= nmemb
失敗:傳回-1,同時errno被設定。
(3)沖洗流 fflush,同步
頭檔案
#include <stdio.h>
函數功能
将緩沖區資料沖洗到對應的裝置上或者丢棄(同步到硬體裝置)
對輸出流,fflush把寫緩沖區的内容寫/更新到檔案中去;
對輸入流,fflush把讀緩沖區的内容直接discards(丢棄);
strea為NULL,fflush把該程序所有打開的輸出流檔案同步
函數原型
int fflush(FILE *stream);
函數參數
FILE *stream //要同步的檔案流
傳回值
成功:傳回0
失敗:傳回-1,同時errno被設定。
練習:
請分析如下程式的輸出結果?
main()
{
printf("hello"); //把hello寫到檔案 stdout的 輸出緩沖區
//中去了,但并沒有寫入到外設檔案中去
sleep(10);
fflush(NULL); //fflsh(stdout); //if stream 輸出流,把輸出緩沖區的内容寫到
//外設檔案中去
//if stream 輸入流,把輸入緩沖區的内容丢棄,這樣的話
//你下次讀,就可以重新從外設檔案中讀啦
while(1);
}
(4)定位流
上面講到的,fread/fwrite …隻是說要從哪個檔案,讀/寫多少個位元組的資料并沒有指定從檔案流的哪個位置開始讀寫,标志IO庫會為每個開打的檔案流儲存一個“檔案偏移量”
“檔案偏移量”:offset,“光标”。直接确定下一個讀/寫的起始位置。
一般來說,每次讀或寫之前,先要定位流
頭檔案
#include <stdio.h>
函數功能
定位一個檔案的偏移量
函數原型
int fseek(FILE *stream, long offset, int whence);
函數參數
FILE *stream //檔案流指針,表示你要定位哪個檔案
long offse //偏移量,可正可負;要結合第三個參數
int whence //從哪裡開始定位 定位的方式,有如下三種:
SEEK_SET:基于檔案開頭定位
新光标位置 = 檔案開頭 + offset(>=0)
SEEK_CUR:current基于目前光标位置定位
新光标位置=目前位置 + offset(可正可負)
SEEK_END:基于檔案末尾定位
新光标位置=檔案末尾 + offset(可正可負)
傳回值
成功:傳回0
失敗:傳回-1,同時errno被設定。
ftell傳回目前光标位置離檔案開頭有多少位元組
函數原型
long ftell(FILE *stream);
rewind把檔案光标,定位在檔案開頭
函數原型
long ftell(FILE *stream);
rewind(stream) => fseek(stream, 0, SEEK_SET);
(5)檔案出錯/結束标記
頭檔案
#include <stdio.h>
函數功能
判斷stream指定的檔案流是否結束
函數原型
int feof(FILE *stream);
函數參數
FILE *stream //填要判斷的檔案流
傳回值
如果傳回真(非0) 檔案到達末尾啦
如果傳回假(0) 該檔案還沒有到達末尾。
注意:
标準IO庫,在讀到檔案末尾時,會往緩沖區中填入一個EOF(二進制 11111111)
(6)格式化IO
(6.1)格式化輸入
scanf/sscanf/fscanf
“格式化輸入”?:按照我指定的格式來輸入。
頭檔案
#include <stdio.h>
函數功能
按照我指定的格式來輸入
函數原型
int scanf(const char *format, …);
函數參數
const char *format,…
scanf可以帶很多個參數,scanf的參數可以分為兩類:
第一個參數為第一類,格式化字元串,format string:
"格式化字元串"就是告訴使用者怎麼輸入的,意思是你必須按照它指定的格式去輸入。
在“格式化字元串”中有三類字元:
a.空白符(space tab)
訓示使用者 你可以輸入任意數量的空白符(包含0個)scanf把\n當作是輸入結束
b.非轉義字元(普通字元,除空白符和%以外的字元)精準比對,你得原樣輸入
c.轉義字元(以%開頭的字元),有特殊含義:
%d -> [0-9]+
%c -> 比對一個字元(可以輸入的字元)
%f -> 浮點數
%s -> 字元串(不帶空白符,scanf把空白符當作是一個分割符号)
其他參數為第二類,位址清單:
格式化字元串中一個轉義字元會對應一個位址,把一個轉移字元的輸入存儲到指定的位址中去,如果轉移字元的個數 多餘 位址個數,程式行為将是 undefined(未定義的)
傳回值
成功比對的變量個數!!!
scanf擷取輸入時,如何結束?scanf從stdin的讀緩沖去中擷取輸入
a.該輸入的都輸入完了
sacnf(“abcd%d %cabcd”,&a,&c);
使用者輸入:
abcd1234Aabcd -> 該輸入的都輸入啦,scanf結束
b.失敗啦
scanf(“abcd%d %cabcd”,&a,&c);
使用者輸入:
ABCD-》scanf停止比對啦
例子:
int r;
int a;
char c;
r = scanf(“abcd%d %c”,&a,&c);
假設使用者入:
ABCD123 A
a=?
c=?
r=?
不比對是以a與c不是123與A ,r=0;
r = scanf("%d %c",&a,&c);
加入使用者輸入:
123B
a=123
c=B
r=2
sscanf它的功能與傳回值 ,和scanf一樣的,隻不過sscanf的輸入來源不是stdin,而str指向的字元串。
sscanf的參數有三類:
1.str是輸入來源字元串
2.format是格式化字元串
3.其他參數為位址清單
函數原型
int sscanf(const char *str, const char *format, …);
例子:
(1)char *str = “123456sdadwa”
int r;
int a;
char c;
r = sscanf(str, “%d %c”, &a,&c);
a=?
c=?
r=?
(2)
char *s = “123”;
int a;
sscanf(s,"%d",&a);
fscanf它的功能和傳回值,與scanf一樣,隻不過,fscanf它的輸入
來源不是stdin,而stream指定的檔案流,是以fscanf的參數,分為三類:
1.第一個參數FILE *,指定輸入來源
2.第二個參數format,格式化字元串,與scanf是一樣
3.其他參數為 位址清單
函數原型
int fscanf(FILE *stream, const char *format, …);
scanf(format,…) <==> fscanf(stdin,format,…)
(6.2)格式化輸出
printf/sprinft/snprintf/fprintf
“格式化輸出”?:按照我指定的格式去輸出。
頭檔案
#include <stdio.h>
函數功能
按照指定的格式去輸出
函數原型
int printf(const char *format, …);
函數參數
const char *format, …
printf可以帶多個參數 ,這麼多參數,可以分為兩類:
第一個參數為第一類 格式化字元串,就是告訴你怎麼輸出的
格式化輸出字元串有兩類:
a.轉義字元:以%開頭的字元
%d -> 按十進制有符号整數輸出
%u
%ld
%lu
%f
%c ->輸出字元的形狀
%s ->把後面的位址,字元串去輸出 直到遇到\0
…
b.非轉義字元
你得原樣輸出
其他參數為第二類,要輸出得變量或對象清單
要輸出得變量或對象個數 應該與轉義字元的個數一緻
傳回值
實際列印的字元個數
例子:
int a = 123;
char c = ‘A’;
int r;
r = printf(“a = %d c = %c”,a,c);
r =?
fprintf它的功能和printf一樣的,隻不過fprintf
輸出不是輸出到stdout,而是輸出到stream指定的
檔案中。是以fprintf它的參數,可以分為三部分:
第一個參數為 FILE *,指定要輸出到哪個檔案中去
第二個參數 format,格式化字元串
其他參數,為要輸出的變量或對象清單
函數原型
int fprintf(FILE *stream, const char *format, …);
傳回值 :
為實際輸出到檔案中的字元個數
printf(format,…) <==> fprintf(stdout,format,…);
sprintf它的功能和printf一樣,隻不過,sprintf
輸出不是輸出到stdout,而是輸出到str指定的記憶體中去。
是以sprintf參數,可以分為三部分:
第一個參數為char *,記憶體位址,指定輸出字元串的輸出位置
第二個參數為format,格式化字元串
其他參數,為實際輸出的變量或對象清單
函數原型
int sprintf(char *str, const char *format, …);
傳回值 :
為實際輸出到記憶體中去的字元個數。
例子:
char filename[512];
char *file =“xxx.mp3”;
char *path ="/home/china";
sprintf(filename,"%s/%s",path,file);
注意:但是,sprintf有一個bug
str隻是指定了一個記憶體的起始位址,并沒有限定它的記憶體範圍,if輸出字元串的長度 > str指向的記憶體的範圍,有越界的風險。
=》記憶體的非法通路
so,為了解決sprintf的這個bug,才有了 下面的snprintf ,snprintf中size指定str指向的那段記憶體空間的最大長度,也就是說,格式化的輸出頂多輸出size-1個字元到str 指向的空間中去。
函數原型
int snprintf(char *str, size_t size, const char *format, …);
例子:
char s[8];
char *str = “1234567890”;
int r;
r = snprintf(s, 8, “%s”,str);
r =
printf("%s",s);
注意:
snprintf它的傳回值 是應該輸出的字元串長度,而不是實際輸出到記憶體中的長度。
總結
1.标準IO庫是什麼東西? 為什麼需要它?它的實作原理是什麼?
2. 标準IO與系統IO之間的關系?标準IO中用什麼東西來表示一個檔案?
3. 為什麼把标準IO稱為流?為什麼說标準IO的效率要系統IO高?
4.标準IO隻能操作 “普通檔案”!!!!