Bỏ qua điều hướng, tới nội dung chính
Học C
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ểmChuyện gì xảy raHậu quả
Không nuốt được dữ liệu hỏngKý tự không khớp vẫn nằm nguyên trong luồngVò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 fileTrả về số nhỏ hơn mong đợi trong cả hai trường hợpKhô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 ghiTrà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ử

  1. Viết hàm xuất báo cáo có căn lề bằng fprintf và kiểm tra kết quả bằng cách mở file lên xem.
  2. Tạo file có một dòng hỏng ở giữa, chạy vòng lặp fscanf và đếm xem xử lý được bao nhiêu dòng.
  3. Viết lại bằng fgets cộng sscanf và xác nhận nó xử lý đúng các dòng còn lại.
  4. Thêm %n vào và thử với các dòng "10 20", "10 20 rac" và "10 20abc".
  5. Viết cặp hàm sv_ghi và sv_doc dù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

  • fprintf giống hệt printf, chỉ thêm tham số luồng. Dùng snprintf chứ đừng dùng sprintf.
  • fscanf khô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ồi sscanf phân tích dòng đó, và báo lỗi kèm số dòng.
  • %n cho 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.