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òng | Và GIỮ LẠI ký tự đó trong chuỗi |
| Luôn thêm ký tự kết thúc | Nên kết quả luôn là chuỗi hợp lệ |
| Trả về con trỏ dích, hoặc NULL | NULL 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àm | Trả gì | Nghĩa |
|---|---|---|
| feof(f) | khác 0 | Lầ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ử
- 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. - 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. - 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.
- 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. - Viết
char c = fgetc(f)thay vìint crồ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
fgetsgiữ 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ừ
fgetschưa nói lên điều gì. Hỏiferrorđể 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.
fgetctrả vềintvì nó cần biểu diễn 256 giá trị byte cộng thêmEOF.