Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 18.528 phút đọc

File nhị phân

Sau bài này bạn sẽ làm được

  • So sánh file văn bản và file nhị phân trên sáu tiêu chí
  • Dùng fread và fwrite đúng, kiểm tra số phần tử trả về
  • Nêu ba cạm bẫy endianness, đệm struct và con trỏ
  • Chỉ ra vì sao ghi thẳng một struct có con trỏ là vô nghĩa

File nhị phân nhỏ hơn, nhanh hơn, và cho phép sửa bản ghi thứ một triệu mà không đụng tới chín trăm nghìn bản ghi trước nó. Đổi lại, nó có ba cách làm hỏng dữ liệu mà file văn bản không có.

#Văn bản và nhị phân khác nhau ở đâu

Cùng một giá trị, hai cách lưu. Văn bản dài hơn nhưng đọc được ở mọi nơi.
Tiêu chíFile văn bảnFile nhị phân
Nội dungKý tự người đọc đượcByte thô đúng như trong bộ nhớ
Số 123455 byte4 byte
Số 3.1415926535897916 byte, và mất chính xác8 byte, đúng từng bit
Xem bằngNotepad, cat, lesshexdump, xxd, trình xem hex
Sửa bản ghi giữa filePhải ghi lại cả filefseek rồi ghi đè, một lần
Khả chuyểnCao, chỉ vướng quy ước xuống dòngThấp, vướng thứ tự byte và đệm struct
Tốc độChậm, phải phân tích chuỗiNhanh, chép thẳng bộ nhớ

#fread và fwrite

size_t fread (      void *dich,  size_t co, size_t n, FILE *f);
size_t fwrite(const void *nguon, size_t co, size_t n, FILE *f);

/* Ca hai tra ve SO PHAN TU da xu ly, KHONG PHAI so byte. */
Đọc một mảng bản ghi
SinhVien ds[100];

size_t doc_duoc = fread(ds, sizeof ds[0], 100, f);

if (doc_duoc < 100) {
    if (feof(f))
        printf("Tep chi co %zu ban ghi\n", doc_duoc);   /* binh thuong */
    else if (ferror(f))
        perror("loi doc");                              /* co van de */
}

/* Chi %zu ban ghi dau la hop le. Cac phan tu con lai
   cua mang ds VAN GIU GIA TRI CU, khong bi xoa. */
Không kiểm tra fwrite
fwrite(ds, sizeof ds[0], n, f);      /* bo qua gia tri tra ve */

/* Dia day o giua chung: ghi duoc 40 trong 100 ban ghi.
   Chuong trinh bao "Da luu thanh cong". */
So với số phần tử mong đợi
if (fwrite(ds, sizeof ds[0], n, f) != n) {
    fprintf(stderr, "Ghi khong day du, tep co the hong\n");

    return -1;
}

Nhìn tận mắt những byte đã ghi

xem-byte.c
#include <stdio.h>
#include <stddef.h>
#include <string.h>

typedef struct { char mssv[8]; int nam; double tb; } SV;

int main(void) {
    printf("sizeof(SV) = %d\n", (int)sizeof(SV));
    printf("  mssv @ %d (8 byte)\n", (int)offsetof(SV, mssv));
    printf("  nam  @ %d (4 byte)\n", (int)offsetof(SV, nam));
    printf("  tb   @ %d (8 byte)\n", (int)offsetof(SV, tb));

    SV a;
    memset(&a, 0, sizeof a);                 /* xoa ca phan dem */
    strcpy(a.mssv, "2021001");
    a.nam = 2003;
    a.tb  = 8.25;

    FILE *f = fopen("sv.dat", "wb");
    fwrite(&a, sizeof a, 1, f);
    fclose(f);

    f = fopen("sv.dat", "rb");
    unsigned char b[64];
    size_t n = fread(b, 1, sizeof b, f);
    fclose(f);

    for (size_t i = 0; i < n; ++i) {
        if (i % 8 == 0) printf("  %02d:", (int)i);
        printf(" %02X", b[i]);
        if (i % 8 == 7) printf("\n");
    }

    return 0;
}
terminal
gcc -std=c11 -o xem-byte xem-byte.c && ./xem-byte
sizeof(SV) = 24
  mssv @ 0 (8 byte)
  nam  @ 8 (4 byte)
  tb   @ 16 (8 byte)

24 byte trong sv.dat:
  00: 32 30 32 31 30 30 31 00
  08: D3 07 00 00 00 00 00 00
  16: 00 00 00 00 00 80 20 40

#Ba cạm bẫy

Cạm bẫy một: thứ tự byte

int v = 12345;                 /* 0x00003039 */

fwrite(&v, sizeof v, 1, f);

/* Tren may nho truoc (x86, ARM thong thuong):  39 30 00 00
   Tren may lon truoc (mang, mot so vi dieu khien): 00 00 30 39

   Ghi tren may nay, doc tren may kia, ban duoc 959447040
   thay vi 12345. Khong co canh bao nao. */
Cách chữa: đóng gói tường minh
#include <stdint.h>

/* Ghi mot so 32 bit theo thu tu byte lon truoc, luon luon */
static int ghi_u32(FILE *f, uint32_t v) {
    unsigned char b[4] = {
        (unsigned char)(v >> 24),
        (unsigned char)(v >> 16),
        (unsigned char)(v >>  8),
        (unsigned char)(v      ),
    };

    return fwrite(b, 1, 4, f) == 4 ? 0 : -1;
}

static int doc_u32(FILE *f, uint32_t *ra) {
    unsigned char b[4];

    if (fread(b, 1, 4, f) != 4) return -1;

    *ra = ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16)
        | ((uint32_t)b[2] <<  8) |  (uint32_t)b[3];

    return 0;
}

/* Hai ham nay cho ket qua GIONG NHAU tren moi may,
   vi chung khong bao gio doc byte cua mot so nguyen tu bo nho.
   Chung chi lam toan hoc tren gia tri. */

Cạm bẫy hai: đệm trong struct

Trình biên dịch hay kiến trúcsizeof(SV)Vì sao
x86-64, gcc, mặc định24double cần thẳng hàng 8 byte
ARM 32 bit, gcc, mặc định24double cũng thẳng hàng 8 byte
ARM 32 bit, ABI cũ20double chỉ cần thẳng hàng 4 byte
Bất kỳ, có #pragma pack(1)20Bỏ hết đệm, nhưng truy cập chậm hơn
Cùng một mã nguồn, tệp ghi ra có kích thước bản ghi khác nhau. Bên đọc không có cách nào biết.

Cạm bẫy ba: con trỏ

con-tro.c
typedef struct { char *ten; int tuoi; } SVP;

SVP a;
a.ten  = malloc(32);
strcpy(a.ten, "Nguyen Van A");
a.tuoi = 20;

fwrite(&a, sizeof a, 1, f);      /* GHI DIA CHI, khong ghi noi dung */
terminal
./thu-con-tro
da ghi 16 byte
gia tri con tro luc ghi: 0000000000C224C0
byte trong file: C0 24 C2 00 00 00 00 00 14 00 00 00 00 00 00 00
=> 8 byte dau la DIA CHI, khong phai chu 'Nguyen Van A'

doc lai: tuoi=20 dung, ten=0000000000C224C0 LA RAC
Struct có con trỏ
typedef struct { char *ten; int tuoi; } SVP;

fwrite(&sv, sizeof sv, 1, f);
Không có con trỏ trong dữ liệu ghi ra
/* Cach 1: mang co dinh, don gian nhat */
typedef struct { char ten[64]; int tuoi; } SV;

/* Cach 2: ghi do dai roi ghi noi dung, khi do dai thay doi nhieu */
static int ghi_chuoi(FILE *f, const char *s) {
    uint32_t n = (uint32_t)strlen(s);

    if (ghi_u32(f, n) != 0)              return -1;
    if (fwrite(s, 1, n, f) != n)         return -1;

    return 0;
}

#Chép file theo khối

chep-khoi.c
#define KHOI 65536

int chep(const char *tu, const char *toi) {
    FILE *vao = fopen(tu, "rb");
    if (!vao) { perror(tu); return -1; }

    FILE *ra = fopen(toi, "wb");
    if (!ra) { perror(toi); fclose(vao); return -1; }

    static char dem[KHOI];           /* static: khong an ngan xep */
    int    loi = 0;
    size_t n;

    while ((n = fread(dem, 1, sizeof dem, vao)) > 0)
        if (fwrite(dem, 1, n, ra) != n) { loi = 1; break; }

    if (ferror(vao)) loi = 1;

    fclose(vao);
    if (fclose(ra) != 0) loi = 1;

    return loi ? -1 : 0;
}
terminal
# Chép một tệp 20 MB bằng hai cách
./do-chep
fgetc/fputc tung byte : 1.168 giay
fread/fwrite khoi 64 KB: 0.013 giay

Tự làm thử

  1. Chạy chương trình xem-byte.c trên máy bạn và so sánh sizeof(SV) cùng các độ lệch với kết quả ở trên.
  2. Bỏ dòng memset đi rồi chạy lại vài lần, xem bốn byte đệm có đổi giá trị không.
  3. Viết cặp ghi_u32 và doc_u32 rồi kiểm tra chúng khớp nhau với các giá trị 0, 1, 255, 256 và UINT32_MAX.
  4. Ghi một struct có con trỏ ra file, in mười sáu byte đầu dạng hex, và chỉ ra tám byte nào là địa chỉ.
  5. Đo tốc độ chép một file 20 MB bằng fgetc và bằng khối 64 KB trên máy bạn.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • fread và fwrite trả về số phần tử, nên tham số kích thước phải là kích thước một phần tử.
  • Thứ tự byte khác nhau giữa các kiến trúc, nên số nguyên phải được đóng gói tường minh từng byte.
  • Đệm trong struct làm kích thước bản ghi khác nhau giữa các nền tảng, và phần đệm còn mang theo rác từ bộ nhớ.
  • Ghi struct có con trỏ là ghi một địa chỉ, không phải ghi nội dung. Dùng mảng cố định hoặc ghi độ dài rồi ghi nội dung.
  • Chép theo khối 64 KB nhanh hơn từng byte khoảng chín mươi lần, và phải kiểm ferror vì fread trả 0 trong cả hai trường hợp hết file và lỗi.