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

Đọc ghi văn bản theo dòng

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

  • Đọc file theo dòng bằng fgets và bỏ ký tự xuống dòng đúng cách
  • Giải thích vì sao while (!feof(f)) xử lý thừa một lần
  • Phân biệt kết thúc file với lỗi đọc bằng feof và ferror
  • Xử lý dòng dài hơn bộ đệm mà không mất dữ liệu

Đọc một file văn bản theo dòng là việc bạn sẽ làm hàng trăm lần. Nó chỉ có một hàm và bốn cái bẫy, và cả bốn đều là những lỗi mà người viết C lâu năm vẫn mắc.

#fgets và ký tự xuống dòng còn sót

char *fgets(char *dich, int n, FILE *f);
Nó làm gìChi tiết
Đọc tối đa n trừ 1 ký tựChừa một chỗ cho ký tự kết thúc chuỗi
Dừng sớm nếu gặp ký tự xuống dòngVà GIỮ LẠI ký tự đó trong chuỗi
Luôn thêm ký tự kết thúcNên kết quả luôn là chuỗi hợp lệ
Trả về con trỏ dích, hoặc NULLNULL nghĩa là hết file hoặc có lỗi, chưa biết cái nào

Vòng đọc đúng

doc-dong.c
#include <stdio.h>
#include <string.h>

int main(void) {
    FILE *f = fopen("du-lieu.txt", "r");
    if (!f) { perror("du-lieu.txt"); return 1; }

    char   dong[1024];
    size_t so_dong = 0;

    while (fgets(dong, sizeof dong, f)) {
        dong[strcspn(dong, "\n")] = '\0';

        printf("%zu: %s\n", ++so_dong, dong);
    }

    if (ferror(f)) { perror("du-lieu.txt"); fclose(f); return 1; }

    fclose(f);

    return 0;
}

#Lỗi while không feof

Kiểm tra feof trước khi đọc
while (!feof(f)) {
    fgets(dong, sizeof dong, f);
    xu_ly(dong);
}
Dùng thẳng giá trị trả về
while (fgets(dong, sizeof dong, f)) {
    xu_ly(dong);
}
terminal
# Tệp có đúng ba dòng: mot, hai, ba
./thu-feof
--- while (!feof(f)) ---
lan 1: [mot
]
lan 2: [hai
]
lan 3: [ba
]
lan 4: [ba
]

--- while (fgets(...)) ---
lan 1: [mot]
lan 2: [hai]
lan 3: [ba]

#Phân biệt hết file với lỗi đọc

Khi fgetstrả về NULL, có hai khả năng hoàn toàn khác nhau: đã đọc hết file bình thường, hoặc đĩa hỏng giữa chừng. Nếu bạn không phân biệt thì chương trình sẽ báo "xử lý xong" trong khi mới đọc được một nửa.

HàmTrả gìNghĩa
feof(f)khác 0Lần đọc trước đã chạm cuối file
ferror(f)khác 0Đã có lỗi thật sự trên luồng này
clearerr(f)không trả gìXóa cả hai cờ trên
while (fgets(dong, sizeof dong, f)) {
    xu_ly(dong);
}

/* Toi day fgets da tra NULL. Vi sao? */

if (ferror(f)) {
    fprintf(stderr, "%s: loi khi doc: %s\n", ten, strerror(errno));
    fclose(f);

    return -1;                      /* KHONG bao la thanh cong */
}

/* Khong co loi, vay la het tep binh thuong */
fclose(f);

return 0;

#Dòng dài hơn bộ đệm

terminal
# Tệp có 3 dòng, dòng giữa dài 30 ký tự. Bộ đệm chỉ 10 byte.
./thu-dong-dai
lan 1: [ngan]  do dai 4  (het dong)
lan 2: [012345678]  do dai 9  (CHUA het dong)
lan 3: [901234567]  do dai 9  (CHUA het dong)
lan 4: [890123456]  do dai 9  (CHUA het dong)
lan 5: [789]  do dai 3  (het dong)
lan 6: [cuoi]  do dai 4  (het dong)

Cách một: phát hiện và báo lỗi

Đủ dùng cho hầu hết trường hợp
#include <string.h>

#define DONG_TOI_DA 1024

/* Doc mot dong. Tra ve:
     1  doc duoc mot dong day du
     0  het tep
    -1  dong qua dai hoac loi doc */
int doc_mot_dong(FILE *f, char *dong, size_t co) {
    if (!fgets(dong, (int)co, f)) return ferror(f) ? -1 : 0;

    size_t n = strcspn(dong, "\n");

    if (dong[n] == '\n') {                /* co ky tu xuong dong: du roi */
        dong[n] = '\0';

        return 1;
    }

    /* Khong co ky tu xuong dong. Hai kha nang:
         a. dong dai hon bo dem
         b. dong cuoi tep khong ket thuc bang xuong dong */
    int c = fgetc(f);

    if (c == EOF) return ferror(f) ? -1 : 1;   /* truong hop b, van hop le */

    ungetc(c, f);                              /* tra lai byte vua doc */

    /* Truong hop a: nuot phan con lai cua dong roi bao loi */
    while ((c = fgetc(f)) != EOF && c != '\n') { }

    return -1;
}

Cách hai: cấp phát lớn dần

Khi dòng có thể dài tùy ý
#include <stdlib.h>
#include <string.h>

/* Doc mot dong do dai bat ky. Nguoi goi phai free ket qua.
   Tra ve NULL khi het tep hoac loi. */
char *doc_dong_dong(FILE *f) {
    size_t co = 128, n = 0;
    char  *bo_dem = malloc(co);

    if (!bo_dem) return NULL;

    for (;;) {
        if (!fgets(bo_dem + n, (int)(co - n), f)) {
            if (n == 0) { free(bo_dem); return NULL; }   /* chua doc gi */

            break;                                       /* dong cuoi khong co '\n' */
        }

        n += strlen(bo_dem + n);

        if (n > 0 && bo_dem[n - 1] == '\n') { bo_dem[--n] = '\0'; break; }

        if (n + 1 < co) break;                  /* fgets dung vi het tep */

        char *moi = realloc(bo_dem, co * 2);    /* Bai 14.8 */
        if (!moi) { free(bo_dem); return NULL; }

        bo_dem = moi;
        co    *= 2;
    }

    return bo_dem;
}

#fputs, fgetc và fputc

int fputs(const char *s, FILE *f);      /* KHONG tu them xuong dong */
int puts(const char *s);                /* TU them xuong dong */

int fputc(int c, FILE *f);
int fgetc(FILE *f);                     /* tra int, xem phan tren */
Chép một file, cách đơn giản nhất
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; }

    int c;
    while ((c = fgetc(vao)) != EOF)
        if (fputc(c, ra) == EOF) break;      /* dung ngay khi ghi hong */

    int loi = ferror(vao) || ferror(ra);

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

    return loi ? -1 : 0;
}

Tự làm thử

  1. Viết chương trình in ra từng dòng của một file kèm số thứ tự, dùng strcspn để bỏ ký tự xuống dòng.
  2. Chạy cả hai vòng lặp while (!feof(f)) và while (fgets(...)) trên cùng một file ba dòng và so sánh.
  3. Cố ý dùng bộ đệm mười byte để đọc file có dòng ba mươi ký tự, đếm xem chương trình thấy bao nhiêu dòng.
  4. Cài doc_mot_dong ở trên và xác nhận nó báo lỗi đúng với dòng quá dài, mà vẫn chấp nhận dòng cuối không có ký tự xuống dòng.
  5. Viết char c = fgetc(f) thay vì int c rồi đọc một file nhị phân, xem chương trình dừng ở đâu.

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

  • fgets giữ lại ký tự xuống dòng, và dong[strcspn(dong, "\n")] = '\0' là cách bỏ nó an toàn.
  • Điều kiện vòng lặp phải chính là lời gọi đọc. while (!feof(f)) xử lý dòng cuối hai lần.
  • NULL từ fgets chưa nói lên điều gì. Hỏi ferror để biết là hết file hay đĩa hỏng.
  • Dòng dài hơn bộ đệm bị cắt thành nhiều mảnh mà không báo gì. Phải kiểm tra xem có ký tự xuống dòng hay không.
  • fgetc trả về int vì nó cần biểu diễn 256 giá trị byte cộng thêm EOF.