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
| Tiêu chí | File văn bản | File nhị phân |
|---|---|---|
| Nội dung | Ký tự người đọc được | Byte thô đúng như trong bộ nhớ |
| Số 12345 | 5 byte | 4 byte |
| Số 3.14159265358979 | 16 byte, và mất chính xác | 8 byte, đúng từng bit |
| Xem bằng | Notepad, cat, less | hexdump, xxd, trình xem hex |
| Sửa bản ghi giữa file | Phải ghi lại cả file | fseek rồi ghi đè, một lần |
| Khả chuyển | Cao, chỉ vướng quy ước xuống dòng | Thấp, vướng thứ tự byte và đệm struct |
| Tốc độ | Chậm, phải phân tích chuỗi | Nhanh, 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úc | sizeof(SV) | Vì sao |
|---|---|---|
| x86-64, gcc, mặc định | 24 | double cần thẳng hàng 8 byte |
| ARM 32 bit, gcc, mặc định | 24 | double cũng thẳng hàng 8 byte |
| ARM 32 bit, ABI cũ | 20 | double chỉ cần thẳng hàng 4 byte |
| Bất kỳ, có #pragma pack(1) | 20 | Bỏ hết đệm, nhưng truy cập chậm hơn |
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ử
- Chạy chương trình
xem-byte.ctrên máy bạn và so sánhsizeof(SV)cùng các độ lệch với kết quả ở trên. - 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. - Viết cặp
ghi_u32vàdoc_u32rồi kiểm tra chúng khớp nhau với các giá trị 0, 1, 255, 256 vàUINT32_MAX. - 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ỉ.
- Đo tốc độ chép một file 20 MB bằng
fgetcvà 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
freadvàfwritetrả 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
ferrorvìfreadtrả 0 trong cả hai trường hợp hết file và lỗi.