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

Đọc cả file vào bộ nhớ

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

  • Viết hàm đọc cả file xử lý đủ mọi nhánh lỗi
  • Giải thích vì sao ftell không cho kích thước đúng ở chế độ văn bản
  • Viết bản đọc theo khối tăng dần, chạy được với ống dẫn
  • Quyết định khi nào nên đọc cả file và khi nào nên đọc theo dòng

Đọc trọn một file vào bộ nhớ rồi xử lý là cách viết gọn nhất và nhanh nhất cho phần lớn công việc. Hàm làm việc đó chỉ mười lăm dòng, nhưng bản viết vội có ba chỗ hỏng, và cả ba đều chỉ lộ ra khi gặp dữ liệu thật.

#Thành ngữ đọc cả file

doc-ca-file.c
#include <stdio.h>
#include <stdlib.h>

/* Doc toan bo mot tep vao bo nho.
   Tra ve con tro toi vung da cap phat, hoac NULL neu that bai.
   NGUOI GOI phai free ket qua.
   Neu ra_co khac NULL thi so byte doc duoc duoc ghi vao do. */
char *doc_ca_file(const char *ten, size_t *ra_co) {
    FILE *f = fopen(ten, "rb");              /* PHAI la "rb" */
    if (!f) return NULL;

    if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }

    long co = ftell(f);
    if (co < 0)                     { fclose(f); return NULL; }

    rewind(f);

    char *bo_dem = malloc((size_t)co + 1);   /* cong 1 cho ky tu ket thuc */
    if (!bo_dem)                    { fclose(f); return NULL; }

    size_t doc = fread(bo_dem, 1, (size_t)co, f);

    if (ferror(f)) { free(bo_dem); fclose(f); return NULL; }

    fclose(f);

    bo_dem[doc] = '\0';                      /* dung doc, KHONG dung co */

    if (ra_co) *ra_co = doc;

    return bo_dem;
}
  1. Mở bằng "rb", không phải "r"

    Ở chế độ văn bản trên Windows, ftell cho số byte trên đĩa còn fread trả về số ít hơn, vì mỗi cặp \r\n bị gộp lại. Bài 18.6 đã đo chênh lệch đó.

  2. Nhảy tới cuối, hỏi vị trí, quay về đầu

    Ba lời gọi này là cách duy nhất mà chuẩn C cho phép đo kích thước một tệp. Không có hàm nào tên là filesize.

  3. Cấp phát dư một byte

    Byte thừa đó chứa ký tự kết thúc chuỗi, nhờ vậy kết quả dùng được ngay với strstr, strtok và mọi hàm chuỗi.

  4. Đặt ký tự kết thúc tại vị trí fread trả về

    Không phải tại co. Nếu hai số khác nhau thì phần giữa chúng là bộ nhớ chưa khởi tạo, và đọc nó là hành vi không xác định.

Dùng nó
size_t co;
char  *noi_dung = doc_ca_file("cau-hinh.txt", &co);

if (!noi_dung) { perror("cau-hinh.txt"); return 1; }

printf("Tep dai %zu byte\n", co);

if (strstr(noi_dung, "debug=1")) bat_go_loi();

free(noi_dung);                    /* DUNG QUEN */

#Ba chỗ hỏng của nó

Hỏng một: rò rỉ ở mọi nhánh lỗi

Thoát sớm quên fclose
char *doc_ca_file(const char *ten) {
    FILE *f = fopen(ten, "rb");
    if (!f) return NULL;

    fseek(f, 0, SEEK_END);
    long co = ftell(f);
    rewind(f);

    char *bo_dem = malloc(co + 1);
    if (!bo_dem) return NULL;          /* RO RI: f chua dong */

    fread(bo_dem, 1, co, f);
    bo_dem[co] = '\0';

    return bo_dem;                     /* RO RI: f chua dong */
}
Một điểm dọn dẹp duy nhất
/* Moi duong ra deu di qua fclose. Xem lai ban day du o tren:
   moi lenh return deu co { fclose(f); return NULL; } */

/* Cach khac, dung goto khi co nhieu tai nguyen: */
char *doc_ca_file(const char *ten, size_t *ra_co) {
    char *bo_dem = NULL;
    FILE *f = fopen(ten, "rb");

    if (!f) return NULL;

    if (fseek(f, 0, SEEK_END) != 0) goto xong;

    long co = ftell(f);
    if (co < 0) goto xong;

    rewind(f);

    bo_dem = malloc((size_t)co + 1);
    if (!bo_dem) goto xong;

    size_t doc = fread(bo_dem, 1, (size_t)co, f);

    if (ferror(f)) { free(bo_dem); bo_dem = NULL; goto xong; }

    bo_dem[doc] = '\0';
    if (ra_co) *ra_co = doc;

xong:
    fclose(f);

    return bo_dem;
}

Hỏng hai: ftell thất bại với ống dẫn

terminal
# Truyền tên tệp: chạy tốt
./dem-tu cau-hinh.txt
Tep dai 1284 byte
# Truyền qua ống dẫn: hỏng
cat cau-hinh.txt | ./dem-tu
loi: khong doc duoc
fseek(f, 0, SEEK_END);      /* voi ong dan: TRA VE KHAC 0, that bai */
long co = ftell(f);         /* hoac tra ve -1 */

/* Neu ban khong kiem tra thi:
     co la -1
     malloc(-1 + 1) tuc malloc(0), tra ve con tro hop le nhung 0 byte
     fread ghi vao do -> TRAN BO DEM

   Neu ban ep sang size_t:
     (size_t)(-1) la 18446744073709551615
     malloc that bai, tra NULL
     may man hon, nhung van la loi. */

Hỏng ba: tin vào kích thước mà không có giới hạn

long co = ftell(f);
char *bo_dem = malloc((size_t)co + 1);

/* Nguoi dung tro chuong trinh vao mot tep 40 GB.
   malloc that bai, ban tra NULL, coi nhu on.

   Nhung neu chuong trinh cua ban la mot dich vu mang
   va co la 8 GB thi may chu cua ban vua bi chiem het RAM
   boi mot yeu cau duy nhat. */

#define TOI_DA (64L * 1024 * 1024)      /* 64 MB */

if (co > TOI_DA) {
    fprintf(stderr, "%s: tep qua lon (%ld byte, toi da %ld)\n",
            ten, co, TOI_DA);
    fclose(f);

    return NULL;
}

#Bản không cần biết kích thước

doc-ca-luong.c
#include <stdio.h>
#include <stdlib.h>

/* Doc ca luong, khong dung fseek nen chay duoc voi ONG DAN.
   Nguoi goi phai free ket qua. */
char *doc_ca_luong(FILE *f, size_t *ra_co) {
    size_t co = 0, suc_chua = 4096;
    char  *bo_dem = malloc(suc_chua);

    if (!bo_dem) return NULL;

    for (;;) {
        if (co + 1 >= suc_chua) {                    /* chua cho ky tu ket thuc */
            size_t moi_sc = suc_chua * 2;
            char  *moi    = realloc(bo_dem, moi_sc);

            if (!moi) { free(bo_dem); return NULL; }

            bo_dem   = moi;
            suc_chua = moi_sc;
        }

        size_t n = fread(bo_dem + co, 1, suc_chua - co - 1, f);

        co += n;

        if (n == 0) break;                           /* het luong hoac loi */
    }

    if (ferror(f)) { free(bo_dem); return NULL; }

    bo_dem[co] = '\0';

    if (ra_co) *ra_co = co;

    return bo_dem;
}
terminal
# Từ tệp
./doc-luong to.bin
doc duoc 20971520 byte
# Từ ống dẫn, dữ liệu nhỏ
cat vb.txt | ./doc-luong
doc duoc 11 byte
20 byte dau: [mot
hai
ba
]
# Từ ống dẫn, 20 MB
cat to.bin | ./doc-luong
doc duoc 20971520 byte

#Đọc cả file hay đọc theo dòng

Công việcNên dùngVì sao
Đếm dòng, đếm từ, tính tổng một cộtĐọc theo dòngBộ nhớ cố định, tệp lớn bao nhiêu cũng chạy
Tìm chuỗi có thể nằm vắt qua hai dòngĐọc cả fileTheo dòng thì phải tự ghép, rất dễ sai
Phân tích cú pháp có cấu trúc lồng nhauĐọc cả fileBộ phân tích cần nhìn lùi và nhìn tới
Xử lý tệp nhật ký hàng gigabyteĐọc theo dòngKhông có cách nào khác
Nạp tệp cấu hình lúc khởi độngĐọc cả fileNhỏ, và mã gọn hơn hẳn
Đọc từ mạng hoặc ống dẫn không tin cậyTheo dòng, hoặc cả file CÓ giới hạnBên kia có thể gửi vô hạn

Tự làm thử

  1. Cài doc_ca_file đầy đủ và kiểm tra nó với tệp rỗng, tệp một byte và tệp không tồn tại.
  2. Chạy nó với đầu vào qua ống dẫn và xác nhận nó báo lỗi thay vì cho kết quả sai.
  3. Cài doc_ca_luong với sức chứa ban đầu 16 byte để ép realloc chạy nhiều lần, rồi kiểm tra kết quả vẫn đúng.
  4. Thêm giới hạn kích thước và thử với một tệp vượt giới hạn.
  5. Viết hàm tách dòng tại chỗ và so sánh tốc độ với vòng fgets trên tệp 20 MB.

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

  • Đo kích thước tệp bằng bộ ba fseek tới cuối, ftell, rồi rewind, và tệp phải mở ở chế độ nhị phân.
  • Đặt ký tự kết thúc tại vị trí fread trả về, không phải tại kích thước đo được.
  • Mọi nhánh lỗi đều phải đóng tệp. Nhãn dọn dẹp ở cuối hàm cùng goto là mẫu chuẩn cho việc này.
  • fseek thất bại với ống dẫn, nên bản đọc theo khối tăng dần mới là bản chạy được ở mọi nơi.
  • Luôn đặt giới hạn kích thước khi tệp do người ngoài cung cấp.