1. 程式人生 > >C語言中的檔案流

C語言中的檔案流

所謂檔案(file)一般指儲存在外部介質上資料的集合,比如我們經常使用的mp3、mp4、txt、bmp、jpg、exe、rmvb等等。這些檔案各有各的用途,我們通常將它們存放在磁碟或者可移動盤等介質中。那麼,為什麼這裡面又有這麼多種格式的檔案呢?原因很簡單,它們各有各的用途,區分就在於這些檔案裡面存放的資料集合所遵循的儲存規則不一樣。舉個例子比如bmp圖片檔案,為什麼他能夠表示一張圖片,因為它有固定的格式,哪一段到哪一段,哪個偏移到哪個偏移應該存放什麼資料是規定好了的。比如有檔案頭,一般是一個結構體,存放的檔案的一些資訊,如圖片的大小,畫素等等。再後來有資料區。然後我們要顯示一張圖片,就只需要按照前面所說的規則將檔案頭結構和資料塊讀出來,然後將這些資料在螢幕上用顏色表示出來,就成了一張圖片。其它檔案格式也類似。

        這裡要說一個更重要的例子,對我們理解檔案有好處。那麼這個檔案就是exe檔案(這裡只討論windows平臺),通常我們認為它是一個可執行程式,這無疑是增加了它的神祕度。從本質上來講exe無非是一種固定的檔案格式罷了。既然這樣,它就有一套自己的儲存規則。跟前面的圖片檔案一樣有規則。此時,你可能會問:你這麼說那我就可以純手工(直接填寫資料填充檔案)寫出一個exe可執行檔案了? 面對你這個問題,我只能說你已經習慣思考了,已經習慣給自己提問了,已經很聰明瞭。那麼答案是肯定的,你完全可以用一個編輯器直接填寫資料寫出一個helloworld.exe檔案或者helloworld.dll檔案。因為這些具有一定格式規則的檔案一般是二進位制儲存的,於是我們可以用一個二進位制編輯器新建一個二進位制檔案,然後向裡面填寫資料。然後雙擊執行輸出“helloworld”字串。你可能會覺得很有成就感,我之前就寫過一個exe和dll。這裡exe和dll的檔案格式也就是著名的PE檔案格式。有興趣你可以去查閱相關資料,此非本文重點。


總結上面的認識,檔案無非就是一段資料的集合,這些資料可以是有規則的集合,也可以是無序的集合。作業系統也就是以檔案為單位對資料進行管理的。也就是說,要訪問外部介質上的資料,必須先按照檔名進行查詢,然後從該檔案中讀取資料。要想寫資料到外部介質,必須得建立一個檔案,然後再寫入。因此,這樣來看,你眼前的檔案將是一堆一堆資料而已,也沒有什麼型別檔案之分了,型別只是為了區分而已,假如你把一個exe檔案的副檔名改為txt,把它用記事本開啟,同樣是可行的,只是會執行exe檔案裡面的東西而已。(這裡又不得不提到一點,如果你是一名程式設計師或者愛好者,那麼你不應該將你的副檔名給隱藏了,要讓它顯示出來,如果你隱藏了,無非是增加了它的神祕感,同時在檔案操作上不方便。通過上面的本質,我相信你能體會到我為什麼這麼說。)

說到這裡,你應該知道檔案是什麼了,那麼再來看二進位制檔案和ASCII文字檔案,為什麼要分為這兩種呢?

首先、文字檔案方式儲存多用於我們需要明顯知道檔案裡面的內容時,比如ini、h、c等檔案都是文字檔案,這種檔案儲存的是字元(ASCII碼),比如一個整數10000,型別是short,佔2位元組,儲存文字形式將佔用5個位元組,一共5個字元。你可以想想更多的例子,體會文字檔案方便之處(提示:這裡的文字檔案不是說是txt檔案,而是指所有以文字格式儲存的檔案。)

其次、二進位制檔案方式多用於直接將記憶體裡面的資料形式原封不動存放到檔案裡,比如上面的short 10000,在記憶體中佔2位元組,儲存內容為10000的二進位制數,存到檔案後還是佔2位元組,內容也是10000的二進位制。這種方式可以整塊資料一塊兒儲存,同時還可以將記憶體資料對映到檔案裡。

       由上面兩點,C語言操作檔案可以是位元組流或者二進位制流。它把資料看成是一連串字元(位元組),而不需要考慮邊界。C語言對檔案的存取是以位元組為單位的。輸入輸出的資料流的開始和結束僅受程式控制而不受物理符號(如回車換行符)控制。這種檔案通常稱為流式檔案,大大增加了靈活性。我們可以產生很多自己的檔案格式,在遊戲程式裡面,用得比較多的就是資源包的格式,一般就是自定義的存取規則。我之前也寫了一個包檔案,存取只需要遵循規則,原理是非常簡單的。大家可以試試在腦子裡面構造一個包檔案。 

        在ANSI C標準中,使用的是“緩衝檔案系統”。所謂緩衝檔案系統指系統自動地在記憶體區為每一個正在使用的檔名開闢一個緩衝區,從記憶體向磁碟輸出資料必須先送到記憶體中的緩衝區,裝滿後再一起送到磁碟去。反向也是如此。這裡需要說明兩個詞:“輸入”“輸出”。輸入表示從檔案裡讀資料到程式裡,輸出表示從程式裡寫資料到檔案中。

       瞭解了檔案及檔案儲存形式,下面該正式進入檔案的讀寫了,不要太激動,還是慢慢來。細節往往決定成敗。在緩衝檔案系統中,有一個很重要的一個東西就是檔案指標,每個使用的檔案都會在記憶體中開闢一個區,用於存放檔案的有關資訊,這些檔案資訊就儲存在一個結構體變數中的,這個結構體是由系統定義的,名為FILE,先來看看VC2005在stdio.h下FILE結構體的定義:

struct _iobuf

{
        char *_ptr;               // 指向buffer中第一個未讀的位元組        

        int   _cnt;                 // 記錄剩餘未讀位元組的個數
        char *_base;           // 指向一個字元陣列,即這個檔案的緩衝
        int   _flag;                // FILE結構所代表的開啟檔案的一些屬性
        int   _file;                 // 用於獲取檔案描述,可以使用fileno函式獲得此檔案的控制代碼。
        int   _charbuf;          // 單位元組的緩衝,即緩衝大小僅為1個位元組,如果為單位元組緩衝,_base將無效
        int   _bufsiz;            // 記錄這個緩衝的大小
        char *_tmpfname;    // temporary file (i.e., one created by tmpfile()
                                        // call). delete, if necessary (don't have to on
                                        // Windows NT because it was done by the system when
                                        // the handle was closed). also, free up the heap
                                        // block holding the pathname.
};
typedef struct _iobuf FILE;
  

好了,上面的結構體就是這樣定義的。這裡不得不再次提到緩衝:

緩衝模式
 常量(mode)
 備註
 
無緩衝模式
 _IONBF
 該檔案不使用任何緩衝,也可以說是位元組緩衝

只能儲存一個位元組。 
行緩衝模式
 _IOLBF
 僅對文字模式開啟的檔案有效,所謂行,即是指每收到一個換行符(\n或\r\n),就將緩衝flush掉
全緩衝模式
 _IOFBF
 僅當緩衝滿時才進行flush
 
上面結構體中的_flag就標記了緩衝的資訊(我們關心這三個):
  

#define _IOYOURBUF  0x0100      // 使用使用者通過setbuf提供的buffer

#define _IOMYBUF      0x0008      // 這個檔案使用內部的緩衝

#define _IONBF          0x0004      // 無緩衝模式

#define _IOLBF           0x0040      // 行緩衝模式

#define _IOFBF           0x0000      // 全緩衝模式
  

同時,_flag也標記了讀寫模式,比如"r+"、"w+"等。

#define _IOREAD         0x0001    // 只讀
#define _IOWRT          0x0002    // 只寫
  

#define _IORW            0x0080    // 可讀可寫

上面的3中模式就是"r"、"w"、"+"任意組合起來表示的意思。

正因為使用緩衝模式,是為了避免頻繁的系統呼叫開銷,有了緩衝就不需要每次都訪問實際的檔案。當然緩衝也會帶來隱患,比如寫檔案時,先是到緩衝,如果此時系統崩潰或者程序意外退出時,有可能導致檔案資料的丟失。因此C語言提供了幾個基本的函式,彌補緩衝帶來的問題:  

int fflush( FILE* stream )  // flush指定檔案的緩衝,若引數為NULL,則flush所有檔案的緩衝。

int setvbuf( FILE *stream, char* buf,  int mode, size_t size )  // 設定緩衝型別,如上面的表格。

void setbuf( FILE* stream,  char* buf )  // 設定檔案的緩衝,等價於( void )setvbuf( stream, buf, _IOFBF, BUFSIZ ).  

所謂flush一個緩衝,是指對寫緩衝而言,將緩衝內的資料全部寫入實際的檔案,並將緩衝清空,這樣可以保證檔案處於最新的狀態。之所以需要flush,是因為寫緩衝使得檔案處於一種不同步的狀態,邏輯上一些資料已經寫入了檔案,但實際上這些資料仍然在緩衝中,如果此時程式意外地退出(發生異常或斷電等),那麼緩衝裡的資料將沒有機會寫入檔案。flush可以在一定程度上避免這樣的情況發生。


在這個表中我們還能看到C語言支援兩種緩衝,即行緩衝(Line Buffer)和全緩衝(Full Buffer)。全緩衝是經典的緩衝形式,除了使用者手動呼叫fflush外,僅當緩衝滿的時候,緩衝才會被自動flush掉。而行緩衝則比較特殊,這種緩衝僅用於文字檔案,在輸入輸出遇到一個換行符時,緩衝就會被自動flush,因此叫行緩衝。

終於把概念性的東西和準備步驟做完了,下面該看看具體的讀寫檔案了。有了前面的準備工作,讀寫檔案將不是難事了,因為有現成的庫函式供我們使用,我們下面的段落將是如何使用這些庫函式和一些注意事項而已了。

首先看如何開啟檔案,先看程式碼:
#include 
int main( void )
{

    FILE* pReadFile = fopen( "E:\\mytest.txt", "r" );   // 開啟檔案  

    if ( pReadFile == NULL )

        return 0;  

    fclose( pReadFile );     // 關閉檔案

    return 0;


  

上面的這段程式碼,只是一個簡單的開啟檔案,如果成功開啟後直接關閉。這裡開啟的是一文字檔案,是以只讀的方式開啟。使用fopen函式開啟,第一個引數是檔案路徑,第二個引數是讀寫模式,返回值為0表示開啟失敗。先看看讀寫模式:

 檔案使用方式
     含義 
 
"r"(只讀) 
 為輸入開啟一個文字檔案,不存在則失敗
 
"w"(只寫)
 為輸出開啟一個文字檔案,不存在則新建,存在則刪除後再新建
 
 "a"(追加)
 向文字檔案尾部增加資料,不存在則建立,存在則追加
 
'rb"(只讀) 
 為輸入開啟一個二進位制檔案,不存在則失敗
 
"wb"(只寫) 
 為輸入開啟一個二進位制檔案,不存在則新建,存在則刪除後新建
 
"ab"(追加) 
 向二進位制檔案尾部增加資料,不存在則建立,存在則追加
 
"r+"(讀寫) 
 為讀寫開啟一個文字檔案,不存在則失敗
 
"w+" (讀寫)
 為讀寫建立一個新的文字檔案,不存在則新建,存在則刪除後新建
 
 "a+"(讀寫)
 為讀寫開啟一個文字檔案,不存在則建立,存在則追加
 
"rb+"(讀寫)
 為讀寫開啟一個二進位制檔案,不存在則失敗
 
"wb+"(讀寫)
 為讀寫建立一個新的二進位制檔案,不存在則新建,存在則刪除後新建
 
 "ab+"(讀寫)
 為讀寫開啟一個二進位制檔案,不存在則建立,存在則追加
 
 

一、讀寫字元

C語言為從檔案中讀寫一個字元提供了兩個函式:

int __cdecl fgetc( FILE* stream );              // 從檔案讀入一個字元

int __cdecl fputc( int ch, FILE* stream );   // 寫入一個字元到檔案

看例子:

#include

int main( void )
{

    char cInput;
    FILE* pReadFile = fopen( "E:\\mytest.txt", "r" );   // 開啟檔案
    if ( pReadFile == NULL )
        return 0;

    while ( ( cInput = fgetc( pReadFile ) ) != EOF )   // 從檔案讀入一個字元,如果到檔案尾部,則返回EOF(-1)
        printf( "%c", cInput );

    fclose( pReadFile );     // 關閉檔案
    return 0;
}

假如mytest.txt檔案的內容是:

masefee

hello

world

三行,那麼我們逐個讀入每個字元,直到EOF結束,EOF很簡單,其實就是#define EOF (-1),WINDOWS為了能夠返回失敗為-1,因此fgetc的返回值使用是int型別。同時-1也不是某個字元的ASCII,所以不影響,一舉兩得。上面程式while迴圈不斷從檔案中讀取單個字元,遇到換行符(WINDOWS下回車符('\r')為13, 換行符('\n')為10),printf輸出後變處理成換行符了,因此檔案裡面3行,逐個讀入程式裡在終端顯示後還是3行。程式碼很簡單,就不用多說了。這裡需要提到一點:

問題一:當第一次執行了fgetc後,我們看看pReadFile指標裡面的內容與剛執行了fopen函式後的內容有所變化,為什麼?

再來看fputc函式:

#include

int main( void )
{
    int i = 0;
    char szOutput[ 32 ] = "masefee\nhello";


    FILE* pWriteFile = fopen( "E:\\mytest.txt", "w" );   // 開啟檔案
    if ( pWriteFile == NULL )
        return 0;

    while ( szOutput[ i ] != 0 )
    {
        fputc( szOutput[ i ], pWriteFile );    // 寫入一個字元到檔案
        i++;
    }

    fclose( pWriteFile );     // 關閉檔案
    return 0;
}

我特意在szOutput數組裡寫了一個'\n'字元,此字元就是換行符newline,意圖是當輸出到e之後,便輸出一個換行符,讓字串換行。因此最終mytest.txt檔案裡面的內容如下:

masefee

hello

到這裡,你可能會想到第一個fgetc的例子是我們預先在檔案中輸入3行字元,然後讀入到程式中。我們在用記事本輸入3行文字的時候,每當換行的時候我們敲鍵盤是按的回車。

問題二:既然我們敲的是回車,為什麼在檔案裡儲存的是'\n'而不是'\r'?

同時,到這裡想到第一個問題,我們又來觀察一下,當剛使用fopen函式時,pWriteFile裡面的內容是:

pWriteFile          0x00437bb0

_ptr                   0x00000000

_cnt                   0

_base                0x00000000

_flag                  2

_file                   3

_charbuf            0

_bufsiz              0

_tmpfname       0x00000000

而執行了fputs函式,到換行符後我們再看pWriteFile裡面的內容:

pWriteFile          0x00437bb0

_ptr                   0x00385019

_cnt                   4087

_base                0x00385010

_flag                  10

_file                   3

_charbuf            0

_bufsiz              4096

_tmpfname       0x00000000

然後我們再看看_base所在記憶體的值:

6d 61 73 65 66 65 65 0a 68

 m  a   s   e    f   e    e  \n  h

從這個現象我們能夠意識到,FILE結構裡面_base所指向的緩衝區,_cnt表示還剩下多少個位元組沒有寫。還可以意識到,我們在不設定任何引數時,預設情況下是採用的全緩衝模式,填充4096位元組後自動會寫入到檔案,在這裡我們沒有那麼多位元組,因此在fclose函式執行後,檔案裡便寫入了值。你可以打斷點在fclose上,等程式斷下來後,觀察你磁盤裡面的mytest.txt是空的,當執行了fclose後大小就變了。這也能體現緩衝區的一個現象。

同樣,如果你想立即將緩衝區的資料寫到檔案裡,可以在fclose函式前面加上:

fflush( pWriteFile );

當執行完此函式後,資料便寫進了檔案,最後再關閉檔案。

二、讀寫字串

C語言為從檔案中讀寫字串提供了2個函式:

char* __cdecl fgets( char* _Buf, int _MaxCount, FILE* _File );

引數一:要從檔案中讀入字串的存放空間。

引數二:最大讀取位元組數。

引數三:檔案指標。

返回值:返回讀入的字串指標。

int      __cdecl fputs( const char* _Str,  FILE* _File );

引數一:要寫入檔案的字串

引數二:檔案指標

返回值:失敗或成功,0表示成功,其它表示失敗。

先來看字串讀取:
#include

int main( void )
{
    char   szInput[ 32 ] = { 0 };
    char* pRet = NULL;

    FILE* pReadFile = fopen( "E:\\mytest.txt", "r" );   // 開啟檔案
    if ( pReadFile == NULL )
        return 0;

    pRet = fgets( szInput, 32, pReadFile );    // 從檔案中讀取一個字串到szInput陣列中

    fclose( pReadFile );     // 關閉檔案
    return 0;
}

其它函式不說了,這裡只說fgets函式,第二個引數傳的是32,實際只能從檔案中讀取31個字元,因為fgets函式內部會將最後一個字元置為'\0', 表示字串結束。那麼我們可以看看fgets函式的內部原理,我這裡寫寫虛擬碼,為了更清晰的表現出來:

char*  fgets( char* dst, int maxcount, FILE* file )

{

    char ch;

    while( --maxcount )

    {

         ch = readFromFile();

         if ( ( *dst++ =  ch ) == '\n' )

             break;

    }

    *dst = 0;   // 賦值為'\0'

     return dst;

}

紅色部分是計數,藍色部分是關鍵,如果最大讀取位元組數量足以讀到換行,將停止讀取字元,然後階數本字串,然後返回。

明白了fgets函式,fputs函式就簡單了:

#include

int main( void )
{
    char szOutput[ 32 ] = "masefee\nhello";

    FILE* pWriteFile = fopen( "E:\\mytest.txt", "w" );   // 開啟檔案
    if ( pWriteFile == NULL )
        return 0;

    fputs( szOutput, pWriteFile );    // 寫入一個字串到檔案

    fclose( pWriteFile );     // 關閉檔案
    return 0;
}

這裡我也專門為字元數組裡增加了一個換行符,寫入字串的時候並不會因為換行符而只寫換行符前面的字元,同時在fputs內部會求第一個引數的長度strlen( Str ); 然後再寫入這麼一個長度的字串到檔案。

到這裡又得提醒一點,即便是檔案裡面含有'\0'(ASCII碼為0的字元)。fgets函式同樣會一直讀取到換行符或者讀取規定的字元個數(此字元個數小於一行字元數)。雖然是讀了一行,中間因為有0,因此字串被截斷,讀出來的字串並沒有一行,只有0前面的所有字元。這裡大家需要注意。同時fputs函式會以0結束寫入檔案,這是跟通常情況一樣的,可以不用關心。

三、格式化資料讀寫

C語言既然有printf、scanf,那麼同樣也有檔案操作的格式化函式:

int __cdecl fprintf( FILE* _File, const char* _Format, ... );

int __cdecl fscanf( FILE* _File, const char* _Format, ... );

這兩個函式跟printf和scanf的用法非常相似,只是這裡輸入輸出是關於檔案的。

直接貼程式碼:

#include

typedef struct SStudent
{
    int   number;
    char  name[ 11 ];
}Student;

int main( void )
{
    Student stu;

    FILE* pReadFile = fopen( "E:\\mytest.txt", "r" );   // 開啟檔案
    if ( pReadFile == NULL )
        return 0;
    
    fscanf( pReadFile, "%d%s", &stu.number, &stu.name );


    fclose( pReadFile );     // 關閉檔案
    return 0;
}

我定義了一個結構體,裡面一個學號,一個姓名。然後開啟檔案,讀取資料到stu結構體變數中。假如檔案中是:

345   masefee

346   Tim

然後讀到stu結構體變數中,number為345,name為"masefee"。

fscanf讀取資料是以空格、製表符、換行符進行分割的,我們可以這樣來填充結構體。

再來看fprintf:

#include

typedef struct SStudent
{
    int   number;
    char  name[ 11 ];
}Student;

int main( void )
{
    Student stu;

    FILE* pWriteFile = fopen( "E:\\mytest.txt", "w" );   // 開啟檔案
    if ( pWriteFile == NULL )
        return 0;

    stu.number = 100;
    strcpy( stu.name, "masefee" );

    fprintf( pWriteFile, "%d    %s", stu.number, stu.name );

    fclose( pWriteFile );     // 關閉檔案
    return 0;
}

此程式將把結構體stu的內容寫到檔案裡,注意這裡的name不會把結束符'\0'寫到檔案裡。

好了,說到這裡,上面幾個基本的檔案操作函式已經寫完了,我只是使用了"r"和"w"兩種方式,其它方式你可以自行測試,也沒有什麼特別的。如果你是用上面的函式去讀取二進位制序列,也是沒有錯的,只不過你更不好控制而已。至於和"+"組合也沒有什麼特別的,無非就是在檔案尾部追加,原理一樣,大家可以自行測試。

四、檔案資料塊讀寫

同樣C語言也提供了兩個函式:

size_t __cdecl fwrite

(
const void *buffer,  // 要寫入檔案的資料塊
size_t size,             // 寫入檔案的位元組數
size_t count,           // 寫入count個size大小的資料
FILE *stream           // 檔案指標
);

size_t __cdecl fread

(

void * _DstBuf,            // 存放從檔案讀出來的資料

size_t _ElementSize,   // 讀取位元組數

size_t _Count,             // 讀入次數

FILE * _File                  // 檔案指標

);

先看看fwrite函式:

#include

typedef struct SStudent
{
    int   number;
    char  name[ 12 ];
}Student;

int main( void )
{
    Student stu;

    FILE* pWriteFile = fopen( "E:\\mytest.txt", "w" );   // 開啟檔案
    if ( pWriteFile == NULL )
        return 0;

    stu.number = 10000;
    strcpy( stu.name, "masefee" );

    fwrite( &stu, sizeof( stu ), 1, pWriteFile );

    fclose( pWriteFile );     // 關閉檔案
    return 0;
}

這樣寫入檔案後,mytest.txt的內容為:

'  masefee 燙燙

你可能會疑惑,為什麼會有亂碼?而且還有可惡的“燙”字。原因很簡單,fwrite函式是以資料塊的形式寫資料到檔案的,比如這裡的stu結構體變數,我們將它整塊寫入檔案,一共16位元組,因此上面的亂碼對應的就是stu結構體變數在記憶體中的存放形式,number佔4位元組,name佔12位元組,具體的數值是:

10 27 00 00 6d 61 73 65 66 65 65 00 cc cc

    10000                 "masefee"             燙燙

因為我們在為name拷貝字串時,並沒有將name的所有字元清零,因此係統預設初識化為0xcc,為什麼初始化為0xcc,之前我應該提過,主要是這個0xcc是彙編中斷指令的機器碼,主要防止訪問越解釋,進行中斷報錯。而0xcc就是中文編碼的“燙”字。

最後面的兩個“燙”還不能省略,因為我們是以塊寫入檔案的,如果去掉兩個cc,那麼將沒有16位元組,如果有多個結構體變數的資料一塊兒寫到檔案中時,結構體的資料對齊是非常重要的,否則將讀寫越界,跟記憶體一樣。這裡就好比記憶體的一個對映。

至於為什麼會出現亂碼,是因為超過可現實ASCII碼值,看上去就是亂的,其實資料還是正常的。

理解了fwrite函式後,fread函式就簡單了,由於篇幅原因我這裡只寫關鍵:

Student stu_out;

fread( &stu_out, sizeof( Student ), 1, pReadFile );

這樣就能填充好stu_out結構體變數,我想你已經體會到了資料塊讀寫時,資料對齊的重要性了。在遊戲的資源包,就是採用的資料塊的儲存形式,同時bmp、jpg、exe、dll等檔案都是由很多個數據塊,通常是結構體的形式直接寫入檔案的,這樣檔案頭記錄了很多偏移,很多大小等就顯得非常重要了。

最後,我直接寫了一個例項,就是簡單的打包,解包程式。可以將多個檔案放置到一個包檔案裡,這個包是二進位制包。基本的功能已經實現,只需要新增比如壓縮,介面等優化工作了。我初步測試了一下是可以成功打包解包的,也沒有太多的條件檢查和效率考慮,本文重在解釋檔案操作的靈活性和重要性。好了,直接上程式碼吧:

#include 
#include 
#include

typedef unsigned int  uint;
typedef unsigned char byte;          

// 包檔案中最大可容納的檔案個數
#define MAX_FILE_COUNT 10

// 全域性包檔案指標
FILE*  g_pMasFile = NULL;

// 資源包檔案頭結構
typedef struct SMaseFileHeader
{
    uint  uFileFlag;       // 包檔案頭標記: 'MASE'
    uint  uFileCount;      // 包內檔案個數
    uint  uFileListOfs;    // 檔案列表偏移
    uint  uMaxFileCount;   // 最大子檔案個數
    uint  uFileSize;       // 包檔案的大小
}MaseHeader;

// 包內檔案資訊結構
typedef struct SFilesMessage
{
    uint  uFileOfs;          // 本檔案在包內的偏移
    uint  uFileSize;         // 本檔案的大小
    char  szFileName[ 260 ]; // 本檔案的路徑
}FilesMsg;


// 開啟包檔案
int OpenMasFile( const char* path, const byte onlyOpen )
{
    uint       uWriteCount;       // 寫入檔案資訊次數;
    byte       bIsNew = 0;        // 是否新建的
    MaseHeader header;            // 檔案頭結構定義
    FilesMsg   msg;

    g_pMasFile = fopen( path, "rb" );  // 用來判斷是否存在
    if ( g_pMasFile == NULL )          // 這裡就沒有用windows API了
    {
        if ( onlyOpen == 1 )           // 只打開不新建
            return -1;

        bIsNew = 1;
        g_pMasFile = fopen( path, "wb" );
        if ( g_pMasFile == NULL )
            return -1;
    }

    // 先關閉,然後在用"rb+"方式開啟
    fclose( g_pMasFile );

    g_pMasFile = fopen( path, "rb+" );
    if ( g_pMasFile == NULL )
        return -1;

    if ( bIsNew == 1 ) // 新建的檔案
    {
        header.uFileFlag     = 'ESAM';
        header.uFileCount    = 0;
        header.uFileListOfs  = sizeof( MaseHeader ); // 緊跟著就是檔案列表
        header.uMaxFileCount = MAX_FILE_COUNT;
        header.uFileSize     = sizeof( MaseHeader ) 
                               + ( MAX_FILE_COUNT * sizeof( FilesMsg ) );

        // 寫入頭資訊
        fwrite( &header, sizeof( MaseHeader ), 1, g_pMasFile );

        memset( &msg, 0, sizeof( FilesMsg ) );
        uWriteCount = MAX_FILE_COUNT;

        // 寫入檔案列表用0佔位
        while( --uWriteCount )
            fwrite( &msg, sizeof( FilesMsg ), 1, g_pMasFile );
    }
    else  // 檔案存在
    {
        // 則讀取標頭檔案資訊
        fread( &header, sizeof( MaseHeader ), 1, g_pMasFile );
    }

    // 檢查檔案頭標記
    if ( header.uFileFlag != 'ESAM' )
    {
        fclose( g_pMasFile );
        return -1;
    }

    // 檢查資料是否完整
    if ( header.uMaxFileCount != MAX_FILE_COUNT )
    {
        fclose( g_pMasFile );
        return -1;
    }

    return 0;
}

// 寫檔案到包裡
int WriteFileToPak( const char* path )
{
    FilesMsg   fileMsg;      // 此檔案的檔案資訊結構
    MaseHeader header;       // 包檔案頭結構定義
    uint       uFileSize;
    uint       uFileListEndOfs;
    byte*      pBuff;
    FILE*      pFile = NULL;

    if ( g_pMasFile == NULL )
        return -1;

    memset( &fileMsg, 0, sizeof( FilesMsg ) );
    fseek( g_pMasFile, 0, SEEK_SET );

    // 則讀取標頭檔案資訊
    fread( &header, sizeof( MaseHeader ), 1, g_pMasFile );

    uFileListEndOfs = header.uFileCount * sizeof( FilesMsg ) + header.uFileListOfs;

    pFile = fopen( path, "rb" );
    if ( pFile == NULL )
        return -1;

    fseek( pFile, 0, SEEK_END );
    uFileSize = ftell( pFile );
    fseek( pFile, 0, SEEK_SET );

    // 檔名長度不能超過260
    strcpy( fileMsg.szFileName, path );
    fileMsg.uFileOfs  = header.uFileSize;
    fileMsg.uFileSize = uFileSize;

    // 寫入檔案資訊
    // 將檔案指標定位到uFileListEndOfs處,以便寫入新的檔案資訊結構
    fseek( g_pMasFile, uFileListEndOfs, SEEK_SET );
    fwrite( &fileMsg, sizeof( FilesMsg ), 1, g_pMasFile );

    // 申請空間
    pBuff = ( byte* )malloc( uFileSize );
    fread( pBuff, uFileSize, 1, pFile );

    // 寫資料到包檔案裡
    fseek( g_pMasFile, header.uFileSize, SEEK_SET );
    fwrite( pBuff, uFileSize, 1, g_pMasFile );

    // 釋放記憶體
    free( pBuff );

    // 重新填充header
    header.uFileCount += 1;
    header.uFileSize  += uFileSize;

    fseek( g_pMasFile, 0, SEEK_SET );

    // 重新寫入包檔案頭
    fwrite( &header, sizeof( MaseHeader ), 1, g_pMasFile );

    return 0;
}

// 從包檔案裡讀資料
int ReadFileFromPak( const FilesMsg msg, byte* _dst )
{
    if ( g_pMasFile == NULL )
        return -1;

    fseek( g_pMasFile, msg.uFileOfs, SEEK_SET );
    fread( _dst, msg.uFileSize, 1, g_pMasFile );

    return 0;
}

// 獲取包中某個檔案的資訊
int GetFileMessage( const char* path, FilesMsg* msg )
{
    FilesMsg   fileMsg;      // 此檔案的檔案資訊結構
    MaseHeader header;       // 包頭結構
    uint       uFileCount;   // 檔案個數

    if ( g_pMasFile == NULL || msg == NULL )
        return -1;

    // 則讀取標頭檔案資訊
    fseek( g_pMasFile, 0, SEEK_SET );
    fread( &header, sizeof( MaseHeader ), 1, g_pMasFile );

    uFileCount = header.uFileCount;
    while ( uFileCount-- )
    {
        fread( &fileMsg, sizeof( FilesMsg ), 1, g_pMasFile );

        // 判斷是否是要獲取的檔案
        if ( stricmp( fileMsg.szFileName, path ) == 0 )
        {
            *msg = fileMsg;
            return 0;
        }
    }

    return -1;
}

// 關閉包檔案
int CloseMasFile( void )
{
    if ( g_pMasFile == NULL )
        return -1;

    fclose( g_pMasFile );
    g_pMasFile = NULL;

    return 0;
}
 

上面已經將整個打包解包介面給實現了,我自定義副檔名為.mase, 這個隨意哈,檔案頭結構上面已經很清晰了。由於篇幅的原因,這裡就不一一解說了,我貼了很多註釋。應該能夠看懂的。

有了上面的介面,我們就可以來操作這個包檔案了,先是看怎麼寫入:

int main( void )
{
    int ret;

    ret = OpenMasFile( "E:\\PhotoPak.mase", 0 );
    if ( ret == -1 )
        goto __exit;

    WriteFileToPak( "E:\\大山.jpg" );
    WriteFileToPak( "E:\\海水.bmp" );
    WriteFileToPak( "E:\\檢視.exe" );
    WriteFileToPak( "E:\\載入.dll" );
    WriteFileToPak( "E:\\說明.txt" );

__exit:
    CloseMasFile();
    return 0;

在這段程式碼裡,演示了怎麼將檔案給寫進包檔案,首先是建立了一個PhotoPak.mase包,然後是向裡面寫入了:大山.jpg、海水.bmp、檢視.exe、載入.dll、說明.txt這麼幾個檔案,注意我的接口裡面都是用二進位制開啟的,因為如果是非二進位制開啟的話,寫入的時候會插入一些物理字元(比如回車符(ASCII:0x0D( 1310 ))等)。那樣插入進去後,然後在解包時再採用非二進位制方式寫入檔案就不是原來的檔案了,這點大家要注意。

好了,寫了這麼幾個檔案後,再看看怎麼把他們從包裡面弄出來,然後能夠正常的開啟和檢視:

int main( void )
{
    byte*       pBuff;
    FILE*       pOutFile;
    FilesMsg    getFileMsg;
    int         ret;

    ret = OpenMasFile( "E:\\PhotoPak.mase", 1 );
    if ( ret == -1 )
        goto __exit;
    
    ret = GetFileMessage( "E:\\檢視.exe", &getFileMsg );
    if ( ret == -1 )
        goto __exit;

    pBuff = ( byte* )malloc( getFileMsg.uFileSize );
    ret = ReadFileFromPak( getFileMsg, pBuff );
    if ( ret == -1 )
        goto __exit_free;

    pOutFile = fopen( "E:\\檢視_out.exe", "wb" );  // 注意使用的是二進位制模式
    if ( ret == -1 )
        goto __exit_free;

    fwrite( pBuff, getFileMsg.uFileSize, 1, pOutFile );
    fclose( pOutFile );
    
__exit_free:
    free( pBuff );

__exit:
    CloseMasFile();
    return 0;

很清楚了吧,直接先傳入路徑,然後獲得檔案的資訊,方便我們分配空間。然後我是將從包裡獲取出來的檔案又寫到磁盤裡,命名為檢視_out.exe, 同樣既然是獲取了pBuff,你同樣可以在記憶體中使用這個檔案,一舉兩得。然後獲取出來,執行這個獲取的檢視_out.exe看是不是能執行。我在WINDOWS XP SP3 下是能執行的,你可以用你自己的一個exe來測試,隨便用什麼檔案。

這裡還要說到幾個注意事項:

1. 這裡我只是測試了較小的檔案解包和寫包,如果檔案比較大的話,可以用分塊進行讀寫。

2. 我沒有寫任何的加密演算法和壓縮演算法,這裡只是展示了基本原理。也沒有太多的效率和安全考慮。

3. 我這裡使用的都是E盤根目錄下的檔案,你也完全可以不是跟目錄,在包檔案裡面是沒有資料夾的概念的,如果沒有在根目錄,你可以在解包的時候,根據路徑先建立好資料夾在磁碟上,然後再將包裡讀出來的檔案寫到相應的路徑下,這就實現了不同資料夾管理的功能。

相關推薦

C語言檔案操作的基本函式總結

#include <stdio.h> int main() { FILE* fp = fopen("data.txt","r"); if(fp == NULL) { printf("open error\n"); return -1;

C語言檔案操作函式彙總

#include <stdio.h> #include <stdlib.h> int main() { FILE* fd = fopen("test.txt","r"); if(NULL == fd)//檔案開啟失敗 { perror("fope

C語言檔案

所謂檔案(file)一般指儲存在外部介質上資料的集合,比如我們經常使用的mp3、mp4、txt、bmp、jpg、exe、rmvb等等。這些檔案各有各的用途,我們通常將它們存放在磁碟或者可移動盤等介質中。那麼,為什麼這裡面又有這麼多種格式的檔案呢?原因很簡單,它們各有各的用

fstream 判斷是否成功開啟檔案 | C++檔案(fstream)的使用方法及示例

ifstream fin("filename"); if (!fin) { cout << "fail to open the file" <<endl; return -1;//或者丟擲異常。 } else { cout << "open

c語言實現超連結(多檔案程式的編譯和連線)

一個實用價值的c語言應用程式往往較大,需要劃分成不同的檔案,那麼如何把這些檔案編譯,連線成一個統一的可執行的檔案並執行呢?   c語言提供了編譯預處理“#include“檔名””來實現‘檔案包含”的操作,其特點是一個原始檔可以將另外一個原始檔的全部包含進來。預處理程式將#i

(轉載)C語言常用的幾個標頭檔案及庫函式 (stdio.h ,string.h ,math.h ,stdlib.h)

不完全統計,C語言標準庫中的標頭檔案有15個之多,所以我主要介紹常用的這四個標頭檔案stdio.h ,string.h ,math.h ,stdlib.h ,以後用到其他的再做補充。下面上乾貨: 1.<stdio.h>:定義了輸入輸出函式、型別以及巨集,函式

C語言自帶的標頭檔案(.h)所包含的函式

由於之前沒有好好學習過C語言,所以對其自帶標頭檔案所包含的內容總是不清楚,每次寫程式碼都是盲目的#include很多.h,現在重新整理一下,發現了不少很好的函式,以方便複習查閱。 不完全統計,C語言標

C語言的輸入輸出和緩衝區(重點)詳解

導讀: C語言中我們用到的最頻繁的輸入輸出方式就是scanf()與printf()。 scanf(): 從標準輸入裝置(鍵盤)讀取資料,並將值存放在變數中。 printf(): 將指定的文字/字串輸出到標準輸出裝置(螢幕)。注意寬度輸出和精度 輸出控制。 C語言藉助了相應的緩衝區

C++檔案檔案的區分

1、檔案的概念 檔案一般是指儲存在外部介質上資料的集合。 外存檔案包括:磁碟檔案、光碟檔案和U盤檔案等,使用最廣泛呢的還是磁碟檔案。 對使用者來說,常用到的檔案有兩類:一類是程式檔案,如.cpp檔案、.obj檔案和.exe檔案。 一類是資料檔案(data

C++檔案(fstream)的使用方法及示例

C++檔案流: fstream  // 檔案流 ifstream  // 輸入檔案流 ofstream  // 輸出檔案流 #include <fstream> //建立一個文字檔案並寫入資訊 //同向螢幕上輸出資訊一樣將資訊輸出至檔案 #includ

c語言檔案的建立和讀寫

在c語言中,fopen用於建立檔案,fwrite用於將資料寫入檔案,而fread用於讀取檔案中的資料,fclose用於關閉檔案(在有些編輯器中如VS2017要使用fopen_s、fwrite_s和fread_s、fclose_s或者在程式碼開始前使用#pragram warn

C語言常用的檔案操作函式

C函式庫中檔案操作函式: (1)fopen:開啟檔案 函式原型:FILE* fopen(char *path, char *mode); 函式引數:path----開啟檔名及其路徑      mode----r w a …… 函式返回:成功則返回指向該流的檔案指標,失敗則返回NULL並把錯誤存在errno中

C語言檔案的全域性變數

宣告:突然看到這篇文章,發現了c語言中使用全域性變數的錯誤,特轉之。 func.c 123456 int buf = 0; void func() { buf = 2; /* Do something else */ } ma

C/C++語言變數作用域:區域性變數,全域性變數,檔案級變數

C/C++語言中的變數分為全域性變數和區域性變數。這種劃分方式的依據是變數的可見範圍或者叫做作用域。 1 區域性變數 區域性變數指的是定義在{}中的變數,其作用域也在這個範圍內。雖然常見的區域性變數都是定義在函式體內的,也完全可以人為的增加一對大括號來限定變

C語言檔案寫入內容

#include <stdio.h> int main () { FILE * pFile; //char buffer[] = { 'x' , 'y' , '2' };

Linux之C語言如何丟擲異常或將異常寫入日誌檔案

Linux中用C語言寫系統日誌__________________________________________________________________________________________________Author:冀博Time :2011/11/24

淺談C語言文字檔案與二進位制檔案

C語言中,按檔案中的資料組織形式來分,資料檔案可分為ASCII碼檔案(即文字檔案)和二進位制檔案。 文字檔案在磁碟中存放時每個字元對應一個位元組,用於存放對應的ASCII碼。 二進位制檔案把資料按其在記憶體中的儲存形式存放在磁碟上,一個位元組並不一定對應一個字元。 對於A

C語言寫多個檔案時迴圈生成檔名

char fileName[50]; char *dirName="E:/lung_data/005/GB_BMP4_5_1"; for(int i=0;i<num;i++) {sprintf_s(fileName,"%s//%d.bmp",dirName,i); }

C語言新建檔案,向檔案輸入內容並讀出檔案內容

三、格式化讀寫函式--fscanf( )函式和fprint( )函式 呼叫方式分別是: fscanf(檔案指標,格式字串,輸入表);fscanf 函式將指標指向的檔案內容,以格式符要求的形式,讀入記憶體指定地址內 fprintf(檔案指標,格式字串,輸出表);fprintf 函式是將記憶體指定地址內的內容

C語言多個檔案組織(include)的原理

大學剛學C的時候,老師教的第一個C程式是打印出一個helloword字樣,並告訴我們,要使用printf這個東西,C檔案開頭需要加上一句:#include"stdio.h" 這個語句是實現了什麼動作? 後來學到C++,好像老師還是同學(記不清楚了)說了一句,#in