標準開讀檔案

一個程式怎麼儲存輸入過的資料呢?總要有一個地方記錄吧!那麼記在文字檔裡,或是二元檔裡,C提供一套標準的函式庫讓我們使用容易的讀取寫入檔案,其實檔案也是像stream一樣有如同stdin和stdout所以這裡我們來討論一下,如何使用標準的函式庫來開讀檔案。

※ 檔案的觀念

在使用C來開讀檔案,其實是非常容易的,因為一旦建立好了之後,使用起來就像是stdin stream一樣,寫入的時候也像printf一樣,但是開讀檔案分成兩種,一種是正常的文字檔案文件,一種是二元檔案文件,文字檔案文件就是我們平常的 txt檔,而二元檔案你用notepad打開是沒有意義的,因為都是乩碼,它是給機器看的二元檔。

※ 開讀檔案

Example:

FILE* fp;
if (!(fp = fopen("data.txt", "r")))
{
puts("open error");
exit(2);
}
fclose(fp);

* FILE *fopen( const char *fname, const char *mode ); 建立初使化檔案指標
* fname是指要開的檔案名字
* mode是指以什麼模式開啟
* 如果正確的開啟檔案會回傳指向檔案的指標,但是失敗會回傳NULL

所以我們開讀檔案第一個步驟就是建立檔案指標,接著就是fopen,但是我們要知道檔案到底有沒有開啟成功,所以就一定要使用if來判斷,如果失敗的話,可以回應一些訊息給使用者,看要做什麼處理,這個地方是假設這個檔案和程式非常具有相依性,所以程式之中會大大使用到,因為沒有開啟成功的話,直接結束程式執行,但是一個好的程式,應該具有錯誤回覆能力,但這不在我們討論的範圍,一但沒有回傳NULL那就是開啟成功了,我們就可以對此檔案操作。

當你檔案在程式之中不使用了,記得要立刻的使用fclose來關閉檔案,因為檔案開啟是會占用資源的,有效的使用才是正確的,所以當不用的時候記得要釋放。

※ 檔案的開啟模式

 mode | 作用 | 檔案不存在 | 檔案存在
------+----+-------+--------
  r   | 讀  |       |
------+----|  錯誤   |   開啟
  r+  | 讀寫 |       |
------+----+-------+--------
  w   | 寫  |       |
------+----|  建檔   | 開啟清除內容
  w+  | 讀寫 |       |
------+----+-------+--------
  a   | 加  |       |
------+----|  建檔   | 開啟寫入檔尾
  a+  | 讀加 |       |
------+----+-------+--------

其中如果要使用二檔開檔模式的話,在模式中加入字元b就可以了,如rb就是以二元模式讀檔,不同的模式開檔會造成的結果不一樣,如果你之前寫了一個檔案很辛苦寫了很多東西,但是使用"w"來開,一開之後你檔案東西全光了,所以每個模式都有不一樣的用途,所以不可以乩使用。

※ 一般開檔與二元開檔

差別只在於有沒有加上「b」這個模式而已,但是開出來的卻是差別很大的喔!一般檔案記錄都是字元就是ascii code但是二元檔記錄的都是機器碼,所以如果你以二元開檔,且用fscanf來讀取資料會造成錯誤,舉例來了,在windows系統上看到的換行字元為 /r/n和linux or unix系統就不用了,unix like的系統都是\n這樣就有差別了,但是使用一般開檔模式的話,是看不到的,因為C的標準函式庫已經幫你做掉了,已經統一成為\n所以你在判斷資料輸入的時候,不會遇到什麼問題,但是如果你今天使用二元模式的話,那你就要自行判斷了。

一般開檔這麼好,那幹麻不用一般開檔就好了,還要使用二元開檔,先來思考一個問題就是,當你1/3會怎麼樣,應該是無限的小數吧!但是如果你用一般檔案存,你最多存到4位小數,就不想存了,而使用二元檔,它是直接將型別寫入就是把double 8 bytes直接由二元碼寫入檔案之中,這樣的話你不會失去你的精準度,這樣就比一般開檔好用多了。

而且二元開檔,可以做隨機存取,可以取得你寫入某個區塊的二元碼,如取得mp3的檔案資訊就是檔尾的1024bytes,這是一般文字模式開檔做不到的。

※ 一般開檔的操做

如果使用一般文字檔開啟模式來操做檔案的話,那使用起來和stdin, stdout沒有什麼差別,只是你使用輸出入函式都要用具有檔案指標的輸出入函式才可以存取檔案,如最基本的幾個函式如下。

* int fgetc( FILE *stream ); 從檔案取得一個字元
* int fputc( int ch, FILE *stream ); 寫入一字元到檔案
* char *fgets( char *str, int num, FILE *stream ); 從檔案取得一行字串
* int fputs( const char *str, FILE *stream ); 寫入字串到檔案中
* int fscanf( FILE *stream, const char *format, ... ); scanf檔案版
* int fprintf( FILE *stream, const char *format, ... ); printf檔案版

以上的函式你一定都用過,只是都用在stdin和stdout但是檔案的話也是一樣,所以如以下範例。

Example:

#include
#include
#include
#include
#include "gear_string.h"
#include "gear_io.h"
#define MAX_SIZE 80
#define IT_SIZE 20

typedef struct _item
{
char name[MAX_SIZE];
int value;
} Item;
typedef struct _box
{
Item it[IT_SIZE];
int mount;
} Box;

void init(FILE* fp, Box* itbox);
void show(Box* itbox);
static void add_item(Item* it, char* buf);
int main()
{
Box itbox;
FILE* fp = fopen("data.txt", "r");
init(fp, &itbox);
fclose(fp);
show(&itbox);
return 0;
}
void show(Box* itbox)
{
int i;
for (i = 0; i <>mount; ++i)
{
printf("str%d: %s and value%d: %d\n", i, itbox->it[i].name, i, itbox->it[i].value);
}
}
void init(FILE* fp, Box* itbox)
{
if (!fp)
{
puts("file open error!!");
exit(1);
}
char strbuf[MAX_SIZE];
int it_index = 0;
itbox->mount = 0;
while (fgets(strbuf, sizeof(strbuf), fp))
{
add_item(&itbox->it[it_index++], strbuf);
++itbox->mount;
}
}
static void add_item(Item* it, char* buf)
{
int index = 0;
while (*buf && *buf != ':')
{
it->name[index++] = *buf;
++buf;
}
it->name[index] = '\0';
sscanf(buf+1, "%d", &it->value);
}

以上程式是把一個檔案中的資料如下:

Memory Size: 1024
Cache Size: 16
Cache Block: 4
Set Associativity: 1

介由檔案讀取,將裡面的資訊存到我們自訂的結構之中,方面來取用資料,每一個項目為一個item所以它有名字和數值,而Box就是item的集合,這是抽象資料型別的設計方式,以後我們在來討論,重點是範例之中有使用到如何開檔讀取裡面的資料這才是重要的,主要就是init那個函式,如何初始一個檔案指標,而且模組化封裝,這樣會更好。

※ 二元檔的檔案操做

而且開檔的話就要使用特別的函式了,因為在二元檔中,看到的是二元機器碼,而不是ascii code所以讀取資料的時候,不能使用scanf 或是printf這樣的函式來讀取資料要用下面的函式來讀取資料。

* int fread( void *buffer, size_t size, size_t num, FILE *stream ); 二元讀檔
* buffer 要存的位址
* size 每一個元素多少byte
* num 要讀幾個元素
* stream 檔案指標
* 回傳是讀到的數量,如果有錯誤就是沒有讀到,回傳0

也有二元檔專用的寫入資料的函式,它和fread非常的類似,我們可以用它來寫入二元檔案之中。

* int fwrite( const void *buffer, size_t size, size_t count, FILE *stream ); 二元寫檔
* buffer 要讀取的位址
* size 每一個元素多少byte
* count 要寫入幾個元素
* stream 輸出的檔案指標
* 回傳是寫入成功的數量,所以只要寫入數不等於回傳的數量就是有出錯了

Example:

int main()
{
char temp[1024];
int bytes;
FILE* fp;
if (!(fp = fopen("data.dat", "rb")))
{
puts("open error");
exit(2);
}
while ((bytes = fread(temp, sizeof(char), 10, fp)))
{
fwrite(temp, sizeof(char), bytes, stdout);
}
fclose(fp);
return 0;
}

以上的程式我想很容易就看出來是在做什麼雖然把temp開那麼大,但是也只有前面10個byte有用到,我們從fp讀取10個byte,如果 fread成功的話,那就會回傳讀到10個byte,那如果有問題的話會讀到其它的數字,但是如果是0的話就是讀光光了,檔案指標已經到檔尾了,每次只要有讀到幾個byte就全部寫到stdout就是銀目去,讀多少個寫多少個,這就是二元讀檔。

※ 檔案指標的操做

一定覺得很奇怪檔案指標還可以操做喔!沒錯,因為我們讀取資料都是使用檔案指標所指向的stream的內容來取得資料的,所以我們可以移動它,如移到檔頭一開始處,或是移動到檔尾,或是任何的位子(但是這只限於使用二元檔),因為只有二元檔才可以計算要位移多少個byte,而一般的讀檔是不行的。

* int fseek( FILE *stream, long offset, int origin ); 移動檔案指標
* stream 是檔案指標
* offset 位移幾個byte可以為負值,向前移動,正值向後移動
* origin 起始點分為SEEK_SET, SEEK_CUR, SEEK_END 分別為最前面,目前位子和最後面(檔尾)
* 回傳0代表成功移動了,不為0代表失敗

Example:

#include
#include

int main()
{
FILE* fp;
if (!(fp = fopen("data.txt", "rb")))
{
perror("");
exit(2);
}
fseek(fp, -10, SEEK_END);
char str[10];
if (fread(str, sizeof(char), 10, fp))
{
puts(str);
}
return 0;
}

上面範例我們有一個data.txt的文字檔,都是由文字組成的,我先使用fseek移動到檔案尾,且offset 10 bytes為的是我們只要取得最後10 bytes的文字,之後透過fread直接讀取且存到我們的buffer之中,雖然印出來是乩碼(因為沒有\0)但是實際上fread是有完整的把資料讀入到str之中的,使用gdb等工具軟體可以發現,其實這樣用很免強的,因為fseek是用在二元檔案移動,而不是文字檔,但是檔案指標的移動是兩者都可以適用的。

* long ftell( FILE *stream ); 計算檔案指標的位子
* stream 檔案指標
* 回傳目前檔案指標指的位子

Example:

#include

int main()
{
FILE* fp;
if (!(fp = fopen("data.txt", "r")))
{
perror("");
exit(2);
}
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
printf("file size: %u", file_size);
return 0;
}

我們可以計算出來檔案的大小為多少位元組,不信你可以自己試看看,用windows的內容看看,但是別忘了,你現在的fseek把指標移動到最尾端了,如果讀資料一定讀不到的,所以請記得要在移動到最前面使用fseek(fp, 0, SEEK_SET);

※ 其它開讀檔案常用到函式

* void rewind( FILE *stream ); 移動檔案指標到檔頭
* stream 檔案指標

這就是fseek(fp, 0, SEEK_SET)的簡易版,讓你不用打那麼多字,功能是一樣的,通當會移動到檔為,是為了配合ftell來使用要計算檔案的大小,為的是可能要用動態配置。

* int ferror( FILE *stream ); 檢查我們的檔案指標是否有錯誤
* 回傳0表示沒有錯誤,不是0就是有錯誤了

ferror常常會用到,通常會配合perror來使用,在範例了我用了很多次了,現在就來介紹一下

* void perror( const char *str ); 印出目前檔案指標錯誤資訊
* str 可以讓你註名是那個檔案出錯的字串

當由ferror來查出錯誤的時候你設計perror("data.txt")那個會印出可能如下的資訊data.txt: file not exist所以str是一個字串只是讓你註名錯誤來自那個檔案而已,看你喜歡怎麼加字都可以。

* int feof( FILE *stream ); 檢查檔案到檔尾
* stream 檔案指標
* 回傳不是0表示到達檔案尾了

這可以讓你檢查如果到達檔案就該停止使用fread來讀取,或是該使用rewind來讓指標移動到檔案頭,否則會造成程式出問題,是個非常好用的工具。
[下一頁...]
※ 開讀檔案範例

這裡我們練習從一個學生資料的檔案為一般文字檔,轉存為二元檔,再由二元讀檔把檔案讀出來輸出,但是二元讀檔我們使用兩種方式,一種是利用推疊空間來存取,另一種是用動態配置來存取。

Example:

stud_data.txt

932361 Alex 93
932363 Keroro 99
932362 ICE 85

fseek_write.c

#include
#include

typedef struct _student
{
int id;
char name[20];
int score;
} Student;

int main()
{
FILE* f_in;
FILE* f_out;
Student stud[20];
int count_stud = 0;
if (!(f_in = fopen("stud_data.txt", "r")))
{
puts("can't read file in");
exit(1);
}
else if (!(f_out = fopen("student.bin", "wb")))
{
puts("can't create binary file");
exit(1);
}
char input_buffer[80];
while (fgets(input_buffer, sizeof(input_buffer), f_in))
{
Student temp_stud;
sscanf(input_buffer, "%d%s%d", &temp_stud.id, temp_stud.name, &temp_stud.score);
stud[count_stud++] = temp_stud;
} fclose(f_in);
fwrite(stud, sizeof(Student), count_stud, f_out);
fclose(f_out);
return 0;
}

fseek_read.c

#include
#include
#define BUFFER_SIZE 2

typedef struct _student
{
int id;
char name[20];
int score;
} Student; int main()
{
FILE* fp;
if (!(fp = fopen("student.bin", "rb")))
{
puts("can't open binary student.bin");
exit(1);
}
int n;
printf("enter mothod: ");
scanf("%d", &n);

if (n == 1)
{
/*one*/
fseek(fp, 0, SEEK_END);
long filesize = ftell(fp);
rewind(fp);

int mount = filesize / sizeof(Student);
Student* stud_ptr = (Student*)malloc(sizeof(Student) * mount);
fread(stud_ptr, sizeof(Student), mount, fp);
fclose(fp); int i;
printf("mount: %d\n", mount);
for (i = 0; i < mount; ++i)
{
printf("id: \t%d\n", stud_ptr[i].id);
printf("name: \t%s\n", stud_ptr[i].name);
printf("score: \t%d\n", stud_ptr[i].score);
puts("**************");
}
free(stud_ptr);
}
else
{
/*two*/
Student stud_ptr[BUFFER_SIZE];
int mount;
while ((mount = fread(stud_ptr, sizeof(Student), BUFFER_SIZE, fp)))
{
int i;
printf("mount: %d\n", mount);
for (i = 0; i < mount; ++i)
{
printf("id: \t%d\n", stud_ptr[i].id);
printf("name: \t%s\n", stud_ptr[i].name);
printf("score: \t%d\n", stud_ptr[i].score);
puts("**************");
}
}
fclose(fp);
printf("mount: %d\n", mount);
}
return 0;
}

在這程式中我們定義了我們自己的結構就是student它可以存名字學號和分數主要就是拿存來放資料用的fseek_write.c先把檔案製成 student.bin之後再利用fseek_read.c把資料讀出來,如果你選擇一的話會使用動態配置,但是選二的話會用推疊方式把資料取出,兩種都有它的實用性,看看到時候開發什麼專案用什麼不同的方式。

0 意見: