Mọi thứ chương trình tính toán đều biến mất khi nó thoát, trừ khi nó ghi lại. Giao diện làm việc với tệp của C nằm trong <stdio.h>, cùng header với printf, và được thiết kế tương tự có chủ đích: một tệp là một luồng (stream) byte, và các hàm làm việc với màn hình đều có bản song sinh nhận thêm đối số là một luồng.
Kiểu handle là FILE *. Bạn không bao giờ nhìn vào bên trong một FILE; đó là kiểu mờ (opaque type, xem typedef) và bạn chỉ giữ con trỏ tới nó.
Ba bước
Mọi thao tác với tệp đều theo cùng một khuôn: mở, dùng, đóng.
Kiểm tra NULL không phải là lo xa thừa thãi. fopen thất bại bất cứ khi nào tệp không tồn tại (ở chế độ đọc), thư mục không ghi được, đường dẫn sai hoặc tiến trình đã hết handle tệp, và trường hợp nào cũng trả về NULL. Dùng một handle NULL sẽ gây segmentation fault.
Gửi thông báo lỗi ra stderr thay vì stdout là quy ước chung: nó không có bộ đệm, và có thể chuyển hướng riêng biệt với đầu ra thật.
Các chế độ của fopen
Đối số thứ hai là một chuỗi chế độ ngắn. Chọn sai là lỗi gây hậu quả nặng nhất trong cả mảng này, vì "w" âm thầm xóa sạch một tệp đã có.
| Chế độ | Đọc | Ghi | Nếu tệp đã tồn tại | Nếu chưa tồn tại |
|---|---|---|---|---|
"r" | có | không | mở ở đầu tệp | thất bại, trả về NULL |
"w" | không | có | bị xóa sạch nội dung | được tạo mới |
"a" | không | có | chỉ ghi vào cuối tệp | được tạo mới |
"r+" | có | có | mở ở đầu tệp | thất bại, trả về NULL |
"w+" | có | có | bị xóa sạch nội dung | được tạo mới |
"a+" | có | có | đọc ở bất kỳ đâu, ghi vào cuối tệp | được tạo mới |
Thêm b vào bất kỳ chế độ nào ("rb", "wb", "ab+") để dùng chế độ nhị phân, chi tiết ở cuối bài.
Hai quy tắc giúp tránh mất dữ liệu thật:
- Dùng
"r"khi bạn định đọc."r+"với tên tệp gõ nhầm sẽ thất bại an toàn;"w+"tạo một tệp rỗng và bạn chỉ mất chút thời gian. Còn"w"với đúng tên tệp trong khi bạn định dùng"r"sẽ phá hủy dữ liệu. - Dùng
"a"cho log. Mọi lệnhfprintfđều ghi vào cuối tệp bất kể luồng đang ở vị trí nào, đúng như điều một tệp log cần.
Ghi và đọc trong cùng một chương trình
Trình soạn thảo bên dưới chạy trọn một vòng: tạo tệp, ghi các bản ghi vào, đóng lại, mở lại để đọc và in ra những gì tìm thấy.
fprintf và fscanf chính là printf và scanf với một luồng làm đối số đầu tiên; mọi thứ về định dạng của chúng đều giống hệt, kể cả độ rộng %31s giúp name không bị tràn.
Điều kiện vòng lặp là == 2, đúng số mục mà chuỗi định dạng yêu cầu. So sánh với EOF thay vào đó là một bug kinh điển: một dòng sai định dạng khiến fscanf trả về 0 chứ không phải EOF, và vòng lặp quay mãi trên dữ liệu mà nó không đọc được.
Đọc từng dòng với fgets
fscanf tiện cho dữ liệu có định dạng cứng nhắc. Với tệp văn bản, như cấu hình, log, CSV, bất cứ thứ gì do con người viết, hãy đọc nguyên dòng. Đây là vòng lặp chuẩn mực:
Điều khiến fgets là lựa chọn mặc định đúng:
- Nó nhận kích thước bộ đệm, nên không thể tràn. Truyền
sizeof linethì lệnh gọi vẫn đúng khi bạn đổi kích thước mảng. - Nó trả về
NULLở cuối tệp hoặc khi có lỗi, một điều kiện vòng lặp gọn gàng. - Nó giữ lại ký tự xuống dòng khi dòng vừa với bộ đệm. Điều đó có ích: không có
'\n'trong dữ liệu nhận được nghĩa là dòng dài hơn bộ đệm và phần còn lại vẫn đang chờ.strcspn(line, "\n")tìm chỉ số của ký tự xuống dòng (hoặc độ dài chuỗi nếu không có), nên gán'\0'vào đó sẽ cắt nó đi trong cả hai trường hợp.
Để phân biệt cuối tệp thật với lỗi, hãy hỏi sau vòng lặp:
if (ferror(in)) {
fprintf(stderr, "read error\n");
} else if (feof(in)) {
/* normal end */
}
Đừng viết while (!feof(fp)) làm điều kiện vòng lặp. feof chỉ trở thành đúng sau khi một lần đọc đã thất bại, nên vòng lặp đó xử lý nội dung bộ đệm cuối cùng thêm một lần. Hãy kiểm tra giá trị trả về của hàm đọc, như cả hai vòng lặp ở trên.
Từng ký tự một: fgetc và fputc
Với công việc ở mức byte, như đếm ký tự, biến đổi một tệp, sao chép, fgetc và fputc xử lý một ký tự mỗi lần gọi.
Một chi tiết quan trọng: c được khai báo là int, không phải char. fgetc trả về int để có thể trả về mọi giá trị byte và giá trị đánh dấu riêng biệt EOF (bằng -1). Lưu nó vào một char khiến phép so sánh với EOF hoặc luôn sai, hoặc đúng nhầm với byte 0xFF, tùy vào char có dấu hay không trên nền tảng của bạn. Đây là một trong những cái bẫy lâu đời nhất của C.
Kiểm tra lỗi đúng cách
Một lần đọc trong môi trường production trông như sau:
errno giữ mã mô tả lỗi gần nhất và strerror biến nó thành một câu; perror in thông báo của bạn kèm câu đó trong một lần gọi. Chúng lần lượt cần <errno.h> và <string.h>.
fclose cũng có thể thất bại, vì nó đẩy dữ liệu trong bộ đệm ra và lần ghi đó có thể không vừa trên ổ đĩa, nên với dữ liệu quan trọng, hãy kiểm tra nó:
if (fclose(fp) != 0) {
fprintf(stderr, "failed to flush and close\n");
}
Chế độ nhị phân trong một đoạn
Chế độ văn bản có thể chuyển đổi ký tự xuống dòng (trên Windows, \n thành \r\n khi ghi và ngược lại khi đọc) và có thể xử lý đặc biệt một số byte. Với dữ liệu không phải văn bản, như ảnh, một struct ghi nguyên trạng, một khối dữ liệu nén, hãy mở với b và dùng fread/fwrite để chuyển byte thô:
fwrite(ptr, size, count, fp) ghi count mục, mỗi mục size byte, và trả về số mục ghi được; fread hoạt động tương tự theo chiều ngược lại. Lưu ý rằng một tệp ghi theo cách này gắn chặt với máy đã ghi nó, vì phần đệm của struct, kích thước số nguyên và thứ tự byte đều lọt vào dữ liệu, nên nó ổn cho tệp cache hoặc tệp tạm, nhưng không phù hợp với định dạng mà chương trình khác phải đọc.
Lỗi thường gặp
- Không kiểm tra
fopenvớiNULL. Lần sập sau đó bị đổ cho lệnh đọc, không phải lệnh mở. - Mở bằng
"w"khi định dùng"r". Tệp bị xóa sạch trước khi bạn kịp nhận ra. - Quên
fclose. Đầu ra trong bộ đệm bị mất, và mỗi tệp không đóng làm rò rỉ một handle. while (!feof(fp)). Xử lý dòng cuối hai lần. Hãy kiểm tra lệnh gọi đọc.char c = fgetc(fp). Làm hỏng phép so sánh vớiEOF. Hãy dùngint.fscanf("%s", buf)không có độ rộng. Cùng lỗi tràn bộ đệm như scanf khi đọc từ bàn phím.- Đường dẫn tương đối.
fopen("data.txt", "r")tìm trong thư mục làm việc, không phải cạnh tệp thực thi. Nếu một tệp "biến mất", thường là vì lý do này.
Câu hỏi thường gặp
Mở tệp trong C như thế nào?
FILE *fp = fopen("data.txt", "r"); mở một tệp để đọc và trả về một handle kiểu FILE *, hoặc NULL nếu thất bại. Luôn kiểm tra NULL trước khi dùng handle, và gọi fclose(fp) khi đã xong.
Các chế độ của fopen trong C là gì?
"r" để đọc (tệp phải tồn tại), "w" để ghi (tạo mới, hoặc xóa sạch nội dung tệp đã có), "a" để ghi nối (tạo mới, ghi vào cuối tệp). Thêm + để mỗi chế độ vừa đọc vừa ghi: "r+", "w+", "a+". Thêm b ("rb", "wb") để mở ở chế độ nhị phân.
Đọc tệp từng dòng trong C như thế nào?
Dùng fgets trong vòng lặp while: while (fgets(line, sizeof line, fp) != NULL) { ... }. Hàm này dừng ở mỗi ký tự xuống dòng hoặc khi bộ đệm đầy, trả về NULL ở cuối tệp, và không thể tràn bộ đệm vì bạn truyền kích thước bộ đệm vào.
Vì sao tệp của tôi trống sau khi ghi trong C?
Thường nhất là do bạn quên fclose. Đầu ra được đưa vào bộ đệm, nên dữ liệu có thể vẫn nằm trong bộ nhớ khi chương trình kết thúc bất thường. fclose đẩy dữ liệu ra và đóng tệp; fflush(fp) đẩy dữ liệu ra mà không đóng. Nguyên nhân còn lại là mở bằng "w" lần thứ hai, việc này xóa sạch tệp bạn vừa ghi.