2021年4月7日 星期三

C 讀寫檔案 範例代碼 [行讀寫.一次讀寫全部 ]

C 讀取檔案 範例代碼

如果是可以使用C++的環境,建議是用C++比較好寫也比較不用擔心指標的問題,C++的範例代碼可以參考這一篇。
https://charlottehong.blogspot.com/2021/04/c.html

寫了兩個版本,一個是單純函式讀取要自己管控字串的記憶體。另一個是類似於封裝成物件的方式自動管理,新手用第二個可能比較好,省去字串管理記憶體的問題,這問題通常是新手出bug的點。

不囉嗦直接上代碼

1. 讀檔的純C函式

使用方式直接輸入檔名就好,會自動讀出來,檔案記得自己創建好。

#include <stdio.h>
#include <stdlib.h>

void ReadFile_Example(const char* name) {
    FILE* pFile;
    long lSize;
    char* buffer;
    long result;

    // 開檔
    if (fopen_s(&pFile, name, "rb") || pFile == NULL) {
        fputs ("File error",stderr); exit (1);
    }
    // 獲取檔案長度
    fseek (pFile , 0 , SEEK_END);
    lSize = ftell (pFile);
    rewind (pFile);
    // 創建所需長度
    buffer = (char*)malloc(sizeof(char)*lSize+1);
    if (buffer == NULL) {fputs ("Memory error",stderr); exit (2);}
    buffer[0] = 0;
    // 讀取檔案
    result = fread(buffer, sizeof(char), lSize, pFile);
    if (result != lSize) {fputs ("Reading error",stderr); exit (3);}
    buffer[lSize] = 0;
    printf("%s", buffer);
    // 釋放資源
    fclose (pFile), pFile=NULL;
    free (buffer), buffer=NULL;
}

int main(int argc, char const* argv[]) {
    ReadFile_Example("a.txt");
    return 0;
}

好啦~拿了就用了,沒看過這麼貼心的教學文吧,貼上直接能執行不囉嗦。
下面開始正經解說了。




開檔案 fopen()

FILE* pFile;
// 方法1 (推薦)
fopen_s(&pFile, fileName, "rb");
// 方法2
pFile = fopen(fileName, "rb");

就這樣簡單一行就可以打開了,這邊我用比較新的標準 fopen_s() 而不是 fopen() 差別在於多一個 s 是更安全的意思,詳細在自己google查一下吧,總而言之是一樣的東西寫法有一點點不同而已。

打開之後需要檢查檔案有沒有開成功,失敗的原因有很多,最常見的是檔名打錯。失敗並不會像C++直接噴例外大可無腦不管。

檢測方式直接看他返回的指標是NULL還是有東西就可以了。他可以直接寫在if裡面節省行數,像底下這個樣子。

// 開檔
void openFile(FILE** p, const char* name, const char* mode) {
    if (fopen_s(p, name, mode)) {
        fputs ("Open file error",stderr); exit (1);
    }
}

這邊簡單處理直接噴一個 exit (1) 就不管了,如果開檔失敗還有相對應的例外處理就是從返回值那邊下手,返回布林值1或0表示成功與失敗。

直接 exit(1) 就是一旦失敗就直接結束程式了。返回值是失敗可以有對應方式,重開或是跳過之類的選項。

函式的使用方式像這個樣子

FILE* pFile;
openFile(&pFile, "a.txt", "rb");

獲取檔案長度

讀取的方式也是有依照特定長度讀取 fread( )與內建的行讀取 getline(),如果想用前者一口氣把全部讀完就需要知道檔案有多長了。

計算長度的方式是利用讀取指標計算的,他就像你打開記事本的時候出現的游標一樣意思,一格一格慢慢前進,讀到最後面遇到結束符號為止。

想知道檔案長度就把游標移動到最後面,然後算一下多少格就是了,最後再把檔案移動回原本的位置即可。

long getFileSize(FILE* p) {
    long temp = ftell (p), lSize = 0;
    fseek (p , 0 , SEEK_END);
    (lSize) = ftell (p);
    fseek (p , temp , SEEK_SET);
    return lSize;
}

fseek() 中的是表示偏移距離與相對位置。定位參數總共有3個。

// 從結束點開始計算偏移0格
fseek (p , 0 , SEEK_END);
// 從起點開始計算偏移 temp 格
fseek (p , temp , SEEK_SET); 
// 從當前位置開始計算偏移 -1 格
fseek (p , -1 , SEEK_CUR);

創建動態陣列

也可以用非動態的陣列存,只是檔案大小不可預測,管控動態陣列是不可少的。這邊直接提供一個函式給大家用。

檢測兩次NULL的意思是因為,第一檢測要知道進來的東西有沒有連接著別的動態陣列,要是不檢查直接把原本指的的東西給丟了,直接就 memory leak 了。第二次檢測是要知道系統有沒有真的配發下來,記憶體要是滿了系統是沒辦法給的。

最後再給個結束符,字串的操作千萬記得給一下,沒有給沒辦法正常讀。有所有的字串相關函式都是依靠結束符運作的。

// 創建動態陣列
void createString(char** str, int len) {
    if (*str != NULL) {fputs ("str is not NULL",stderr); exit (2);}
    *str = (char*)malloc(sizeof(char) * len);
    if (*str == NULL) {fputs ("Memory error",stderr); exit (2);}
    *str[0] = 0;
}

要新增就這樣用,這樣就會自動建立了。只是用完後要記得自己釋放。

char* str=NULL;

createString(&str, 10);
printf("%s\n", str);

free(str), str=NULL;

不是太大重點,如果不明白什麼意思,複製貼上就是了。

讀取檔案

讀取的參數 read()

// 讀取檔案
fread(buffer, sizeof(char), lSize, pFile);
buffer[lSize] = 0;
printf("%s", buffer);

buffer 是依據檔案長度建立的字串 char*
lSize 是檔案的總長度

其中間的這一行特別重要

buffer[lSize] = 0;

因為讀檔並不包含字串的結束字符,必須自己設置。估計很多人卡關會卡在這裡,這也引出一個問題是字串的創建長度必須是,檔案長度的+1才可以,還要裝最後一個結束符。

以上就完成拉,想練習可以把三個函式奏起來,組合成一個可以讀檔的程式,當作練習。

打開的參數

簡單整理一下懶人包

模式參數說明
r[讀檔], 有+號時 檔案必須存在不會自動建立
w[寫檔], 有+號時 檔案可以不存在會自動建立新的
b在 Windwos 下必須追加
+可讀可寫
a從尾部追加

其中 b 的意思直接把他當作windwos上必須寫上就好了。不然讀取的時候微軟的換行標記 \r\n 會被讀成只有 \n 導致長度錯了,進而導致可能越界讀到垃圾值。
可以參考這一篇:https://blog.csdn.net/cattylll/article/details/7107089

詳細說明看下面的表格,最常用的就

參數詳細說明

模式參數說明
r開啟檔案進行唯讀,若檔案不存在,則傳回NULL
w開啟檔案進行唯寫,若檔案不存在,則建立新檔,若檔案存在則將之刪除,再建立新檔
a開啟檔案進行附加,若檔案存在,則資料從檔案尾端寫入,若檔案不存在則建立新檔
rb以二進位模式開啟檔案進行唯讀
wb以二進位模式開啟檔案進行唯寫
ab以二進位模式開啟檔案進行附加
r+開啟檔案進行讀寫,若檔案不存在,則建立新檔,若檔案存在,資料將從檔案開頭進行覆寫
w+開啟檔案進行讀寫,若檔案不存在,則建立新檔,若檔案存在則覆寫原有的資料
a+開啟檔案進行附加、讀取,若檔案不存在則建立新檔,若檔案存在,則資料從檔案尾端寫入
r+b以二進位方式開啟檔案進行讀寫
w+b以二進位方式開啟檔案進行讀寫
a+b以二進位方式開啟檔案進行附加、讀取







2. 封裝好的物件

對於新手來說,讀檔最大的難關應該是在字串的操作吧,必須自己手動管理。C是沒有物件的,指標的操作只能自己小心點知道在幹嘛。

我就就大概遵從物件的規則仿製了一個,類物件的操作方式,想練習可以看看我怎麼寫的,照著這個方式去寫程式,寫檔案的函式我留著沒寫,當練習把它寫出來吧。

代碼放最後面,先簡單說明一下怎麼用。

使用方式

宣告變數用這樣的方式

FileData* file = newFileData();

讀取檔案內建會自動管理記憶體直接取就好

FileData_getFileString(file, "a.txt");
printf("%s\n\n", file->data);

最後結束的時候記得釋放資源,在釋放之前是可以連續讀取不同檔案的。

FileData_dtor(file);

代碼

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

typedef struct FileData{
    FILE* pFile;
    char* data;
    long size;
    long capa;
    bool init;
} FileData;
// 檢查是否由建構子初始化
void FileData_check(FileData* f) {
    if (f->init != true)     {
        fputs ("FileData not init.",stderr); exit (1);
    }
}
// 初始化
FileData* newFileData() {
    FileData* p = (FileData*)malloc(sizeof(FileData));
    if (p == NULL) {
        fputs ("Memory error",stderr); exit (2);
    } else     {
        p->pFile = NULL;
        p->data = NULL;
        p->size = 0;
        p->capa = 0;
        p->init = true;
    }
    return p;
}
// 解構
void FileData_dtor(FileData* f){
    FileData_check(f);
    if (f->pFile != NULL)     {
        fclose (f->pFile);
        f->pFile = NULL;
    }
    if (f->data != NULL)     {
        free (f->data);
        f->data = NULL;
        f->size = 0;
        f->capa = 0;
    }
}
// 讀取檔案到字串
void FileData_getFileString(FileData* self, const char* name) {
    // 檢查
    FileData_check(self);
    if (self->pFile != NULL) {
        fclose (self->pFile);
        self->pFile = NULL;
    }
    // 宣告
    FILE** pFile = &(self->pFile);
    char** buffer = NULL;
    // 開檔
    if (fopen_s(pFile, name, "rb") || !(*pFile)) 
        fputs ("Open file error",stderr),  exit (1);
    // 獲取檔案大小
    fseek (*pFile , 0 , SEEK_END);
    self->size = ftell (*pFile);
    fseek (*pFile , 0 , SEEK_SET);
    // 如果容量不足,重建所需長度
    buffer = &(self->data);
    if (self->capa <= self->size) {
        char* pStr= NULL;
        free(*buffer);
        pStr = (char*)malloc(sizeof(char) * self->size+1);
        self->capa = self->size+1;
        if (pStr == NULL) {fputs ("Memory error",stderr); exit (2);}
        *buffer = pStr;
    }
    fread(*buffer, sizeof(char), self->size, *pFile);
    (*buffer)[self->size] = 0;
    //printf("%s\n", *buffer);
    // 釋放資源
    fclose (*pFile);
}
// 讀取一行到字串
bool FileData_getFileLine(FileData* self, const char* name, long len) {
    // 檢查
    FileData_check(self);
    // 宣告
    FILE** pFile = &(self->pFile);
    char** buffer = NULL;
    // 開檔
    if(self->pFile == NULL){
        if (fopen_s(pFile, name, "rb") || !(*pFile)) 
            fputs ("Open file error",stderr),  exit (1);
    }
    // 獲取檔案大小
    self->size = len;
    // 如果容量不足,重建所需長度
    buffer = &(self->data);
    if (self->capa <= self->size) {
        char* pStr= NULL;
        free(*buffer);
        pStr = (char*)malloc(sizeof(char) * self->size+1);
        self->capa = self->size+1;
        if (pStr == NULL) {fputs ("Memory error",stderr); exit (2);}
        *buffer = pStr;
    }
    if (fgets(*buffer, self->size, *pFile) != NULL) {
        return true;
    }
    // 沒有輸出清空
    *buffer[0] = 0;
    return false;
}
// 覆蓋寫檔案
void FileData_overWrite(FileData* self, const char* name, const char* inStr) {

}

// 測試
void test_getFileString() {
    FileData* file = newFileData();
    FileData_getFileString(file, "b.txt");
    printf("%s\n\n", file->data);
    FileData_getFileString(file, "a.txt");
    printf("%s\n\n", file->data);
    FileData_getFileString(file, "c.txt");
    printf("%s\n\n", file->data);
    FileData_dtor(file);
}
void test_getFileLine() {
    FileData* file = newFileData();

    while(FileData_getFileLine(file, "a.txt", 100)) {
        printf("%s", file->data);
    } printf("\n");

    fseek (file->pFile , 0 , SEEK_SET);
    while(FileData_getFileLine(file, "a.txt", 101)) {
        printf("%s", file->data);
    } printf("\n");

    FileData_dtor(file);
}
//==============================================================
int main(int argc, char const* argv[]) {
    test_getFileString();
    test_getFileLine();
    return 0;
}
//==============================================================

1 則留言:

  1. 完整的封裝物件代碼可以看這裡:
    https://ideone.com/y7EfL3

    回覆刪除