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ế độ | Đọc | Ghi | File chưa có | File đã có | Vị trí ban đầu |
|---|---|---|---|---|---|
| "r" | có | không | lỗi, trả NULL | giữ nguyên | đầu file |
| "w" | không | có | tạo mới | XÓA SẠCH | đầu file |
| "a" | không | có | tạo mới | giữ nguyên | cuối file |
| "r+" | có | có | lỗi, trả NULL | giữ nguyên | đầu file |
| "w+" | có | có | tạo mới | XÓA SẠCH | đầu file |
| "a+" | có | có | tạo mới | giữ 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ành | Chế độ văn bản làm gì | Chế độ nhị phân làm gì |
|---|---|---|
| Linux, macOS | Khô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 file | Khô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ĩa | Thường vì |
|---|---|---|---|
| ENOENT | 2 | No such file or directory | Gõ sai tên, hoặc thư mục cha không tồn tại |
| EACCES | 13 | Permission denied | Không có quyền, hoặc đường dẫn là một thư mục |
| EMFILE | 24 | Too many open files | Quên fclose trong vòng lặp |
| ENOSPC | 28 | No space left on device | Đĩa đầy, lộ ra lúc ghi chứ không lúc mở |
| EISDIR | 21 | Is a directory | Truyề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 */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
fwritetrước đó đều đã trả về thành công.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
fclosetrong một vòng lặp là cạn hạn mức, và mọi lời gọifopensau đó đề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ử
- 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ở.
- 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. - 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.
- Thử
fopenvới tên một thư mục và xemerrnolà gì trên máy bạn. - Viết lại hàm
luu_diemở trên sao cho nó thoát sớm bằnggotomà 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
bcho 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
fopentrả về NULL, và báo lỗi kèm tên tệp cùngstrerror(errno). - Khi ghi thì
fcloselà lần ghi cuối cùng, nên phải kiểm tra giá trị trả về của nó.