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

Mở và đóng file

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

  • Nhớ tác dụng của sáu chế độ fopen lên file đã tồn tại
  • Dùng hậu tố b và biết nó thay đổi gì trên Windows
  • Báo lỗi bằng errno và perror thay vì thông báo chung chung
  • Nêu hai lý do phải kiểm tra giá trị trả về của fclose

Một lời gọi fopen viết sai chế độ có thể xóa sạch dữ liệu người dùng trong một phần nghìn giây, mà trình biên dịch không hề cảnh báo. Bài này đi qua sáu chế độ, hai cái bẫy và một mẫu viết đúng.

#fopen và sáu chế độ

#include <stdio.h>

FILE *fopen(const char *duong_dan, const char *che_do);

/* Tra ve con tro toi luong, hoac NULL neu that bai. */
Chế độĐọcGhiFile chưa cóFile đã cóVị trí ban đầu
"r"cókhônglỗi, trả NULLgiữ nguyênđầu file
"w"khôngcótạo mớiXÓA SẠCHđầu file
"a"khôngcótạo mớigiữ nguyêncuối file
"r+"cócólỗi, trả NULLgiữ nguyênđầu file
"w+"cócótạo mớiXÓA SẠCHđầu file
"a+"cócótạo mớigiữ nguyênđọc từ đầu, ghi ở cuối
terminal
# Tạo một file 20 byte rồi thử từng chế độ
./thu-che-do
fopen("co-san.txt", "rb") -> OK, kich thuoc sau khi mo = 20
fopen("co-san.txt", "ab") -> OK, kich thuoc sau khi mo = 20
fopen("khong-co.txt", "rb") -> NULL, errno=2 (No such file or directory)

# sau khi mo bang "wb" roi dong ngay:
fopen("co-san.txt", "rb") -> OK, kich thuoc sau khi mo = 0

#Hậu tố b và chế độ văn bản

fopen("a.txt", "r")      /* che do van ban  */
fopen("a.dat", "rb")     /* che do nhi phan */
fopen("a.dat", "rb+")    /* doc ghi nhi phan, chu b dat truoc dau cong */
Hệ điều hànhChế độ văn bản làm gìChế độ nhị phân làm gì
Linux, macOSKhông làm gì cảKhông làm gì cả
WindowsĐổi \n thành \r\n khi ghi và ngược lại khi đọc, coi Ctrl+Z là kết thúc fileKhông làm gì cả

#Kiểm tra và báo lỗi

Không kiểm tra, hoặc báo chung chung
FILE *f = fopen(ten, "r");
fgets(dong, sizeof dong, f);      /* f co the la NULL -> SAP */

/* Hoac kiem tra nhung bao khong ra gi: */
if (!f) {
    printf("Loi mo file!\n");     /* loi gi? tep nao? */
    return 1;
}
Nói rõ tệp nào và lỗi gì
#include <errno.h>
#include <string.h>

FILE *f = fopen(ten, "r");
if (!f) {
    fprintf(stderr, "%s: %s\n", ten, strerror(errno));

    return 1;
}
errno
Biến toàn cục do thư viện chuẩn đặt khi một hàm thất bại. Nó cho biết nguyên nhân cụ thể, và mỗi nguyên nhân có một hằng số tên riêng như ENOENT hay EACCES.
Hằng sốSốNghĩaThường vì
ENOENT2No such file or directoryGõ sai tên, hoặc thư mục cha không tồn tại
EACCES13Permission deniedKhông có quyền, hoặc đường dẫn là một thư mục
EMFILE24Too many open filesQuên fclose trong vòng lặp
ENOSPC28No space left on deviceĐĩa đầy, lộ ra lúc ghi chứ không lúc mở
EISDIR21Is a directoryTruyền tên thư mục cho fopen
terminal
./thu-loi
fopen khong-ton-tai.txt: No such file or directory
fopen thu-muc-khong-co/a.txt: No such file or directory
fopen thu muc: Permission denied

#fclose và hai lý do phải kiểm tra

int fclose(FILE *f);        /* tra 0 neu OK, EOF neu that bai */
  1. fclose xả bộ đệm trước khi đóng

    Những byte cuối cùng của bạn có thể còn nằm trong bộ đệm. Nếu lần ghi đó thất bại vì đĩa đầy hay vì hạn ngạch, thì đây là nơi duy nhất lỗi lộ ra. Mọi lời gọi fwrite trước đó đều đã trả về thành công.

  2. fclose trả số hiệu file lại cho hệ điều hành

    Mỗi tiến trình có hạn mức số file mở đồng thời. Quên fclose trong một vòng lặp là cạn hạn mức, và mọi lời gọi fopen sau đó đều thất bại.

terminal
# Vòng lặp mở file mà không đóng
./ro-ri
mo that bai sau 509 lan, errno=24 (Too many open files)
# Xem hạn mức trên Linux
ulimit -n
1024
Có đường thoát bỏ qua fclose
for (int i = 0; i < n; ++i) {
    FILE *f = fopen(danh_sach[i], "r");
    if (!f) continue;                      /* RO RI: khong dong */

    xu_ly(f);

    if (co_loi(f)) return -1;              /* RO RI: thoat som */

    fclose(f);
}
Mỗi đường ra đều đi qua fclose
for (int i = 0; i < n; ++i) {
    FILE *f = fopen(danh_sach[i], "r");
    if (!f) {
        fprintf(stderr, "%s: %s\n", danh_sach[i], strerror(errno));

        continue;
    }

    int kq = xu_ly(f);

    if (fclose(f) != 0)                    /* dong TRUOC khi thoat */
        fprintf(stderr, "%s: loi khi dong: %s\n",
                danh_sach[i], strerror(errno));

    if (kq != 0) return -1;
}

#Mẫu mở đóng chuẩn

Mẫu cho hàm chỉ đọc
#include <errno.h>
#include <stdio.h>
#include <string.h>

int dem_dong(const char *ten, long *ra) {
    FILE *f = fopen(ten, "r");
    if (!f) {
        fprintf(stderr, "%s: %s\n", ten, strerror(errno));

        return -1;
    }

    long n = 0;
    int  c;

    while ((c = fgetc(f)) != EOF)
        if (c == '\n') ++n;

    int loi_doc = ferror(f);       /* hoi TRUOC khi dong */

    fclose(f);

    if (loi_doc) {
        fprintf(stderr, "%s: loi khi doc\n", ten);

        return -1;
    }

    *ra = n;

    return 0;
}
Mẫu cho hàm có ghi
int luu_diem(const char *ten, const int *diem, size_t n) {
    FILE *f = fopen(ten, "wb");
    if (!f) {
        fprintf(stderr, "%s: %s\n", ten, strerror(errno));

        return -1;
    }

    int loi = 0;

    if (fwrite(diem, sizeof diem[0], n, f) != n) {
        fprintf(stderr, "%s: ghi khong day du\n", ten);
        loi = -1;
    }

    if (fclose(f) != 0) {          /* KIEM TRA, va no van chay du loi o tren */
        fprintf(stderr, "%s: %s\n", ten, strerror(errno));
        loi = -1;
    }

    return loi;
}

Tự làm thử

  1. Viết chương trình tạo file 20 byte rồi mở nó bằng cả sáu chế độ, in kích thước sau mỗi lần mở.
  2. Chạy chương trình so sánh số byte của "w" và "wb" trên hệ điều hành bạn đang dùng.
  3. Viết vòng lặp mở file mà không đóng, đếm xem mở được bao nhiêu lần trước khi thất bại.
  4. Thử fopen với tên một thư mục và xem errno là gì trên máy bạn.
  5. Viết lại hàm luu_diem ở trên sao cho nó thoát sớm bằng goto mà vẫn đóng tệp đúng một lần, rồi so sánh hai cách xem cách nào dễ đọc hơn.

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

  • Sáu chế độ khác nhau ở ba điểm: có đọc được không, có ghi được không, và làm gì với tệp đã tồn tại.
  • Chế độ "w" xóa sạch tệp ngay lúc mở, trước khi bạn ghi một byte nào.
  • Luôn thêm b cho dữ liệu nhị phân. Trên Windows, chế độ văn bản chèn thêm byte và làm hỏng dữ liệu.
  • Kiểm tra fopen trả về NULL, và báo lỗi kèm tên tệp cùng strerror(errno).
  • Khi ghi thì fclose là lần ghi cuối cùng, nên phải kiểm tra giá trị trả về của nó.