2018年2月25日 星期日

『C語言』二進位檔(binary file)與文字檔(text file)的差異 程式範例/完整說明

檔案內容是怎麼被儲存的

檔案的儲存單位是位元組(byte),一個位元組(byte) 由8個位元(bit) 組成,一個位元(bit)可以表示兩個數值 0 與 1,可以用連續位元(bit) 的組合來表示更大的二進位數值。

範例:

1 個位元(bit): 可表示 (2 的 1 次方 = 2種值,從 0 ~ 1)
(二進制)     (十進制)   (16進制)
0                  0                0x00
1                  1                0x01

2 個位元(bit): 可表示 (2 的 2 次方 = 4種值,從 0 ~ 3)
(二進制)     (十進制)   (16進制)
00                0               0x00
01                1               0x01
10                2               0x02
11                3               0x03

一個位元組(byte),8 個位元(bit) 可表示 (2 的 8 次方 = 256種值,從 0 ~ 255)
(二進制)     (十進制)   (16進制)
00000000    0                0x00
00000001    1                0x01
00000010    2                0x02
...
11111101    253             0xFD
11111110    254             0xFE
11111111    255             0xFF

兩個位元組(bytes),16 個位元(bit),可表示 (2 的 16 次方 = 65536種值,從 0 ~ 65535)
(二進制)                     (十進制)   (16進制)
0000000000000000      0              0x0000
0000000000000001      1              0x0001
0000000000000010      2              0x0002
...
1111111111111101       65533         0xFFFD
1111111111111110       65534         0xFFFE
1111111111111111        65535         0xFFFF

因為檔案的內容是由一個一個位元組(byte) 組合而成,把兩個或四個位元組(bytes) 組合起來一起看,可以表示不同的數值範圍。

文字檔(text file) 是怎麼被儲存的?

其實文字檔的內容也是由位元組(byte) 組成,只是要怎麼去解讀其內容。

最基本的 ASCII 編碼,以一個位元組(byte) 為單位,定義了數字範圍 0 - 127 與文數字等符號的對應表。

數字 32 (十進制) 表示空白 (space)
數字 97 (十進制) 表示英文小寫字母 a
數字 116 (十進制) 表示英文小寫字母 t
數字 104 (十進制) 表示英文小寫字母 h
數字 105 (十進制) 表示英文小寫字母 i
數字 115 (十進制) 表示英文小寫字母 s
數字 101 (十進制) 表示英文小寫字母 e
數字 120 (十進制) 表示英文小寫字母 x

文字編輯軟體開啟文字檔時,根據 ASCII 編碼,將每一個位元組(byte) 代表的數值轉換成對應的符號,顯示出來。

使用底下 C code 將存在 str 陣列之一連串的位元組(byte),寫入檔案 text_file。

範例使用方式
1. 把底下 C code 存成檔案,命名如 main.c
2. 輸入底下指令將 main.c 編譯成 main 
    gcc -o main main.c
3. 輸入底下指令執行程式 
    ./main
4. 分別用 od 與 cat 指令觀察 text_file 的內容。

範例:

#include<stdio.h>

#define FILENAME        "text_file"

int main(int argc, char *argv[])
{
        FILE *fp = NULL;
        char str[] = {116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 116, 101, 120, 116};

        fp = fopen(FILENAME, "w");
        fwrite(str, sizeof(str), 1, fp);
        fclose(fp);
        return 0;
}

結果:

1. 執行指令 od -t d1 text_file 

0000000  116  104  105  115   32  105  115   32   97   32  116  101  120  116
0000016

使用 od 來顯示檔案每一個位元組(byte) 的內容,檔案的內容以十進制,一個一個位元組(bytes) 的方式顯示,可以看出檔案內容就如同 C code 裡的 str 陣列。

2. 執行指令 cat text_file 

this is a text

改用 cat 來顯示 text_file 後會看到一個句子,由此可知,實際上文字檔內容也是數字,只是 cat 以 ASCII 編碼的方式來解讀並顯示此檔案內容。

底下另一個 C code 可以建立一個內容是 this is a text 的文字檔範例。
此範例呼叫 fprintf() 可以直接以 this is a text 為參數建立檔案。

範例:

#include<stdio.h>

#define FILENAME        "text_file"

int main(int argc, char *argv[])
{
        FILE *fp = NULL;

        fp = fopen(FILENAME, "w");
        fprintf(fp, "%s", "this is a text");
        fclose(fp);
        return 0;
}

結果:

1. 執行指令 od -t d1 text_file 

0000000  116  104  105  115   32  105  115   32   97   32  116  101  120  116
0000016

結果如同上一個範例,檔案內容完全相同

2. 執行指令 cat text_file 

this is a text

顯示同上一個範例一樣的字串,由此可知,當要產生文字檔時,應該選用 fprintf 的方式會比 fwrite 的方式容易的多。

二進位檔(binary file) 是怎麼被儲存的?

從上面可知,二進位檔也是一連串位元組(byte) 所組成,但是如果不知道解讀二進位檔內容的方式,也就是位元組的組成結構,則無法使用此檔案,用文字編輯器打開就像一堆亂碼。

其實二進位檔的範圍很廣

例如:
1. 執行檔 (如剛剛 C code 產生的 main 執行檔本身)
2. 圖檔
3. 音樂檔
4. 你自己定義結構的檔案

底下 C code 建立一個名為 MY_BIN_FILE 的資料結構,對資料結構的成員填值後寫入檔案,再使用 od 與 cat 觀察內容。

範例:

#include<stdio.h>

#define FILENAME        "bin_file"

struct MY_BIN_FILE
{
        unsigned char num0;
        unsigned char num1;
        unsigned short num2;
        unsigned int num3;
};

int main(int argc, char *argv[])
{
        struct MY_BIN_FILE data;
        FILE *fp = NULL;

        fp = fopen(FILENAME, "w");

        data.num0 = 1;
        data.num1 = 100;
        data.num2 = 30000;
        data.num3 = 1234567890;

        fwrite(&data, sizeof(data), 1, fp);

        fclose(fp);
        return 0;
}

結果:

因為 num0 與 num1 都是一個位元組,所以使用 d1 顯示(以每一個位元組來做解讀)。
從結果可以看出,確實有符合寫入的預期。

od -t d1 bin_file 
0000000    1  100   48  117  -46    2 -106   73
0000010

因為 num2 是二個位元組,所以使用 d2 顯示(以每兩個位元組來做解讀)。
從結果可以看出,確實有符合寫入的預期。

od -t d2 bin_file 
0000000  25601  30000    722  18838
0000010

因為 num3 是四個位元組,所以使用 d4 顯示(以每四個位元組來做解讀)。
從結果可以看出,確實有符合寫入的預期。

od -t d4 bin_file 
0000000  1966105601  1234567890
0000010

使用 cat bin_file 會看到亂碼,請自行實驗。

由此可知,如果你有一個二進位檔(binary file),但是不知道其組成結構,就算讀取其內容,還是無法使用。必須找到正確的軟體去開啟。

到底該選擇存成二進位檔(binary file) 還是文字檔(text file)?

因為數字也可以用文字檔的方式做儲存,只是檔案佔用的空間會變多,但好處是可以透過文字編輯軟體做閱讀與編輯,不需要透過特定程式才能解讀。

底下 C code 修改了一下前面的範例,改用 fprintf 的方式來寫入成文字檔。且相同的四個數字資訊也以文字的方式儲存在檔案中,使用 od 與 cat 觀察內容,並用 wc 觀察檔案大小。

範例:

#include<stdio.h>

#define FILENAME        "text_file"

struct MY_BIN_FILE
{
        unsigned char num0;
        unsigned char num1;
        unsigned short num2;
        unsigned int num3;
};

int main(int argc, char *argv[])
{
        struct MY_BIN_FILE data;
        FILE *fp = NULL;

        fp = fopen(FILENAME, "w");

        data.num0 = 1;
        data.num1 = 100;
        data.num2 = 30000;
        data.num3 = 1234567890;

        fprintf(fp, "%d %d %d %d", data.num0, data.num1, data.num2, data.num3);

        fclose(fp);
        return 0;

}

結果:

數字都以 ASCII 的方式被儲存在檔案中
數字 48 (十進制) 表示數字 0
數字 49 (十進制) 表示數字 1

od -t d1 text_file 
0000000   49   32   49   48   48   32   51   48   48   48   48   32   49   50   51   52
0000020   53   54   55   56   57   48

使用 cat 可以看到數字,而不是亂碼

cat text_file 
1 100 30000 1234567890

使用 wc 指令加上 -c,計算檔案佔用的位元組(byte) 數,很明顯,為了保留相同的"資訊",存成二進位檔(binary file) 只有佔用 8 個位元組,但是文字檔(text file) 卻佔用 22 個位元組。

wc -c bin_file 
8 bin_file

wc -c text_file 
22 text_file

注意:以上 C code 範例為了減少版面,沒有做函數回傳值的檢查,並不是很 solid 的程式。

沒有留言:

張貼留言