前言:Hello,大家好😘,我是心跳sy,上一節我們主要學習了格式化輸入輸出的基本內容,這一節我們對格式化進行更加深入的了解,對文件概念進行介紹,并且對輸入、輸出與文件讀寫的基本概念進行學習,本節主要對printf,scanf深入了解,并介紹文件處理函數,如fprintf,fopen,fclose,freopen,tmpfile,tmpnam,fflush,setvbuf,setbuf以及其他文件操作函數進行理解,對字符的輸入輸出(putchar,getchar,fputc,fgetc)行的輸入輸出(puts,fputs,gets,fgets)塊的輸入輸出(fread,fwrite)以及字符串的輸入和輸出(sprintf,snprintf,sscanf)會到下一節介紹,我們一起來看看吧💞💞💞!!!
? c語言的輸入/輸出庫非常龐大,并且是c語言的高級應用,我們前面已經學會使用printf,scanf函數的基本用法,這一節我們主要對文件操作進行介紹,并對格式化輸入輸出函數進行深入的了解,上一節還沒太懂的友友們看完這一篇相信會有更加深入的了解~?
? 我們知道文件讀寫是許多應用程序不可缺少的部分,在計算機編程中起著至關重要的部分,它允許程序通過讀入和寫入文件來持續化數據,以此來實現數據的長期保存和共享;而文件讀寫的基本概念是通過輸入輸出操作來與計算機上的文件進行交互,所以我們需要熟練掌握運用????
? 本篇文章對各個示例均有詳細的解釋,對難理解的概念也進行了詳細的詮釋,耗時幾天完成,內容較長,精心打造,無cv,概念均參考權威書籍。友友們耐心看完,放心食用,沒學過文件和有關輸入輸出的友友們也能看懂哦~😘😘😘
1、??流的概念??
?👉在c語言中,流表示任意輸入的源或任意輸出的目的地,可以想象成水流,只不過c語言的流更富有邏輯順序概念,它提供和儲存數據,產生數據的叫做輸入流,消耗數據的叫做輸出流,c程序與數據的交互都是以流的形式進行的,而我們下來介紹的文件讀寫就是先“打開文件”以打開數據流,然后“關閉文件”以關閉數據流;我們之前學習的printf,scanf就是格式化的輸出函數、輸入函數,所屬的流是標準流,我們接下來會詳細介紹~🌈🌈
1.1、💫文件指針?
👉?一般形式:FILE *指針變量標識符;c程序中對流的訪問是通過文件指針實現的,文件指針的類型是FILE *(FILE類型在頭<stdio.h>中聲明),其中文件指針表示的特定流具有標準的名字,比如stdin指針(標準輸入流)等,如果用戶需要,也可以自定義流:FILE *fp;(注意操作系統通常會限制可以同時打開的流,但程序可以聲明任意數量的FILE *類型變量。)
1.2、💫標準流和重定向?
👉c語言頭<stdio.h>提供了3個標準流(同樣c++中頭<iostrream>也提供了標準輸入輸出流,我們以后介紹),c語言中這3個標準流可以直接使用,不需要對其進行聲明,也不用打開或關閉它們。下圖提供3個標準流:
?
?👉這3個標準流的應用非常廣泛,我們經常使用的printf,scanf,putchar,getchar,puts,gets函數都是通過stdin獲得輸入,通過stdout進行輸出的。默認情況下,stdin表示鍵盤,stdout和stderr表示屏幕,然而許多操作系統允許通過重定向的機制來改變這些默認的含義。?
👉通常我們可以強制程序從其他文件獲得輸入而不是從鍵盤那里,方法是在命令行中放上文件的名字,并在其前面加上字符小于號<,跟在程序名后面:(這里demo是指程序,意為demo程序代碼里面的stdin,將指向文件in.dat,即從in.dat中獲取數據)
demo<in.dat
???這種方法叫做輸入重定向,它的本質是使stdin流表示文件(in.dat)而非鍵盤,其精妙之處在于demo程序不會意識到在從文件in.dat中讀取數據,它會認為從stdin獲得的任何數據都是從鍵盤輸入的。
???輸出重定向與輸入重定向類似,對stdout流的重定向是通過在命令行中放置文件名,并在其前加上字符大于號>實現:
demo>out.dat
👉現在所有寫入stdout流的數據都將進入out,dat文件中,而不是出現在屏幕上。值得一提的是😃,我們還可以把輸入重定向和輸出重定向結合起來使用,而且<,>字符不用與文件名相鄰,重定向文件的順序也無關緊要,下面兩個例子是等效的:
demo < in.dat > out.dat
demo >out.dat <in.dat
??需要注意的是,輸出重定向有一個問題,就是會把寫入stdout的所有內容都放入文件中,如果程序運行失常或寫出出錯消息,那么我們在看文件的時候才能發現,而這些應該是出現在stderr中的,所以通過把出錯消息寫到stderr而不是stdout中,可以保證即使在對stdout重定向時,出錯消息仍能顯示到屏幕上。(比如linux c語言用perror()函數將錯誤消息寫入標準錯誤stderr)
1.3、💫文本文件與二進制文件
👉<stdio.h>支持兩種類型的文件:文本文件和二進制文件。我們知道,計算機的儲存在物理上是二進制的,所以文本文件與二進制文件的區別不是物理上的,而是邏輯上的,兩者只在編碼層次上有差異,簡單的來說,文本文件是基于字符編碼的文件(人們可以檢查和編輯文件),常見的編碼有ASCII編碼、UNICODE編碼等等,例如c程序的源代碼是儲存在文本文件中;二進制文件是基于值編碼的文件,其中字節除了可以表示字符,還可以表示其他類型的數據,比如浮點數和整數。
👉如果上面概念還未完全理解,我們再次深入理解: 文本文件基于字符編碼,基本上是定長編碼(也有非定長如UTF-8),每個字符在具體編碼中是固定的,如ASCII碼是8個比特的編碼,UNICODE一般占16個比特。而二進制文件可看成是變長編碼,因為是值編碼,多少比特代表一個值,完全由自己決定,所以比較靈活,節約空間。
👉我們來看一個栗子🌸:當存儲實型數字時,如3.1415927,文本文件需要9個字節,分別存儲:3 . 1 4 1 5 9 2 7這9個字符的ASCII值;而二進制文件只需要4個字節:DB 0F 49 40
👉我們經常會遇到用記事本打開文件亂碼的情況,原因如何??
🌸文本工具打開一個文件,首先讀取文件物理上所對應的二進制比特流,然后按照所選擇的解碼方式來解釋這個流,然后將這個解釋結果顯示出來。一般來說,選取的解碼方式會是ASCII碼形式(一個字符8個比特),接下來它會8個比特8個比特地來解釋文件流,記事本無論打開什么文件都按既定的字符編碼工作(ASCII碼),所以當打開一個二進制文件時,就會出現亂碼,因為解碼和譯碼不對應。
??文本文件具有兩種二進制文件沒有的特性:
???1、文本文件分為若干行。文本文件的每一行通常以一兩個特殊字符結尾,特殊字符的選擇與操作系統有關。在Windows中,行末的標記是回車符('\x0d')與一個緊跟其后的回行符('\x0a')。在UNIX和Macintosh操作系統(Mac OS)新版中,行末的標記是一個單獨的回行符。
- \x0d代表回車字符,它的ASCII碼值為13(十進制)。在文本文件中,回車字符通常用于表示光標返回到當前行的開頭,但不換行。
- \x0a代表換行字符,它的ASCII碼值為10(十進制)。換行字符用于在文本文件中表示將光標移動到下一行的開頭。
在不同的操作系統和編程環境中,回車和換行字符的使用方式可能會有所不同:
- 在Windows操作系統中,通常使用回車和換行兩個字符(\r\n)來表示換行,即先回車再換行。(\r的ASCII碼就是13,是回車;\n的ASCII碼為10,是換行,與\x0a,\x0d等價)
- 在Unix/Linux操作系統和類Unix環境(如macOS)中,通常只使用換行字符(\n)來表示換行。
- 在早期的Macintosh操作系統中,通常只使用回車字符(\r)來表示換行。
??2、文本文件可以包含一個特殊的“文件末尾”標記。一些操作系統允許在文本文件的末尾使用一個特殊的字節作為標記。在Windows中,標記為'\xla'(Ctrl+Z)。Ctrl+Z不是必須的,但如果存在,它就標志著文件的結束,其后的所有字節都會被忽略。使用Ctrl+Z的這一習慣繼承自DOS(磁盤操作系統),而DOS中的這一習慣又是從CP/M(早期用于個人計算機的一種操作系統)來的。大多數其他操作系統(包括UNIX)沒有專門的文件末尾字符。
??二進制文件不分行,也沒有行末標記和文件末尾標記,所有字節都是平等對待的。
2、??文件操作??
👉輸入輸出重定向雖然簡單易懂,但是在許多程序中受限制,當程序依賴重定向時,它無法控制自己的文件,甚至無法知道這些文件的名字,也無法同時寫入或讀入兩個文件,所以這時我們將使用<stdio.h>提供的文件操作,我們下面一起來學習打開、關閉文件、改變緩沖文件的方式以及怎樣刪除文件和重命名文件!!😃😃
?2.1、💫打開文件(fopen函數)
?
👉如果要把文件用作流,打開時就需要調用fopen函數,也叫作打開文件流。fopen的第一個參數是含有要打開文件名的字符串,其值應符合運行環境的文件名規范,可以包含路徑位置信息(如果系統支持)。第二個參數是“模式字符串”,它用來指定打算對文件執行的操作,例如字符串“r”表示從文件讀入數據,但不會向文件寫入數據,我們下面會詳細介紹。
???注意,從C99開始,對fopen函數原型聲明用restrict關鍵字進行修飾,這表明filename和mode所指向的字符串的內存單元不共享。
???????注意在Windows系統中,fopen函數調用用的文件名中含有字符 \ 時,一定要小心,c語言會把 \ 看作轉義字符的開始標志。如下圖:
fopen("c:\test_8_9\test1.dat", "r")
?以上調用會失敗,因為編譯器會把 \t 看作轉義字符,所以有效的辦法是用 \\ 來代替 \ 或者直接使用 / 代替 \ 。如下兩種方法都可行:
fopen("c:\\test_8_9\\test1.dat", "r")
fopen("c:/test_8_9/test1.dat", "r")
👉?fopen函數返回一個文件指針,程序通常把此指針存儲在一個變量中,然后后續使用時直接使用,fopen函數常見調用形式如下,其中fp是FILE*類型的變量,當程序調用輸入函數從文件in.dat中讀取數據時,會把fp作為實參。
👉當無法打開文件時,fopen函數會返回一個空指針,其原因可能是因為文件位置不對或者我們沒有打開文件的權限。
fp = fopen("in.dat", "r");
?🔴我們看下面的一個例子:下面的例子fopen函數的一個參數是文件路徑,第二個參數模式字符串中采用了“w”意為打開文件“寫”:
#include<stdio.h>
int main()
{FILE* fp = fopen("C:\\Users\\樊雙藝\\Desktop\\c.txt.txt", "w");if (fp != NULL){fprintf(fp, "Hello, world!\n");fclose(fp);}return 0;
}
我們可以看到文本文件中寫入了Hello,world!
?
???????這個例子有幾點需要大家注意:
??永遠不要假設可以打開文件,每次都要測試 fopen函數的返回值以確保不是空指針,所以這里我們用 if語句來判斷 fp是否為空,這里的 fprintf函數稍后介紹,我們現在只需要知道它的第一個參數是指向要寫入文件的指針。
??當成功寫入文件后,我們需要關閉文件,一定要注意這是配套存在的??????
2.2、💫模式
👉fopen函數的第二個參數要傳遞哪種模式字符串不僅依賴于稍后我們想對文件進行什么操作,還取決于文件中的數據是文本形式還是二進制形式。
👉下圖為文本文件的模式字符串:?
?
👉下圖為二進制文件的模式字符串:當使用fopen打開二進制文件時,需要在模式字符串包含字母b(UNIX系統中文本文件與二進制文件具有完全相同的格式,所以不需要字母b,但是UNIX程序員仍應該包含字母b,便于代碼移植)
??從兩個表格可以看出,頭<stdio.h>對寫數據和追加數據進行了區分:當給文件寫數據時,通常會對先前的內容進行覆蓋;然而,當為追加文件時,向文件寫入的數據添加在文件末尾,因而可以保留文件的原始內容。?
??另外,帶有字母“x”的打開模式是從C11才開始引入的,這個字母表示獨占模式。在這種模式下,如果文件已經存在或者無法創建,fopen函數將執行失敗;否則文件將以獨占(非共享)模式打開。
??圖中帶有“+”的字符串(也就是當打開文件用于讀和寫)時,需要先調用一個文件定位函數,不然就不能從讀轉為寫,除非讀遇到文件末尾;相應的如果既沒調用fflush函數,也沒有文件定位函數,那么就不能由寫模式轉為讀模式。文件定位函數我們下節會介紹,fflush函數稍微會介紹。
2.3、💫關閉文件(fclose函數)?
?
👉fclose函數允許程序關閉不再使用的文件,也叫作關閉文件流。fclose函數的參數必須是文件指針,此指針來自fopen函數或freopen函數(稍后介紹)的調用,如果成功關閉了文件,flcose函數會返回零,否則它會返回錯誤代碼EOF。
??實例見fopen函數實例,程序員注意成對使用即可。
2.4、💫為已打開的流附加文件?(freopen函數)
?
👉freopen函數為已經打開的流附加一個不同的文件(簡單地說用于重定向輸入輸出流)。最常見的用法是把文件和一個標準流(前文介紹的3只)相關聯,可以在不改變代碼原貌的情況下改變輸入輸出環境。其中三個參數,filename是需要重定向到的文件名或文件路徑,mode代表模式字符串,stream是需要被重定向的文件流。
👉返回值:通常是它的第三個參數(文件指針),如果無法打開文件則返回NULL。
??下面實例表示往foo文件寫數據:其中假設freopen返回值為NULL,所以打不開foo文件。
if (freopen("foo", "w", stdout) == NULL)
{//erro;foo can not be opened
}
2.5、💫從命令行獲取文件名?
👉當正在編寫的程序需要打開文件時,就會出現一個問題:如何把文件名提供給程序呢?最好的解決方案是讓程序從命令行獲取文件的名字,例如,當執行名為demo的程序時,可以通過把文件名放入命令行的方法為程序提供文件名:
demo names.dat dates.dat
🌈這里我們要通過定義帶有兩個形式參數的main函數來訪問命令行參數,我們下面介紹原理,會的友友們可以直接跳過~💞
2.5.1、💫命令行參數
👉運行程序時經常需要提供一些信息——文件名或者改變程序行為的開關,如果我們要訪問這些命令行信息參數,必須通過把main函數定義為含有兩個參數的函數來實現,這兩個參數通常命名為argc和argv。形式如下:
int main(int argc, char* argv[])
{...
}
👉argc(“參數計數”)是命令行參數的數量(包括程序名本身),argv(“參數向量”)是指向命令行參數的指針數組,這些命令行參數以字符串的形式存儲,argv[0]指向程序名,而從argv[1]到argv[argc-1]則指向余下的命令行參數。
👉argv有一個附加元素,即argv[argc],這個元素始終是一個空指針(NULL)。
??我們來看一個例子:
👉如果用戶輸入命令行:ls -l remind.c?(這里的 ls 是Linux的命令,ls 命令是“ list ”的縮寫,用于列出或顯示目錄的內容。而 ls -l 是 ls 的命令參數,會以長列表格式顯示文件,內含文件的詳細信息,這里我們只介紹這一種參數,還有很多關于 ls 命令的功能,友友們下來可以了解了解🥳?)
👉那么argc將為3,argv[0]將指向含有程序名的字符串,argv[1]將指向字符串“-l ”,argv[2]將指向字符串“remind.c”,而argv[3]將為空指針。如下圖:
?
👉圖中沒有詳細的程序名,因為操作系統的不同,程序名可能會包括路徑或其他信息,如果程序名不可用,那么argv[0]會指向空字符串。
👉因為argv是指針數組,所以訪問命令行參數非常容易,常見的做法是,期望有命令行參數的程序會設置循環來按順序檢查每一個參數。設定循環的方法之一就是使用整型變量作為argv數組的下標。例如,下面的循環每行一條地顯示命令行參數:
int i;
for (i = 1; i < argc; i++)
{printf("%s\n", argv[i]);
}
👉另一種方法是構造一個指向argv[1]的指針(argv[1]本就是指向字符的指針,所以必須構造二級指針指向字符指針),然后對指針重復進行自增操作來逐個訪問數組余下的元素。因為argv數組的最后一個元素始終是空指針,所以循環可以在找到數組中一個空指針時停止。例如:
char** p;
for (p = &argv[1]; *p != NULL; p++)
{printf("%s\n", *p);
}
??注意:這里我們設置了一個字符型二級指針,p是指向字符的指針的指針,p=&argv[1]是有意義的,因為argv[1]是一個字符指針,所以&argv[1]就是指向指針的指針,因為*p和NULL都是指針,所以測試*p!=NULL沒有問題,p指向數組元素首元素地址,所以p自增可以指向下一個字符;printf中顯示*p也是合理的,因為*p指向字符串的第一個字符,存放第一個字符地址,字符串常量的內存儲存是連續的,所以可以直接打印出整個字符串。(如果還是不太理解的友友可以學習一下二級指針再理解一下代碼解釋~💞)
??通過上面的介紹我們基本清楚了如何定義帶有兩個形式參數的main函數來訪問命令行參數,我們回到從命令行獲取文件名這一模塊;一起來看看之前提到的例子:
demo names.dat dates.dat
👉我們已經知道argc是命令行參數的數量,而argv是指向參數字符串的指針數組。argv[0]指向程序的名字,從argv[1]到argv[argc-1]都指向剩余的實際參數,而argv[argc]是空指針。在上述例子中,argc為3,argv[0]指向含有程序名的字符串,argv[1]指向字符串“names.dat”,argv[2]指向字符串“dates.dat”,argv[3]指向空。所以我們就可以通過匹配命令行參數數量來判斷文件是否具有文件名,并且通過argv[]指針來找到文件名。
2.6、💫臨時文件(tmpfile函數和tmpnam函數)?
👉現實生活中程序經常需要產生臨時文件,即只在程序運行時存在的文件,例如C編譯器就常常產生臨時文件。編譯器可能先把c程序翻譯成一些儲存在文件中的中間形式,稍后把程序翻譯成目標代碼時,編譯器就會讀取這些文件。一旦程序完全通過了編譯,就不再需要保留那些含有程序中間形式的文件了。頭<stdio.h>中提供了兩個函數來處理臨時文件,即tmpfile函數和tmpnam函數。
👉tmpfile函數創建一個臨時文件(用“wb+”模式打開),該臨時文件將一直存在,除非關閉它或程序終止。tmpfile函數的調用會返回文件指針,如果臨時文件創建失敗,函數會返回空指針,此指針可以用于稍后訪問該文件。
FILE* tempptr;
tmpptr = tmpfile();//創建一個臨時文件,tmpptr是臨時指針變量
??雖然tmpfile函數很易于使用,但是它有兩個缺點:
??無法知道tmpfile函數創建的文件名是什么;
??無法在以后使文件變為永久的。
如果這些缺點導致了問題,那么備用方案就是用fopen函數產生臨時文件,因為我們不想讓此文件與前面已存在的文件擁有相同的名字,所以需要一個新函數產生新的文件名,就是tmpnam函數。
👉tmpnam函數為臨時文件產生名字。如果它的實際參數是空指針,那么tmpnam函數會把文件名儲存到一個靜態變量中,并且返回指向此變量的指針。?
char* filename;
...
filename = tmpnam(NULL);//創建一個臨時文件名
👉否則,tmpnam函數會把文件名復制到程序員提供的字符數組中:(在這種情況下,tmpnam函數一樣會返回指向數組第一個字符的指針,L_tmpnam是在頭文件中定義的宏,它指明了保存臨時文件名的字符數組至少的長度)
char filename[L_tmpnam];
...
tmpnam(filename);
2.7、💫文件緩沖(fflush函數、setvbuf函數、setbuf函數)?
?
?👉向磁盤驅動器傳入數據或者從磁盤驅動器傳出數據都是相對較慢的操作,因此在每次程序想讀或寫入字符時都直接訪問磁盤文件是不可行的。這時一個效率高的方法就是緩沖,把寫入流的數據存儲在內存的緩沖區域內;當緩沖區滿了(或者關閉流)時,對緩沖區進行“清洗”(寫入輸出設備)。比如printf函數在輸出時,是先輸出到緩沖區,然后才輸出到屏幕上的,輸入流可以用類似的方法進行緩沖:緩沖區包含來自輸入設備的數據;我們可以從緩沖區讀取數據而不是從設備本身直接讀取數據。緩沖區的存在大大提高了讀取效率,當然,把緩沖區的內容傳給磁盤或從磁盤傳遞給緩沖區也是需要花時間的,但是大規模的“塊”移動總比多次小字節速度快得多。??
? 頭<stdio.h>中的函數會在緩沖有用時自動進行緩沖操作。緩沖是在后臺發生的,但在極少的時候需要我們更主動的操作,這就需要用到上述3個函數。
👉當程序向文件寫輸出時,數據通常先放在緩沖區中。當緩沖區滿了或者關閉文件時,緩沖區會自動清洗(向輸出設備寫入),有時我們期望通過一定頻率來清洗文件的緩沖區,就需要調用fflush函數,fflush函數的參數是指向指定緩沖流的FILE對象的指針,如果函數調用成功,則返回0,否則返回EOF。下面調用的含義就是釋放清洗指定流的緩沖區。
fflush(fp);
?👉而如果需要清洗和fp相關聯的文件,那么就調用下面實例,清洗了全部輸出流:
fflush(NULL);//flushes all buffers
👉setvbuf函數允許改變緩沖流的方法,并且允許控制緩沖區的大小和位置。也可以理解為該函數可指定流的緩沖區,并且允許指定緩沖區的模式和大小(以字節為單位)。函數第一個參數是指向文件對象的指針,該對象標識打開的流。函數的第二個參數是用戶分配的緩沖區的地址,長度至少為字節大小。如果設置為空指針,該函數將自動分配一個緩沖區,需指定緩沖區大小,若調用成功,函數返回0,否則返回非0;函數的第三個參數是期望的緩沖類型模式,有三種緩沖類型,分別定義為3個宏,我們等會列表展示;最后一個參數是緩沖區內字節的數量(緩沖區大小),較大的緩沖區可以提供較好的性能,而較小的緩沖區可以節約空間。
👉下圖為函數第三個參數——3個宏定義緩沖類型模式 ,其中全緩沖又叫滿緩沖:
👉??上述3個宏均在頭<stdio.h>中定義,對于沒有與交互式設備相連的流來說,滿緩沖是默認設置。
??下面的例子調用setvbuf函數,把buffer數組的N個字節作為緩沖區,把stream的緩沖變成了滿緩沖:?
char buffer[N];
...
setvbuf(stream,buffer,_IOFBF,N);
👉setbuf函數是一個較早期的函數,現在的新程序用得不多了,它設定了緩沖模式和緩沖區大小的默認值。
如果buffer是空指針(無緩沖),那么setbuf(stream,buffer)的調用就等價于:
(void)setbuf(stream,NULL,_IONBF,0);
否則等價于(滿緩沖),這里的BUFFERSIZ是在頭文件中定義的宏:
(void)setbuf(stream,buffer,_IOFBF,BUFFERSIZ);
??注意:使用setvbuf函數和setbuf函數時,一定要確保在釋放緩沖區之前已經關閉了流,特別是如果緩沖區是局部于函數的,并且有自動存儲期,一定要確保在函數返回之前關閉流。?
2.8、其他文件操作(remove函數和rename函數)?
?
👉remove函數和rename函數允許程序執行基本的文件管理操作。不同于其他文件處理函數,這兩個函數對文件名(而不是文件指針)進行處理,操作不涉及流,如果調用成功,那么這兩個函數都返回0,否則都返回非0.
??remove函數刪除已經指定文件名的文件:
remove("foo"); //刪除文件名為“foo”的文件
👉如果程序使用fopen函數(而不是tmpfile函數,因為tmpfile函數無法知道創建的臨時文件的文件名是什么)來創建臨時文件,那么它可以使用remove函數在程序終止前刪除此文件。一定要確保已經關閉了要移除的文件,因為對于當前打開的文件,移除文件的效果是由實現定義的。
??rename函數改變文件的名字:
rename("foo","bar"); //改變文件名由“foo”變為“bar”
👉對于用fopen函數創建的臨時文件,如果程序需要使文件變為永久的,那么用rename函數改名就可以了。如果具有新名字的文件已經存在了,改名的效果會由實現定義。
??注意:如果打開了要改名的文件,一定要記住在調用rename函數之前關閉此文件,對打開的文件執行改名操作會失敗。?
🌈這里的實現定義英文名稱是implementation-defined,意為由編譯器設計者來決定采取某種行動的,這個詞語提醒我們,在實際編程時要考慮在多個運行環境下程序會產生不一樣的結果的情況。
3、??深入理解格式化輸入輸出??(printf,fprintf,scanf,fscanf函數)
上節我們介紹了格式化輸入輸出的基本用法,上節介紹的基本概念已經夠我們前期使用,這節我們繼續對格式化輸入輸出函數進行深入介紹,并介紹格式化輸入輸出函數與流結合的用法,我們一起來看看吧~
3.1、💫...printf函數與fprintf函數
?
👉fprintf和printf函數向輸出流中寫入可變數量的數據項,并且利用格式串來控制輸出的格式。這兩個函數的定義原型都是以 ...(省略號)結尾的,表明后面還可能有可變數量的實際參數。這兩個函數的返回值都是寫入的字符數,若出錯則返回一個負值。
👉fprintf函數與printf函數唯一不同的地方就是printf函數始終指向stdout(標準輸出流)寫入內容,而fprintf函數則向它自己的第一個實際參數指定的流中寫入內容。
printf("number:%d\n",number); //寫入標準輸出流fprintf(fp,"number:%d\n",number); //寫入fp所指向的流
??下面實例展示了調用fprintf函數向fp指定文件流中寫入內容:
#include <stdio.h>
#include <stdlib.h>
int main()
{FILE* fp;fp = fopen("C:\\Users\\樊雙藝\\Desktop\\file.txt", "w+");fprintf(fp, "%s %s %s %d", "We", "are", "in", 2023);fclose(fp);return(0);
}
文件顯示內容如下:?
??
🌈可以看出:printf函數的調用等價于fprintf函數把stdout作為第一個實際參數而進行的調用。?
??和<stdio.h>中其他函數一樣,fprintf函數不僅可以把數據寫入磁盤文件,還可以用于任何輸出流,事實上,fprintf函數最常見的應用之一是——向標準誤差流(stderr)寫入出錯消息,和磁盤文件沒有任何關系。下面調用類似實例:
fprintf(stderr,"Error:data file can not be opened.\n");
👉向stderr寫入出錯消息可以保證消息輸出在屏幕上,即使用戶重定向stdout也沒關系。?
🌈【在<stdio.h>中還有另外兩個函數也可以向流寫入格式化的輸出,分別是vfprintf函數和vprintf函數,這兩個函數都不太常見,我們下節介紹。】
3.2、💫...printf函數轉換說明
👉fprintf函數和printf函數都要求格式串包含普通字符或轉換說明。普通字符會原樣輸出,而轉換說明則描述了如何把剩余的實參轉換為字符格式顯現出來。現在我們對上節課的轉換說明內容進行回顧,并補充深入內容。
👉...printf函數的轉換說明由字符%和跟隨其后的最多5個不同的選項構成:?
?
?下面進行解釋,選項的順序必須與上面一致:
🌈標志(可選項,允許多于一個)。標志 - 會導致數在欄內左對齊,而其他標志會影響數的顯示形式,如下表:
??示例:標志作用于%d轉換(其他類似),第一行顯示了不帶任何標志的效果,接下來四行分別顯示帶有標志-、+、空格、0的效果(標志#從不用于%d,關于#的示例會在介紹完轉換指定符后展示)。剩下幾行為組合標志的效果:
int main()
{int i = 123;printf( "%8d\n", i);printf( "%-8d\n", i);printf( "%+8d\n", i);printf( "% 8d\n", i);printf( "%08d\n", i);printf("%-+8d\n", i);printf("%- 8d\n", i);printf("%+08d\n", i);printf("% 08d\n", i);return 0;
}
運行結果如下:?
??
分析如下:?
🌈最小欄寬(可選項),要輸出的字符的最小數目。如果數據太小以至于無法達到這一寬度,那么會進行填充(默認情況下會在數據的左側添加空格,從而使其在欄內右對齊。)如果數據項過大以至于超過了這個寬度,那么會完整的顯示數據項。欄寬既可以是整數也可以是字符 * 。如果欄寬是字符 * ,那么欄寬由下一個參數決定,如果這個參數為負,它會被視為前面帶 - 標志的正數。
🌈精度(可選項),精度的含義依賴于轉換指定符:如果轉換指定符是d,i,o,u,x,X,那么精度表示最少位數(如果位數不夠,則添加前導0);如果轉換指定符是a,A,e,E,f,F,那么精度表示小數點后的位數;如果轉換指定符是g,G,那么精度表示有效數字的個數;如果轉換指定符是s,那么精度表示最大字節數。精度是由小數點( . )后跟一個整數或字符 * 構成的。如果出現字符 * ,那么精度由下一個參數決定,如果只有小數點,則精度為0。
??示例:最小欄寬和精度結合作用于轉換說明%s的效果
int main()
{char arr[10] = "bogus";printf( "%6s\n", arr);printf( "%-6s\n", arr);printf( "%.4s\n", arr);printf( "%6.4s\n", arr);printf("%-6.4s\n", arr);return 0;
}
?
分析如下:
🌈長度指定符(可選項)。長度指定符配合轉換指定符,共同指定轉入的實際參數的類型(例如:%d通常表示一個int值,而%hd用于顯示short int值;%ld用于顯示long int值)。?
👉另外C99中還定義了長度轉換符hh(字符型或無符號字符型) 例如:signed char/unsigned char;以及長度指定符 j 和 t ,這兩個不常見,我們以后遇到后介紹。
👉轉換指定符n(表中未指出),適配于任何整型長度指定符,長度類型符與轉換說明結合時的類型均為指針類型。
🌈轉換指定符(必有)。?轉換指定符必須是下表中列出的某一種字符。注意f、F、e、E、g、G、a和A全部設計用來輸出double類型的值。但把它們用于float類型的值也可以:由于有默認實參提升,float類型實參在傳遞給帶有可變數量實參的函數時會型自動轉換為double類。類似的,傳遞給...printf函數的字符也會自動轉換為int類型,所以可以正常使用轉換指定符c。
?
注意:
👉C99時新定義了a、A兩個轉換說明,使用格式[-]0xh.hhhhp±d的格式把double類型轉換為十六進制科學計數法形式。其中[-]是可選的負號,h代表十六進制數位,±是正號或負號,d是指數,d為十進制數,表示2的冪。a表示用小寫形式顯示a-f,A表示用大寫形式顯示A-F。
👉支持寬字符:從C99開始就可以使用fprintf來輸出寬字符。%le轉換說明用于輸出一個寬字符,%ls用于輸出一個由寬字符組成的字符串。
??示例:說明了標志#作用于o、x、X、g、G轉換效果
int main()
{int i = 123;printf( "%8o\n", i);printf("%#8o\n", i);printf( "%8x\n", i);printf("%#8x\n", i);printf( "%8X\n", i);printf("%#8X\n", i);return 0;
}
?
分析如下:
???示例:說明了%g轉換如何以%e和%f的格式顯示數
int main()
{printf("%.4g\n", 123456.);printf("%.4g\n", 12345.6);printf("%.4g\n", 1234.56);printf("%.4g\n", 123.456);printf("%.4g\n", 12.3456);printf("%.4g\n", 1.23456);printf("%.4g\n", 0.123456);printf("%.4g\n", 0.0123456);printf("%.4g\n", 0.00123456);printf("%.4g\n", 0.000123456);printf("%.4g\n", 0.0000123456);printf("%.4g\n", 0.00000123456);return 0;
}
?
分析如下:
?
🌈值得說說的是,用字符 * 填充格式串往往會帶來奇妙的結果,我們來看一個例子:
int main()
{int i = 123;printf("%6.4d\n", i);printf("%*.4d\n", 6, i);printf("%6.*d\n", 4, i);printf("%*.*d\n", 6, 4, i);return 0;
}
?
👉可以看出:這四次輸出都完全相同,用字符 * 取代最小欄寬度或者精度,為字符 * 填充的值剛好出現在待顯示的值之前。這就可以體現出字符 * 的優勢,就是在于它允許使用宏來指定欄寬或精度:
printf("%*d",WIDTH);
3.3、💫...scanf函數與fscanf函數
👉fscanf函數和scanf函數從輸入流讀入數據,并且使用格式串來指明輸入的格式。格式串后面可以有任意數量的指針(每個指針指向一個對象)作為額外的實際參數。輸入的數據項根據格式串中的轉換說明進行轉換并且存儲在指針指向的對象中。?
👉scanf函數始終從標準輸入流stdin中讀入內容,而fscanf函數則從它的第一個參數所指定的流中讀入內容:
scanf("%d%d",&i,&j); //從標準輸入流讀入fscanf(fp,"%d%d",&i,&j); //從指定流讀入
👉可以看出scanf函數的調用等同于以stdin作為第一個實際參數的fscanf函數的調用。?
??需要注意的是:如果發生輸入失敗(即沒有輸入字符可以讀)或者匹配失敗(即輸入字符和格式串不匹配),那么...scanf函數會提前返回。scanf和fscanf函數都返回讀入并且賦值給對象的數據項的數量。如果在讀取任何數據項之前發生輸入失敗,那么會返回EOF。
3.4、💫...scanf函數格式串
👉scanf函數的調用類似于printf函數的調用,但它們的工作原理完全不同,我們常把scanf函數和fscanf函數看作“模式匹配函數”,這個概念我們上節提過,這里的匹配就是指scanf函數在讀取輸入時的輸入的內容與格式串的匹配,scanf函數是一個要求極其苛刻的函數,它只要發現不匹配,函數就會返回不再進行讀取,而不匹配的字符及其以后的字符將會被“放入原處”,等待下一次讀取。
scanf函數的格式串可能含有三種信息:
🌈轉換說明:scanf函數格式串中的轉換說明類似于printf函數格式串中的轉換說明。大多數的轉換說明(除了%[ 、%c、%n例外)會跳過輸入項開始處的空白字符。但是,轉換說明不會跳過尾部的空白字符。如果輸入含有(空格123回車),那么轉換說明%d會讀取空格、1、2、3,但是會留下回車不讀取。又例如:%d%d%d?是按十進值格式輸入三個數值。輸入時,在兩個數據之間可以用一個或多個空格、tab 鍵、回車鍵分隔。
🌈空白字符:scanf格式串中的一個或多個空白字符與輸入流空白字符相匹配。
🌈非空白字符:包括一些普通字符,除%外用戶必須保證輸入的內容與格式串字符相匹配。
3.5、💫...scanf函數轉換說明
scanf函數的轉換說明由字符%和跟隨其后的下列選項(按照出現的順序)構成:
🌈字符 * (可選項)。字符 * 的出現意味著賦值屏蔽;這是一個可選的星號,表示數據是從流 stream 中讀取的,可以被忽視,表示讀入此數據但是不會把它賦值給對象。用 * 匹配的數據項不包含在scanf函數返回的計數中。(注意區別于printf函數)
🌈最大欄寬(可選項)。最大欄寬限制了輸入項字符的數量。注意printf函數中是最小欄寬,如果達到了這個最大限度,那么此數據項的轉換將結束。轉換開始處跳過的空白字符不進行統計。
🌈長度指定符(可選項)。長度指定符表明用于存儲輸入數據項的對象的類型與特定轉換說明中常見的類型長度不一致。(與printf函數長度指定符只有 [ 字符不一樣)
🌈轉換指定符(必有):轉換指定符必定是下表列出的某一種字符:
解釋如下:?
👉數值型數據項可以始終用符號(+或 -)作為開頭。然而,說明符o,u,x,X把數據項轉換成無符號的形式,所以通常不用這些說明符來讀取負數。
👉說明符 [ 是說明符s更加復雜(更加靈活)的版本,使用 [ 的完整轉換說明格式是%[集合]或者%[^集合]?,這里的集合可以是任意字符集。(但是如果 ] 是集合中的一個字符,那么它必須首先出現。)%[集合]匹配集合(即掃描集合)中的任意字符序列。%[^集合]匹配不在集合中的任意字符序列(我們可以理解為數學上的補集,我們匹配的就是補集的內容)。例如:%[abc]匹配的是只含有字母a,b,c的任何字符串,而%[^abc]匹配的是不含有字母a,b,c的任何字符串。
??示例:
#include<stdio.h>
int main()
{int a, b, c;printf("請輸入三個數字:");scanf("%d, %d, %d", &a, &b, &c);printf("%d, %d, %d\n", a, b, c);return 0;
}
?
??使用scanf時一定要注意格式串的對應,像上面的例子1,2,3之間必須輸入逗號,否則會造成匹配錯誤,如下圖:
?
123會被當成第一個輸入對象,并存入a中,接下來2與逗號不匹配,scanf提前返回,b和c的內容為無效值。?
???示例? ?轉換指定符 [ 的效果:
#include<stdio.h>
int main()
{char str[20] = { 0 };int n = 0;n = scanf("%[0123456789]", str);printf("%d %s", n, str);return 0;
}
?
可以看出對于轉換說明%[0123456789],我們輸入 123abc,它只匹配含有0123456789的字符,所以只輸出123,返回值為賦值給對象的數據項的數量。
3.6、💫檢測文件末尾和錯誤條件(clearerr函數、feof函數、ferror函數)
我們知道,...scanf函數讀入并存儲n個數據項,那么我們就希望它的返回值就是n,如果返回值小于n,那么一定是出錯了,一共有三種情況:
??文件末尾。函數在完全匹配格式串之前遇到了文件末尾。
??讀取錯誤。函數不能從流中讀取字符。
??匹配失敗。數據項的格式是錯誤的,例如,函數可能在搜索整數的第一個數字時遇到了一個字母。
但是如何知道遇到的情況是哪種呢?
👉每個流都有與之相關的兩個指示器:錯誤指示器和文件末尾指示器,當打開流時會清除這些指示器。遇到文件末尾就設置文件末尾指示器,遇到讀錯誤就設置錯誤指示器。(輸出流上發生寫錯誤時也會設置錯誤指示器。)匹配失敗不會改變任何一個指示器。
👉一旦設置了錯誤指示器或者文件末尾指示器,他就會保持這種狀態直到被顯示的清除(可能通過clearerr函數的調用)。C 庫函數?void clearerr(FILE *stream)?清除給定流 stream 的文件結束和錯誤標識符:
clearerr(fp); //同時清除指定流的文件末尾指示器和錯誤指示器
👉因為其他庫函數因為副作用可以清除某種指示器或兩種都可以清除,所以不需要經常使用clearerr函數。?
👉我們可以調用feof函數和ferror函數來測試流的指示器,從而確定出先前在流上的操作失敗的原因。C 庫函數?int feof(FILE *stream)會測試給定流 stream 的文件結束標識符,如果為與fp相關的流設置了文件末尾指示器,那么feof(fp)函數調用就會返回非零值。C 庫函數?int ferror(FILE *stream)?會測試給定流 stream 的錯誤標識符。如果設置了錯誤指示器,那么ferror(fp)函數的調用也會返回非零值,而其他情況下,這兩個函數都會返回零。?
👉如果我們想知道當scanf函數返回小于預期的值是什么情況,可以使用feof函數和ferror函數來確定原因。如果feof函數返回了非零的值,那么就說明已經到達了輸入文件的末尾。如果ferror函數返回了非零的值,那么就表示在輸入過程中產生了讀錯誤。如果兩個函數都沒有返回非零值,那么一定是發生了匹配錯誤。不管問題是什么,scanf函數的返回值都會告訴我們在問題產生前所讀入的數據項的數量。
??我們來看一個示例:應用feof和ferror函數,自定義一個搜索文件中以整數起始的行,下面是自定義函數的調用,其返回值賦值給n。
n=find_int("foo");
其中“foo”是要搜索文件的名字,函數返回找到的整數的值并將其賦值給n,如果出現問題(文件無法打開或者發生讀錯誤,再或者沒有以整數起始的行),find_int函數將返回一個錯誤的值(-1,-2,-3)?
int find_int(const char* filname)
{FILE* fp = fopen(filename, "r");int n;if (fp == NULL){return -1; //不能打開文件}while (fscanf(fp, "%d", &n) != 1){if (ferror(fp)){fclose(fp);return -2; //輸入錯誤}if (feof(fp)){fclose(fp);return -3; //找不到整數}scanf(fp, "%*[^\n]");}fclose(fp);return n;
}
分析:
?
🌈至此我們有關文件的基本操作以及對格式化輸入輸出的詳細理解就結束了~~本節主要對文件的基本操作進行介紹,下一節會對如何把單獨的字符、一行數據和塊數據怎么輸入輸出文件流進行介紹,涉及函數(putchar,getchar,fputc,fgetc)行的輸入輸出(puts,fputs,gets,fgets)塊的輸入輸出(fread,fwrite)以及字符串的輸入和輸出(sprintf,snprintf,sscanf),我們到時再進行詳細介紹。
🌈感謝各位友友們花費了寶貴的時間來閱讀本篇文章,創作不易,希望大家多多支持呀😘😘😘,如在閱讀中發現任何問題,歡迎各位友友大佬們在評論區指正支持?????????
?
?
?