1. 程式人生 > >要點4:C的檔案操作

要點4:C的檔案操作

關於檔案操作個人比較困惑的地方有兩點: 1. 關於`w`和`wb`的區別 2. 如何定位檔案的讀寫位置 # 檔案格式和開啟模式 c中的檔案開啟模式分為:文字模式和二進位制模式,分別處理文字格式檔案和二進位制格式檔案。 **兩個模式的主要區別是在換行符的處理上**,利用文字模式在寫文字內容到檔案的時候,需要將換行符轉換成系統對應的編碼方式. **系統不同,對換行符的表示方式也是不一樣的**,例如`unix`系統是`\n`,而`MS-DOS`是`\r\n`,`Mac`是`\r`。`C`裡面都是用`\n`作為換行符的,所以在文字寫入時,底層需要將`C`形式換行符`\n`做對應的轉換之後寫入檔案,讀取檔案時將對應系統的換行符轉成`C`形式的。因為`unix`系統的換行符是`\n`,這和`C`形式一致,所以`unix`系統下文字模式和二進位制模式沒有區別。 C中使用`fopen`函式建立檔案控制代碼,函式原型如下: ```c FILE *fopen(const char *filename, const char *mode) ``` `filename`表示檔案路徑,`mode`表示開啟模式,成功返回一個檔案控制代碼指標,失敗返回null。 `mode` 有下列幾種形態字串: - `r` 以只讀方式開啟檔案,該檔案必須存在。 - `r+` 以可讀寫方式開啟檔案,該檔案必須存在。 - `rb+` 讀寫開啟一個二進位制檔案,允許讀資料。 - `rw+` 讀寫開啟一個文字檔案,允許讀和寫。 - `w` 開啟只寫檔案,若檔案存在則檔案長度清為0,即該檔案內容會消失。若檔案不存在則建立該檔案。 - `w+` 開啟可讀寫檔案,若檔案存在則檔案長度清為零,即該檔案內容會消失。若檔案不存在則建立該檔案。 - `a` 以附加的方式開啟只寫檔案。若檔案不存在,則會建立該檔案,如果檔案存在,寫入的資料會被加到檔案尾,即檔案原先的內容會被保留。(EOF符保留) - `a+` 以附加方式開啟可讀寫的檔案。若檔案不存在,則會建立該檔案,如果檔案存在,寫入的資料會被加到檔案尾後,即檔案原先的內容會被保留。 (原來的EOF符不保留) - `wb` 只寫開啟或新建一個二進位制檔案;只允許寫資料。 - `wb+` 讀寫開啟或建立一個二進位制檔案,允許讀和寫。 - `ab+` 讀寫開啟一個二進位制檔案,允許讀或在檔案末追加資料。 - `at+` 開啟一個文字檔案,`a`表示`append`,就是說寫入處理的時候是接著原來檔案已有內容寫入,不是從頭寫入覆蓋掉,`t`表示開啟檔案的型別是文字檔案,`+`號表示對檔案既可以讀也可以寫。 上述的形態字串都可以再加一個`b`字元,如`rb`、`w+b`或`ab+`等組合,加入`b` 字元用來告訴函式庫以二進位制模式開啟檔案。**如果不加`b`,表示預設加了`t`**,即`rt`,`wt`,其中`t`表示以文字模式開啟檔案。 在`windows`上分別利用`w+`和`wb+`模式測試一下文字模式和二進位制模式寫資料的區別: ```c #include #include int main(int argc, char* argv[]) { // 檔案w+.txt FILE *fp1 = fopen(".\\w+.txt", "w+"); if (!fp1) { fputs("檔案開啟錯誤!", stdin); return EXIT_FAILURE; } fprintf(fp1, "%s", "The first line!\nThe second line!\n"); // 寫入內容中帶有換行符 fclose(fp1); // 檔案wb+.txt FILE* fp2 = fopen(".\\wb+.txt", "wb+"); if (!fp2) { fputs("檔案開啟錯誤!", stdin); return EXIT_FAILURE; } fprintf(fp2, "%s", "The first line!\nThe second line!\n"); // 寫入內容中帶有換行符 fclose(fp2); return EXIT_SUCCESS; } ``` 左側顯示的是`w+.txt`,右側顯示的是`wb+.txt`,明顯可以看出儲存的換行符是有區別的,`wb+`模式沒有將C程式碼中的`\n`進行特殊處理: ![](https://cdn.jsdelivr.net/gh/cnsimo/pic_bed/20200821014552.png) # 檔案讀寫位置定位 如果可以在訪問檔案的時候,能夠直接定位到某個位置進行讀取,那就可以實現像陣列一樣隨機訪問了。 C語言提供了幾個相關的函式,他們的原型如下: ```c int fseek( FILE *stream, long offset, int origin ); long ftell( FILE *stream ); int fgetpos( FILE *restrict stream, fpos_t *restrict pos ); int fsetpos( FILE *stream, const fpos_t *pos ); void rewind( FILE *stream ); ``` 其中,`rewind` 函式用於將檔案內部的位置指標重新指向一個流(資料流或者檔案)的起始位置。這裡需要注意的是,這裡的“指標”表示的不是檔案指標,而是檔案內部的位置指標。即隨著對檔案的讀寫,檔案的位置指標(指向當前讀寫位元組)向後移動。而檔案指標指向整個檔案,如果不重新賦值,檔案指標不會發生改變。 例如,使用`w+`模式開啟一個檔案寫入內容之後,再輸出檔案內容,程式碼可以這麼寫: ```c #include #include #define MAXLEN 80 int main() { // 開啟檔案 char filename[MAXLEN] = ".\\test.txt"; FILE* fp = fopen(filename, "w+"); if (!fp) { fputs("檔案開啟失敗!", stdout); exit(EXIT_FAILURE); } // 寫入文字 char* text = "This is a test file!"; fputs(text, fp); // 還原位置指標 rewind(fp); // 讀取檔案內容 char c; while ((c = fgetc(fp)) != EOF) { putchar(c); } // 關閉檔案 fclose(fp); return EXIT_SUCCESS; } ``` `rewind`功能比較簡單,只能用於返回到檔案開頭,如果想要跳轉到其他位置,則`fseek`功能更加強大,它用來設定檔案的讀寫位置,可以實現檔案的隨機訪問。 `fseek`的三個引數, 第一個是檔案控制代碼,第三個引數是基準位置,第二個是相對於基準位置的偏移處,基準位置有三個: |名稱|代表位置|值形式| |------|------|-----| |SEEK_SET|檔案首部|0| |SEEK_CUR|當前位置|1| |SEEK_END|檔案尾部|2| 示例程式碼: ```c #include #include int main() { // 開啟檔案 FILE* fp = fopen(".\\test.txt", "w+"); if (!fp) { fputs("檔案開啟失敗!", stdout); exit(EXIT_FAILURE); } // 先寫入123,然後改成abc fputc('1', fp); fputc('2', fp); fputc('3', fp); // 先將指標轉到中間改b fseek(fp, -2, SEEK_END); fputc('b', fp); // 將指標轉到開頭改a fseek(fp, 0, SEEK_SET); fputc('a', fp); // 將指標轉到第三個字元改c fseek(fp, 1, SEEK_CUR); fputc('c', fp); // 關閉檔案 fclose(fp); return EXIT_SUCCESS; } ``` 需要注意的是,`SEEK_END`指向了檔案結尾,所以需要向前偏移2,才能將指標指到`1`的後面。 對於以文字模式開啟的流,使用fseek函式時候需要注意,因為'\n'換行符與系統換行符之間的轉換會導致`fseek`產生意外的結果。**`fseek`只有在下面兩種情況下才能保證當檔案以文件模式開啟時能正確使用`fseek`函式**: - 與起始位置相對偏移為0的重置,即沒有改動指標位置 - `origin`設定為`SEEK_SET`,`offset`為呼叫`ftell`返回的值時進行的指標位置重置情況 # fsetpos和fseek `fsetpos`/`fgetpos`和`fseek`/`ftell`感覺很像,剛開始覺得他們可以用來互相替換,`fsetpos`也可以用來實現隨機訪問,後來發現錯了,`fseek`之所以能夠實現隨機訪問檔案是因為可以傳入一個整型的引數作為檔案偏移,而`fsetpos`接收的引數是`fpos_t *`,這個`fpos_t`只能使用通過`fgetpost`返回的值,不能直接指定,所以兩者還是有區