1. 程式人生 > >詳細剖析二進位制檔案的讀寫

詳細剖析二進位制檔案的讀寫

一).一般問題
  二進位制檔案與我們通常使用的文字檔案儲存方式有根本的不同。這樣的不同很難用言語表達,自己親自看一看,理解起來會容易得多。因此,我推薦學習二進位制檔案讀寫的朋友安裝一款十六進位制編輯器。這樣的編輯器有很多,在我們的 CVF 附帶的整合開發環境下就可以(將二進位制檔案拖動到 IDE 視窗後鬆開)。Visual Studio 2005 也是可以的。(不過需要在 File 選單下 Open,File)
  另外推薦一款使用較多的軟體,叫做 UltraEdit(以下簡稱 UE)。是很不錯的文字編輯器,也能做十六進位制編輯器使用。
  為什麼要用十六進位制編輯器?而不用 2 進位制呢?因為 2 進位制實在太小,書寫起來會很長,很不直觀。而我們的計算機把 8 位作為一個位元組。剛好 2 ** 8 = 256 = 16 ** 2。用 8 位 2 進製表達的數,我們用 2 個十六進位制資料來表達,更直觀和方便。

二).檔案格式


  所有檔案,籠統意義上將可以區分為兩類,一類是文字檔案,一類是二進位制檔案。

1).文字檔案
  文字檔案用記事本等文字編輯器開啟,我們可以看懂上面的資訊。所以使用比較廣泛。通常一個文字檔案分為很多很多行,作為資料儲存時,還有列的概念。實際上,儲存在硬碟或其他介質上,檔案內容是線一樣儲存的,列是用空格或 Tab 間隔,行是用回車和換行符間隔。
  以 ANSI 編碼(使用較多)的文字檔案來說,例如我們儲存如下資訊:

引用 10
11
12


  需要的空間是:3 行 × 每行 2 個字元 + 2 個回車符 + 2 個換行符 = 10 位元組。文字檔案儲存資料是有格式,無資料型別的。比如 10 這個資料,並不指定是整型還是實型還是字串。它有長度,就是 2,兩個位元組。儲存時計算機儲存它的 ASCII 碼:31h,30h。(十六進位制表示)。回車符是:0Dh,換行符:0Ah。
     因此,這個資料儲存是這樣的:

引用 31 30 0D 0A 31 31 0D 0A 31 32


  (紅色為回車符和換行符) 31h 30h 就是 10,31h 31h 就是 11,31h 32h 就是 12。因此我們也可以認為文字檔案是特殊的二進位制檔案。
2).二進位制檔案
  二進位制檔案,是無格式有資料型別的。比如上面的 10 11 12 三個數。但二進位制檔案沒有行的概念。我們要緊湊地儲存他們。(當然也可以中間加入一些空白的位元組)
  從資料型別上來說,我們首先考慮整型。如果把 10 11 12 當作 2 字長的整型。則 10 表示為:0Ah 00h。因為 0Ah 對應十進位制 10。而後面的 00h 是空白位。2 字長的整型如果不足 FFh,也就是不足 255,則需要一個空白位。類似的:11 表示為 0Bh 00h,12 表示為 0Ch 00h。
  當整型資料超過 255 時,我們需要 2 個位元組來儲存。比如 2748(ABCh),則表示為:BCh 0Ah。要把低位寫在前面(BCh),高位寫在後面(0Ah)。
  當整型資料超過 65535 時,我們就需要 4 個位元組來儲存。比如 439041101(1A2B3C4Dh),則表示成:4Dh 3Ch 2Bh 1Ah。當資料再大時,我們就需要 8 位元組儲存了。
  二進位制檔案的實型資料也有位元組長度的區分,比如 4 字長,8 字長。但實型資料的長度並不僅僅代表它的表達的範圍,更多的代表精度。所以,8 字長的我們又稱為雙精度。關於實型資料如何儲存為 2 進位制。則有很多套規則。現在都廣泛使用的是 IEEE 標準浮點格式。關於這樣的規則,我還正在瞭解,比較麻煩。就不多說了。在這裡也沒有必要了解。
  二進位制檔案也可以儲存字元型資料,儲存方法和文字檔案一樣。都是使用 ASCII 編碼儲存的。所以我們用記事本開啟某些二進位制檔案時,也能看到一些有意義的字串。(無意義的亂碼我們可以認為是整型或實型,不過記事本程式當作字元來解釋,因此造成了亂碼)

三).使用二進位制檔案的好處


  為什麼要使用二進位制檔案。原因大概有三個:
  第一是二進位制檔案比較節約空間,這兩者儲存字元型資料時並沒有差別。但是在儲存數字,特別是實型數字時,二進位制更節省空間,比如儲存 Real*4 的資料:3.1415927,文字檔案需要 9 個位元組,分別儲存:3 . 1 4 1 5 9 2 7 這 9 個 ASCII 值,而二進位制檔案只需要 4 個位元組(DB 0F 49 40)
  第二個原因是,記憶體中參加計算的資料都是用二進位制無格式儲存起來的,因此,使用二進位制儲存到檔案就更快捷。如果儲存為文字檔案,則需要一個轉換的過程。在資料量很大的時候,兩者就會有明顯的速度差別了。
  第三,就是一些比較精確的資料,使用二進位制儲存不會造成有效位的丟失。

四).二進位制檔案的儲存方式
     列舉一個二進位制檔案如下:

引用 00000000h: 0F 01 00 00 0F 03 00 00 12 53 21 45 58 62 35 34 ; .........S!EXb54
00000010h: 41 42 43 44 45 46 47 48 49 47 4B 4C 4D 4E 4F 50 ; ABCDEFGHIGKLMNOP


  這裡列出的是在 UltraEdit(UE) 裡看到的東西。其實只有紅色部分是檔案內容。前面的是 UE 加入的行號。後面的是 UE 嘗試解釋為字元型的參考。
  這個檔案一共有 32 位元組長。顯示為兩列,每列 16 個位元組。實際上,這僅僅是 UE 的顯示而已。真實的檔案並不分行。僅僅知道這個檔案的內容,如果我們沒有任何說明的話,是不能看出任何有用資訊的。
  下面我規定一下說明:我們認為,前 4 個位元組是一個 4 位元組的整型資料(0F 01 00 00 十六進位制:10Fh 十進位制:271)。這 4 個位元組之後的 4 個位元組是另一個 4 位元組的整型資料(0F 03 00 00 十六進位制:30Fh 十進位制:783)。其後的 4 個位元組(12 53 21 45 )表示一個 4 位元組的實型資料:2.5811919E+3。再其後的 4 個位元組(58 62 35 34)表示另一個 4 位元組的實行資料:1.6892716E-7。而只後的 16 個位元組(41 42 43 44 45 46 47 48 49 47 4B 4C 4D 4E 4F 50)我們認為是 16 個位元組的字串(ABCDEFGHIGKLMNOP)
  實際上,二進位制檔案只是儲存資料,並不寫明資料型別,比如上面的第 9 位元組到第 16 位元組(12 53 21 45 58 62 35 34),我們剛才認為是 2 個 4 位元組的實型,其實也可以認為是 8 個位元組的字元型( S!EXb54)。而後面的 16 個位元組的字串(ABCDEFGHIGKLMNOP),我們也可以認為是 2 個 8 位元組的整型,或者 4 個 4 位元組的整型,甚至 2 個 8 位元組的實型,4 個 4 位元組的實型,等等等等。
  因此,面對一個二進位制檔案,我們不能準確地知道它的含義,我們需要他的資料儲存方式的說明。這個說明告訴我們第幾個位元組到第幾個位元組是什麼型別的資料,儲存的資料是什麼含義。否則的話,我們只能猜測,或者無能為力。

五).如何使用語句操作二進位制檔案
  我們將上面的那個二進位制檔案儲存為:TestBin.Bin 來舉例。
  讀取和寫入二進位制其實是兩個很類似的操作,瞭解了其中之一,另一個也就不難了。
  二進位制檔案我們通常使用直接讀取方式,Open 語句可以寫為:

引用 Open( 12 , File = 'TestBin.Bin' , Access = 'Direct' , Form = 'Unformatted' , RecL = 4 )


  上面的 Access 表示直接讀取方式,Form 表示無格式儲存。比較重要的是 RecL 。我們讀取資料時,是用記錄來描述單位的,每一次讀入或寫入是一個記錄。記錄的長度在 Open 時就確定下來,以後不能改變。如果需要改變,只能 Close 以後再此 Open。
  記錄長度在某些編譯器下表示讀取的 4 位元組長度的倍數,規定為 4 表示記錄長度為 16 位元組。有些編譯器下就直接表示記錄的位元組數,規定為 4 則表示記錄長度為 4 位元組。這個問題需要參考編譯器手冊。在 VF 系列裡,這個值是前面一個含義。可以通過設定工程屬性的 Fortran,Data,Use Bytes as RECL= Unit for Unformatted Files 來改變,使之成為後一個含義。在命令列模式下,則使用 /assume:byterecl 這個編譯選項。
  確定 RecL 大小是我們需要做的事情,一般來說,不適合太大,也不適合太小。還需要結合資料儲存方式來考慮。太小的話,我們需要執行讀寫的次數就多,太大的話,我們就不方便操作小範圍的資料。
  有時候我們甚至會分多次來讀取資料,每一次的 RecL 都不同。對於上面的 TestBin.Bin 檔案來說,它比較簡單,我以 16 位元組長度和 8 位元組長度兩種讀取方式來演示,你甚至可以一次 32 個位元組長度全部讀完:
  (1)RecL = 4 【記錄長度 16 位元組】

引用 Program main
   Implicit None
   Integer*4 :: iVar1 , iVar2
   Real*4 :: rVar1 , rVar2
   Character(Len=16) :: cStr
   Open( 12 , File = 'TestBin.Bin' , Access = 'Direct' , Form = 'Unformatted' , RecL = 4 )
   Read( 12 , Rec = 2 ) cStr
   Read( 12 , Rec = 1 ) iVar1 , iVar2 , rVar1 , rVar2
   Write( * , * ) cStr
   Write( * , * ) iVar1 , iVar2 , rVar1 , rVar2
   Close( 12 )
End Program main


  這裡的 Open 裡指定了 RecL = 4(記錄長度是 16 位元組)。
  第一個 Read 語句,直接讀取第二筆記錄(也就是第 17 位元組到第 32 位元組)。讀取出的 cStr = "ABCDEFGHIGKLMNOP"。
  第二個 Read 語句,返回來讀取第一筆記錄(也就是前面 16 個位元組)。讀取出的資料分別放入 4 個 4 位元組的變數。(其中前面兩個是整型,後面兩個是實型)
輸出結果為:
ABCDEFGHIGKLMNOP
          271          783    2581.192       1.6892716E-07
  看到這個結果,就說明我們成功了。
  同時我們可以看到,第一個語句,我們直接跳到第二條記錄讀取,並沒有讀取第一條。這就是直接讀取資料的方便。有時候我們根本不需要某些資料,這時候,我們可以直接跳到某一條記錄上。這個記錄甚至可以是我們實現算出來的變數。比如:
  iRec = ( a + b ) / C
     Read( 12 , Rec = iRec ) cStr
     實現我們儲存了 100 天的資料,我們只需要第 21 天的資料,我們怎麼辦?在順序讀取時,我們可能會開闢一個 100 元素的陣列,或者迴圈執行 20 次空白的讀取。但是在直接讀取時,我們只需要執行一句 Read( 12 , Rec = 21 )。這是多麼的方便。(直接讀取和順序讀取雖然於文字檔案和二進位制檔案沒有直接的關聯,但是文字檔案通常用順序讀取,而二進位制檔案通常用直接讀取。這是他們的性質決定的。)
    (2)RecL = 2【記錄長度為 8 位元組】

引用 Program main
   Implicit None
   Integer*4 :: iVar1 , iVar2
   Real*4 :: rVar1 , rVar2
   Character(Len=16) :: cStr
   Open( 12 , File = 'TestBin.Bin' , Access = 'Direct' , Form = 'Unformatted' , RecL = 2 )
   Read( 12 , Rec = 4 ) cStr( 9 : 16 )
   Read( 12 , Rec = 3 ) cStr( 1 : 8   )
   Read( 12 , Rec = 1 ) iVar1 , iVar2 
   Read( 12 , Rec = 2 ) rVar1 , rVar2
   Write( * , * ) cStr
   Write( * , * ) iVar1 , iVar2 , rVar1 , rVar2
   Close( 12 )
End Program main

    
  這裡設定的 RecL = 2 ,意思是一筆記錄 8 個位元組。所以我們不能一次讀取 cStr 這個 16 位元組的字串。我們必須分兩次讀取。第一次讀取第 4 筆記錄,放入字串後半段。第二次讀取第 3 筆記錄,放入字串前半段。(可以調換位置)。然後讀取第一筆記錄的兩個整型變數和第二筆記錄的兩個實型變數。
  輸出結果和(1)的方法一樣。
  (3)寫入二進位制檔案
  寫入二進位制檔案同樣需要考慮 RecL 的問題。我們這裡以 RecL = 4 來舉例。

引用 Program main
   Implicit None
   Open( 12 , File = 'TestBinW.Bin' , Access = 'Direct' , Form = 'Unformatted' , RecL = 4 )
   Write( 12 , Rec = 1 ) 271 , 783 , 2581.192_4 , 1.6892716E-07
   Write( 12 , Rec = 2 ) "ABCDEFGHIGKLMNOP"
   Close( 12 )
End Program main



  寫入二進位制檔案和讀取二進位制檔案是差不多的,我就不再解釋了。需要注意的是,如果直接寫入第 N 筆記錄,而檔案沒有隻有 M 筆記錄(M < N),那麼,第 M+1 到第 N-1 筆記錄會用 0 填充。也就是說,二進位制檔案不會出現斷裂。

  二進位制檔案的讀寫是比較靈活的,實際應用中,我們使用哪種方式,我們應該根據自己的情況來設計。如何選擇合適的記錄長度 RecL,如何設計高效的儲存方式等。