Bài 18.424 phút đọc
fprintf và fscanf
Sau bài này bạn sẽ làm được
- Dùng fprintf để ghi ra file và ra luồng lỗi chuẩn
- Kể ba nhược điểm của fscanf khi đọc dữ liệu thật
- Kiểm tra số mục đọc được và dùng %n để biết đã đọc tới đâu
- Viết vòng đọc fgets cộng sscanf có báo lỗi theo từng dòng
fprintf là bạn cũ đội tên mới, học năm phút là xong. fscanf thì ngược lại: nó trông tiện, chạy được với dữ liệu sạch, rồi âm thầm bỏ sót hai phần ba dữ liệu thật của bạn.
#fprintf, một hàm ba chỗ dùng
int printf ( const char *dinh_dang, ...);
int fprintf(FILE *f, const char *dinh_dang, ...);
int sprintf(char *dich, const char *dinh_dang, ...);
/* printf(...) chinh la fprintf(stdout, ...) */Ghi một báo cáo ra file
#include <stdio.h>
int xuat_bao_cao(const char *ten, const SinhVien *ds, size_t n) {
FILE *f = fopen(ten, "w");
if (!f) { perror(ten); return -1; }
fprintf(f, "%-10s %-24s %6s\n", "MSSV", "HO TEN", "TB");
fprintf(f, "%-10s %-24s %6s\n", "----------", "------------------------",
"------");
for (size_t i = 0; i < n; ++i)
fprintf(f, "%-10s %-24s %6.2f\n", ds[i].mssv, ds[i].ho_ten,
ds[i].diem_tb);
fprintf(f, "\nTong cong %zu sinh vien\n", n);
if (fclose(f) != 0) { perror(ten); return -1; }
return 0;
}terminal
cat bao-cao.txt
MSSV HO TEN TB ---------- ------------------------ ------ 20210001 Nguyen Van An 8.25 20210002 Tran Thi Binh 7.10 20210003 Le Hoang Cuong 9.00 Tong cong 3 sinh vien
#Ba nhược điểm của fscanf
int fscanf(FILE *f, const char *dinh_dang, ...);
/* Tra ve SO MUC gan duoc thanh cong,
hoac EOF neu that bai truoc khi gan duoc muc nao. */terminal
# Tệp so.txt: ba dòng, dòng giữa có một từ hỏng
cat so.txt
10 20 30 abc 40 50
./thu-fscanf
--- fscanf, khong xu ly loi --- lan 1: 10 + 20 = 30 dung sau 1 lan. Con lai gi trong tep? vi tri hien tai: [abc ] --- fgets + sscanf --- dong 1: 10 + 20 = 30 dong 2: SAI DINH DANG, bo qua dong 3: 40 + 50 = 90
| Nhược điểm | Chuyện gì xảy ra | Hậu quả |
|---|---|---|
| Không nuốt được dữ liệu hỏng | Ký tự không khớp vẫn nằm nguyên trong luồng | Vòng lặp dừng sớm, hoặc quay vô tận nếu bạn không thoát |
| Không phân biệt được lỗi với hết file | Trả về số nhỏ hơn mong đợi trong cả hai trường hợp | Không báo được cho người dùng là dòng nào hỏng |
| %s không có giới hạn mặc định | Đọc tới khoảng trắng đầu tiên, dài bao nhiêu cũng ghi | Tràn bộ đệm, cùng loại lỗi với gets ở Bài 11.3 |
#Mẫu đúng: fgets cộng sscanf
doc-diem.c
#include <ctype.h>
#include <stdio.h>
#include <string.h>
/* Tra ve 1 neu phan con lai chi gom khoang trang */
static int chi_con_khoang_trang(const char *s) {
while (isspace((unsigned char)*s)) ++s;
return *s == '\0';
}
int doc_diem(const char *ten) {
FILE *f = fopen(ten, "r");
if (!f) { perror(ten); return -1; }
char dong[256];
size_t so_dong = 0, so_hop_le = 0, so_hong = 0;
while (fgets(dong, sizeof dong, f)) {
++so_dong;
/* Bo qua dong rong va dong chu thich */
if (dong[0] == '\n' || dong[0] == '#') continue;
char mssv[16];
float diem;
int het = 0;
if (sscanf(dong, "%15s %f%n", mssv, &diem, &het) == 2
&& chi_con_khoang_trang(dong + het)) {
ghi_nhan(mssv, diem);
++so_hop_le;
} else {
dong[strcspn(dong, "\n")] = '\0';
fprintf(stderr, "%s:%zu: sai dinh dang: %s\n", ten, so_dong, dong);
++so_hong;
}
}
int loi = ferror(f);
fclose(f);
if (loi) { fprintf(stderr, "%s: loi khi doc\n", ten); return -1; }
fprintf(stderr, "%zu dong: %zu hop le, %zu hong\n",
so_dong, so_hop_le, so_hong);
return so_hong == 0 ? 0 : 1;
}#Dùng %n để biết đã đọc tới đâu
int het = 0;
sscanf(dong, "%d %d%n", &a, &b, &het);
/* %n KHONG doc gi ca. No ghi vao bien so ky tu da xu ly
TINH TOI THOI DIEM DO. Va no KHONG duoc tinh vao gia tri
tra ve cua sscanf. */#Ghi và đọc phải khớp nhau
Cặp hàm ghi và đọc của bạn là một hợp đồng. Nếu chúng nằm ở hai chỗ xa nhau trong mã nguồn thì sớm muộn sẽ có người sửa một bên mà quên bên kia.
Hai hàm ở hai nơi
/* trong luu.c */
fprintf(f, "%s,%s,%d\n", sv->mssv, sv->ho_ten, sv->nam_sinh);
/* trong tai.c, cach do 400 dong */
sscanf(dong, "%15[^,],%63[^,],%d", sv->mssv, sv->ho_ten, &sv->nam_sinh);
/* Hom nay ai do them truong "lop" vao ham ghi ma quen ham doc.
Chuong trinh van chay, chi la truong nam_sinh gio nhan
ma lop. Khong ai phat hien ra trong ba thang. */Hai hàm cạnh nhau, dùng chung hằng số
/* trong sinhvien.c, HAI HAM CANH NHAU */
#define DINH_DANG_GHI "%s,%s,%d,%.2f\n"
#define DINH_DANG_DOC "%15[^,],%63[^,],%d,%f"
#define SO_TRUONG 4
int sv_ghi(FILE *f, const SinhVien *sv) {
return fprintf(f, DINH_DANG_GHI, sv->mssv, sv->ho_ten,
sv->nam_sinh, sv->diem_tb) < 0 ? -1 : 0;
}
int sv_doc(const char *dong, SinhVien *sv) {
return sscanf(dong, DINH_DANG_DOC, sv->mssv, sv->ho_ten,
&sv->nam_sinh, &sv->diem_tb) == SO_TRUONG ? 0 : -1;
}Đặc tả tập ký tự
%[^,] /* doc moi ky tu TRU dau phay */
%15[^,] /* nhu tren, toi da 15 ky tu */
%[^\n] /* doc ca dong, ke ca khoang trang */
%[0-9] /* chi doc chu so */
%[^,\n] /* dung o dau phay HOAC xuong dong */
/* Day la cach duy nhat de scanf doc duoc chuoi CO KHOANG TRANG.
%s luon dung o khoang trang dau tien. */Tự làm thử
- Viết hàm xuất báo cáo có căn lề bằng
fprintfvà kiểm tra kết quả bằng cách mở file lên xem. - Tạo file có một dòng hỏng ở giữa, chạy vòng lặp
fscanfvà đếm xem xử lý được bao nhiêu dòng. - Viết lại bằng
fgetscộngsscanfvà xác nhận nó xử lý đúng các dòng còn lại. - Thêm
%nvào và thử với các dòng"10 20","10 20 rac"và"10 20abc". - Viết cặp hàm
sv_ghivàsv_docdùng chung hằng số định dạng, rồi ghi ra rồi đọc lại một trăm bản ghi và so sánh.
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
fprintfgiống hệtprintf, chỉ thêm tham số luồng. Dùngsnprintfchứ đừng dùngsprintf.fscanfkhông nuốt được dữ liệu hỏng, nên một từ sai làm vòng lặp dừng sớm hoặc quay vô tận.- Mẫu đúng là
fgetsđọc trọn một dòng rồisscanfphân tích dòng đó, và báo lỗi kèm số dòng. %ncho biết đã xử lý tới ký tự thứ mấy, dùng để từ chối dòng còn rác ở cuối.- Hàm ghi và hàm đọc phải nằm cạnh nhau và dùng chung hằng số định dạng.