Xử lý tệp tin (File Handling)
- 07-11-2023
- Toanngo92
- 0 Comments
Mục lục
Giới thiệu
Hầu hết các chương trình cần đọc và ghi dữ liệu vào các hệ thống lưu trữ dựa trên đĩa cứng. Trình xử lý văn bản cần lưu trữ các tệp văn bản, bảng tính cần lưu trữ nội dung của các ô, và cơ sở dữ liệu cần lưu trữ các bản ghi. Buổi học này khám phá các cơ chế trong ngôn ngữ C để nhập và xuất (I/O) dữ liệu đến hệ thống đĩa.
Ngôn ngữ C không chứa bất kỳ câu lệnh I/O tường minh nào. Tất cả các hoạt động I/O được thực hiện bằng cách sử dụng các hàm từ thư viện tiêu chuẩn C. Cách tiếp cận này khiến hệ thống tệp C trở nên mạnh mẽ và linh hoạt. I/O trong C là duy nhất vì dữ liệu có thể được chuyển dưới dạng biểu diễn nhị phân nội bộ hoặc dưới dạng văn bản dễ đọc cho con người. Điều này giúp tạo ra các tệp phù hợp với mọi nhu cầu.
Quan trọng để hiểu sự khác biệt giữa tệp và luồng (stream). Hệ thống I/O trong ngôn ngữ C cung cấp một giao diện cho người dùng, độc lập với thiết bị cụ thể được truy cập. Giao diện này không thực sự là một tệp mà là một biểu diễn trừu tượng của thiết bị. Giao diện trừu tượng này được gọi là luồng (stream), và thiết bị thực sự được gọi là tệp (file).
Luồng File
Hệ thống tệp C hoạt động với nhiều loại thiết bị khác nhau bao gồm máy in, ổ đĩa cứng, ổ đĩa băng và các thiết bị terminal. Mặc dù tất cả các thiết bị này rất khác nhau về cơ bản, hệ thống tệp đệm biến đổi mỗi thiết bị thành một thiết bị logic được gọi là luồng. Vì tất cả các luồng hoạt động tương tự, nên dễ dàng xử lý các thiết bị khác nhau. Có hai loại luồng – luồng văn bản và luồng nhị phân.
Luồng Văn Bản
Luồng văn bản là một chuỗi các ký tự. Các luồng văn bản có thể được tổ chức thành các dòng kết thúc bằng ký tự xuống dòng mới (new line). Tuy nhiên, ký tự xuống dòng mới là tùy chọn ở dòng cuối và được xác định bởi việc triển khai. Hầu hết các trình biên dịch C không kết thúc luồng văn bản bằng ký tự xuống dòng mới. Trong luồng văn bản, một số dịch chuyển ký tự cụ thể có thể xảy ra theo yêu cầu của môi trường. Ví dụ, ký tự xuống dòng mới có thể được chuyển thành cặp ký tự carriage return và line feed. Do đó, có thể không có một mối quan hệ một-một giữa các ký tự được viết (hoặc đọc) và các ký tự trên thiết bị bên ngoài. Hơn nữa, do sự chuyển đổi có thể xảy ra, số lượng ký tự được viết (hoặc đọc) có thể không giống nhau so với số ký tự trên thiết bị bên ngoài.
Luồng Nhị Phân
Luồng nhị phân là một chuỗi các byte có mối quan hệ một-một với các byte trên thiết bị bên ngoài, tức là không có sự dịch chuyển ký tự. Ngoài ra, số lượng byte được viết (hoặc đọc) giống nhau với số lượng trên thiết bị bên ngoài. Luồng nhị phân là một chuỗi phẳng của byte, không có bất kỳ cờ nào để chỉ định cuối tệp hoặc cuối bản ghi. Cuối tệp được xác định bởi kích thước của tệp.
Các hàm tệp và Cấu trúc File
Một tệp có thể liên quan đến bất cứ điều gì từ một tệp đĩa đến một thiết bị terminal hoặc máy in. Tuy nhiên, tất cả các tệp không có khả năng giống nhau. Ví dụ, một tệp đĩa có thể hỗ trợ truy cập ngẫu nhiên trong khi một bàn phím không thể. Một tệp được liên kết với một luồng bằng cách thực hiện một thao tác mở (open). Tương tự, nó sẽ được tách ra khỏi luồng bằng thao tác đóng (close). Khi một chương trình kết thúc bình thường, tất cả các tệp sẽ tự động đóng. Tuy nhiên, khi một chương trình gặp lỗi và bị tắt đột ngột, các tệp vẫn còn mở.
Các hàm cơ bản của tệp
Tên hàm | Chức năng |
fopen() | Mở tệp tin |
fclose() | Đóng tệp tin |
fputc() | Ghi một ký tự vào một tập tin |
fgetc() | Đọc một ký tự từ một tập tin |
fread() | Đọc từ tập tin vào bộ đệm |
fwrite() | Ghi từ bộ đệm vào tập tin |
fseek() | Tìm kiếm một vị trí cụ thể trong file |
fprintf() | Sử dụng như printf(), nhưng trong một tập tin |
fscanf() | Sử dụng như scanf(), nhưng trong một tập tin |
feof() | Trả về true khi nó kết thúc tập tin |
ferror() | Trả về true nếu xảy ra lỗi |
rewind() | Đặt lại định vị vị trí về đầu tệp |
remove() | Xoá một tập tin |
fflush() | Ghi dữ liệu từ bộ đệm vào một tệp cụ thể |
Các hàm trên được chứa trong tệp tiêu đề stdio.h. Tệp tiêu đề này phải được bao gồm trong một chương trình sử dụng các hàm này. Hầu hết các hàm tương tự như các hàm I/O trên bảng điều khiển. Tệp tiêu đề stdio.h cũng định nghĩa một số hằng số dùng cho xử lý tệp. Ví dụ, hằng số EOF được định nghĩa là -1, chứa giá trị được trả về khi một hàm cố gắng đọc qua cuối tệp.
Con trỏ Tệp
Con trỏ tệp là thiết yếu để đọc hoặc ghi vào các tệp. Đó là một con trỏ tới một cấu trúc chứa thông tin về tệp. Thông tin bao gồm tên tệp, vị trí hiện tại của tệp, việc tệp đang được đọc hoặc viết, và xem có xảy ra bất kỳ lỗi nào hoặc cuối tệp đã xảy ra. Người dùng không cần phải biết chi tiết, vì các định nghĩa được lấy từ stdio.h bao gồm một khai báo cấu trúc gọi là FILE. Điều duy nhất cần phải khai báo cho một con trỏ tệp được biểu tượng bằng:
FILE *fp;
Điều này cho biết rằng fp là một con trỏ tới một FILE.
Tệp văn bản
Có nhiều hàm để xử lý tệp văn bản. Chúng được thảo luận bên dưới:
Mở tệp bằng văn bản
Hàm fopen() mở một luồng để sử dụng và liên kết một tệp với luồng đó. Con trỏ tệp liên kết với tệp được trả về bởi hàm fopen(). Trong hầu hết các trường hợp, tệp đang được mở là một tệp đĩa. Mẫu cho hàm fopen() là:
FILE *fopen(const char *filename, const char *mode);
trong đó filename là một con trỏ đến một chuỗi ký tự tạo thành tên tệp hợp lệ và cũng có thể bao gồm thông số đường dẫn.
Chuỗi được trỏ bởi mode xác định cách mà tệp sẽ được mở. Bảng bên dưới hiển thị các chế độ hợp lệ để mở tệp.
Chế độ | Ý nghĩa |
r | Mở một tệp tin văn bản để đọc |
w | Tạo một tập tin văn bản để viết |
a | Nối vào một tập tin văn bản |
r+ | Mở tệp văn bản để đọc/ghi |
w+ | Tạo một tập tin văn bản để đọc/ghi |
a+f | Nối hoặc tạo một tệp văn bản để đọc/ghi |
Như có thể thấy từ bảng trên, các tệp có thể được mở ở chế độ văn bản hoặc chế độ nhị phân. Một con trỏ null được trả về nếu xảy ra lỗi khi hàm fopen() mở một tệp. Lưu ý rằng các chuỗi như ‘a+f’ cũng có thể được biểu diễn như ‘af+’
Nếu một tệp xyz được mở để ghi, mã cho nó sẽ như sau:
FILE *fp;
fp = fopen("xyz", "w");
Tuy nhiên, một tệp thường được mở bằng cách sử dụng một tập hợp các lệnh tương tự như sau:
FILE *fp;
if ((fp = fopen("xyz", "w")) == NULL) {
printf("Không thể mở tệp");
exit(1);
}
Hằng số NULL được định nghĩa trong stdio.h như ‘\0’. Nếu một tệp được mở bằng cách sử dụng phương pháp như trên, hàm fopen() sẽ phát hiện bất kỳ lỗi nào khi mở tệp, chẳng hạn như bảo vệ ghi hoặc đĩa đầy, trước khi cố gắng ghi vào tệp. NULL được sử dụng để chỉ ra sự thất bại vì không bao giờ có một con trỏ tệp nào có giá trị đó.
Nếu một tệp được mở để ghi, bất kỳ tệp nào có cùng tên và đã mở trước đó sẽ bị ghi đè. Điều này xảy ra vì khi một tệp được mở trong chế độ ghi, một tệp mới được tạo ra. Nếu muốn thêm bản ghi vào một tệp hiện có, tệp đó nên được mở với chế độ ‘a’. Nếu tệp được mở trong chế độ đọc và nó không tồn tại, một lỗi sẽ được trả về. Nếu tệp được mở cho các hoạt động đọc/ghi, nó sẽ không bị xóa nếu nó tồn tại. Tuy nhiên, nếu nó không tồn tại, nó sẽ được tạo ra.
Theo tiêu chuẩn ANSI, tám tệp có thể được mở cùng một lúc. Tuy nhiên, hầu hết các trình biên dịch C (và môi trường) cho phép mở nhiều hơn tám tệp.
Đóng một tệp văn bản
Vì có một giới hạn về số lượng tệp có thể được mở cùng một lúc, việc đóng tệp sau khi đã sử dụng là quan trọng. Điều này giải phóng tài nguyên hệ thống và giảm nguy cơ vượt quá giới hạn đã đặt. Đóng một luồng cũng đẩy bất kỳ bộ đệm liên quan nào (một hoạt động quan trọng để ngăn mất dữ liệu) khi ghi vào đĩa. Hàm fclose() đóng một luồng đã được mở bằng cách gọi hàm fopen(). Hàm này viết bất kỳ dữ liệu còn lại nào trong bộ đệm đĩa vào tệp. Mẫu cho fclose() là:
int fclose(FILE *fp);
trong đó fp là con trỏ tệp.
Hàm fclose() trả về một giá trị nguyên 0 cho việc đóng thành công. Bất kỳ giá trị trả về khác sẽ chỉ ra một lỗi. Hàm fclose() sẽ thất bại nếu đĩa bị gỡ ra khỏi ổ đĩa một cách không đúng cách hoặc không còn không gian trống trên đĩa.
Chức năng khác được sử dụng để đóng luồng là hàm fcloseall(). Hàm này hữu ích khi cần đóng nhiều luồng đã mở cùng một lúc. Nó đóng tất cả các luồng đã mở và trả về số lượng luồng đã đóng hoặc EOF nếu phát hiện lỗi. Nó có thể được sử dụng như sau:
int fel = fcloseall();
if (fel == EOF)
printf("Lỗi khi đóng tệp");
else
printf("%d tệp đã được đóng", fel);
Hàm fcloseall() giúp đóng nhiều luồng một cách tiện lợi và trả về thông báo về số luồng đã đóng hoặc lỗi nếu có.
Ghi một ký tự
Luồng có thể được viết vào tệp bằng cách viết từng ký tự hoặc dưới dạng chuỗi. Hãy trước tiên thảo luận về việc viết ký tự vào tệp. Hàm fputc() được sử dụng để viết các ký tự vào một tệp đã được mở trước đó bằng fopen(). Mẫu cho hàm này là:
int fputc(int ch, FILE *stream);
trong đó stream là con trỏ tệp được trả về bởi fopen() và ch là ký tự cần được viết.
Mặc dù ch được khai báo kiểu int, nó được chuyển đổi bởi fputc() thành một unsigned char. Hàm fputc() viết một ký tự vào một luồng cụ thể tại vị trí tệp hiện tại và sau đó di chuyển chỉ số vị trí tệp. Nếu fputc() thành công, nó trả về ký tự đã viết, ngược lại nó trả về EOF.
Đọc một ký tự
Hàm fgetc() được sử dụng để đọc các ký tự từ một tệp đã được mở trong chế độ đọc, sử dụng con trỏ tệp fp. Mẫu cho fgetc() là:
int fgetc(FILE *stream);
trong đó stream là con trỏ tệp kiểu FILE, được trả về bởi fopen().
Hàm fgetc() trả về ký tự tiếp theo từ vị trí hiện tại trong luồng đầu vào và tăng chỉ số vị trí tệp. Ký tự đọc được là một unsigned char và được chuyển đổi thành một số nguyên. Nếu đến cuối tệp, fgetc() trả về EOF.
Để đọc một tệp văn bản cho đến khi gặp cuối tệp, mã sẽ là:
ch = fgetc(fp);
while (ch != EOF);
Chương trình sau đây sử dụng các hàm đã thảo luận cho đến nay. Nó nhận các ký tự từ bàn phím và ghi chúng vào một tệp cho đến khi người dùng nhập một ký tự cụ thể. Sau khi người dùng đã nhập thông tin, chương trình hiển thị nội dung tệp trên màn hình.
#include <stdio.h>
int main() {
FILE *fp;
int ch;
/* Writing to file JAK */
if ((fp = fopen("jak", "w")) == NULL) {
printf("Cannot open file\n\n");
exit(1);
}
printf("Enter characters (type @ to terminate):\n");
ch = getchar();
while (ch != '@') {
fputc(ch, fp);
ch = getchar();
}
fclose(fp);
/* Reading from file JAK */
printf("\n\nDisplaying contents of file JAK\n\n");
if ((fp = fopen("jak", "r")) == NULL) {
printf("Cannot open file\n\n");
exit(1);
}
ch = fgetc(fp);
while (ch != EOF) {
putchar(ch);
ch = fgetc(fp);
}
fclose(fp);
return 0;
}
Dòng chạy ví dụ cho chương trình trên sẽ như sau:
Enter Characters (type @ to terminate):
This is the first input to the File JAKe
Displaying Contents of File JAK
This is the first input to the File JAK
Đọc/Ghi Chuỗi
Ngoài fputc() và fgetc(), C hỗ trợ các hàm liên quan khác để đọc và ghi chuỗi ký tự vào và từ một tệp đĩa. Các hàm này là:
int fputs(const char *str, FILE *fp);
char *fgets(char *str, int length, FILE *fp);
Hàm fputs() hoạt động tương tự như fputc(), ngoại trừ việc nó ghi toàn bộ chuỗi vào luồng đã chỉ định.
Nó trả về EOF nếu xảy ra lỗi.
Hàm fgets() đọc một chuỗi từ luồng đã chỉ định cho đến khi enther một ký tự xuống dòng hoặc đã đọc được length-1 ký tự. Nếu đọc được ký tự xuống dòng, nó sẽ được coi là một phần của chuỗi (khác với gets()). Chuỗi kết quả sẽ kết thúc bằng ký tự null. Hàm này trả về con trỏ đến chuỗi nếu thành công và con trỏ null nếu xảy ra lỗi.
Tập tin nhị phân
Các hàm được sử dụng để xử lý tệp nhị phân giống như các hàm được sử dụng để xử lý tệp văn bản. Tuy nhiên, các chế độ mở của các hàm fopen() khác nhau trong trường hợp tệp nhị phân
Mở tập tin nhị phân
Bảng sau liệt kê các chế độ khác nhau cho hàm fopen () trong trường hợp tệp nhị phân:
Chế độ | Ý nghĩa |
rb | Mở tệp nhị phân để đọc |
wb | Tạo một tập tin nhị phân để viết |
ab | Ghi tệp nhị phân và duy trì nội dung hiện có |
r+b | Đọc và ghi tệp nhị phân |
w+b | Ghi tệp nhị phân và đọc nó |
a+b | Ghi tệp nhị phân, duy trì nội dung hiện có và có khả năng đọc nó |
Nếu bạn muốn mở một tệp có tên “xyz” để ghi dữ liệu vào tệp dưới dạng tệp nhị phân, bạn có thể sử dụng mã như sau:
FILE *fp;
fp = fopen("xyz", "wb");
Mã trên sẽ mở tệp “xyz” để ghi dữ liệu vào tệp nhị phân. Nếu tệp đã tồn tại, nó sẽ bị ghi đè.
Đóng tệp nhị phân
Hàm fclose() cũng có thể được sử dụng để đóng các tệp nhị phân, cùng với tệp văn bản. Một ví dụ về fclose() như sau:
int fclose(FILE *fp);
trong đó fp là một con trỏ trỏ đến một tập tin đang mở.
Ghi tệp nhị phân
Một số ứng dụng đòi hỏi sử dụng tệp dữ liệu để lưu trữ các khối dữ liệu, trong đó mỗi khối bao gồm các byte liền kề. Mỗi khối thường đại diện cho một cấu trúc dữ liệu phức tạp hoặc một mảng.
Ví dụ, một tệp dữ liệu có thể chứa nhiều cấu trúc có cùng cấu trúc hoặc có thể chứa nhiều mảng cùng loại và kích thước. Trong những trường hợp như vậy, có thể mong muốn đọc hoặc ghi toàn bộ khối từ tệp dữ liệu hoặc ghi toàn bộ khối vào tệp dữ liệu thay vì đọc hoặc ghi từng thành phần riêng lẻ (ví dụ, thành viên cấu trúc hoặc phần tử mảng) bên trong từng khối.
Hàm fwrite() được sử dụng để ghi dữ liệu vào tệp dữ liệu trong những trường hợp như vậy. Hàm này có thể được sử dụng để ghi bất kỳ loại dữ liệu nào. Nguyên mẫu của hàm fwrite() như sau:
size_t fwrite(const void *buffer, size_t num_bytes, size_t count, FILE *fp);
Kiểu dữ liệu size_t là một bổ sung của ANSI C để cải thiện tính di động. Nó được định nghĩa trước đó là một kiểu số nguyên đủ lớn để chứa kết quả của fwrite(). Đối với hầu hết các hệ thống, nó có thể được coi là một số nguyên không dấu.
- buffer là con trỏ đến thông tin sẽ được ghi vào tệp.
- num_bytes xác định số byte được đọc hoặc viết.
- count xác định số mục (mỗi mục có độ dài num_bytes) được đọc hoặc viết.
- fp là con trỏ tệp đến một luồng đã mở trước đó. Tệp được mở cho các hoạt động này nên ở chế độ nhị phân.
Hàm này trả về số đối tượng đã được ghi vào tệp nếu hoạt động ghi thành công. Nếu giá trị này nhỏ hơn num, có lỗi đã xảy ra. Hàm ferror() (sẽ được thảo luận sau) có thể được sử dụng để xác định vấn đề.
Đọc tệp nhị phân
Hàm fread() có thể được sử dụng để đọc bất kỳ loại dữ liệu nào. Nguyên mẫu của hàm này là:
void fread(void *buffer, size_t numbytes, size_t count, FILE *fp);
- buffer là con trỏ đến vùng nhớ sẽ nhận dữ liệu từ tệp.
- numbytes xác định số byte cần đọc hoặc viết.
- count xác định số mục (mỗi mục có độ dài numbytes) được đọc hoặc viết.
- fp là con trỏ tệp đến một luồng đã mở trước đó. Tệp được mở cho các hoạt động này nên ở chế độ nhị phân.
Hàm này sẽ trả về số đối tượng đã đọc nếu hoạt động đọc thành công. Nó sẽ trả về 0 nếu cuối tệp đã đạt hoặc một lỗi đã xảy ra. Hàm feof() và ferror() (sẽ được thảo luận sau) có thể được sử dụng để xác định vấn đề.
Cả hai hàm fread() và fwrite() thường được gọi là các hàm đọc hoặc ghi không định dạng.
Miễn là tệp đã được mở cho các hoạt động nhị phân, fread() và fwrite() có thể đọc và ghi bất kỳ loại thông tin nào. Ví dụ, chương trình sau viết và sau đó đọc lại một số thực kiểu double, một số nguyên và một số nguyên dài từ và vào một tệp dữ liệu. Chú ý cách nó sử dụng hàm sizeof() để xác định độ dài của mỗi kiểu dữ liệu.
Ví dụ 2:
#include <stdio.h>
int main()
{
FILE *fp;
double d = 23.317;
int i = 13;
long l = 12345674;
if ((fp = fopen("jak", "wb")) == NULL)
{
printf("Không thể mở tệp\n");
return 1;
}
fwrite(&d, sizeof(double), 1, fp);
fwrite(&i, sizeof(int), 1, fp);
fwrite(&l, sizeof(long), 1, fp);
fclose(fp);
if ((fp = fopen("jak", "rb")) == NULL)
{
printf("Không thể mở tệp\n");
return 1;
}
fread(&d, sizeof(double), 1, fp);
fread(&i, sizeof(int), 1, fp);
fread(&l, sizeof(long), 1, fp);
printf("Giá trị: %lf %d %ld\n", d, i, l);
fclose(fp);
return 0;
}
Như chương trình này minh họa, bộ đệm có thể được đọc và thường chỉ là bộ nhớ được sử dụng để lưu trữ một biến. Trong chương trình đơn giản này, giá trị trả về của fread() và fwrite() được bỏ qua. Tuy nhiên, những giá trị này nên được kiểm tra lỗi để lập trình hiệu quả.
Một trong những ứng dụng hữu ích nhất của fread() và fwrite() liên quan đến việc đọc và ghi các kiểu dữ liệu do người dùng định nghĩa, đặc biệt là các cấu trúc (structures). Ví dụ, với cấu trúc như sau:
struct struct_type
{
float balance;
char name[20];
}
Câu lệnh sau ghi nội dung của biến cust vào tệp mà con trỏ fp trỏ đến:
fwrite(&cust, sizeof(struct struct_type), 1, fp);
Điều này cho phép bạn ghi cả cấu trúc struct_type vào tệp một cách dễ dàng và sau đó đọc nó trở lại từ tệp.
Chức năng xử lý tập tin
Các chức năng xử lý file khác sẽ được thảo luận trong phần này.
Sử dụng ‘feof()’
Khi một tệp được mở để đọc dưới dạng nhị phân, một giá trị số nguyên bằng với EOF có thể được đọc. Quy trình đọc đầu vào sẽ chỉ định sự kết thúc của tệp trong trường hợp như vậy, ngay cả khi sự kết thúc vật lý của tệp chưa được đạt. Một hàm feof() có thể được sử dụng trong trường hợp như vậy. Nguyên mẫu của hàm này là:
int feof(FILE *fp);
Nó trả về true (1) nếu đã đạt đến cuối tệp, ngược lại nó trả về false (0). Hàm này được sử dụng khi đọc dữ liệu nhị phân.
Đoạn mã sau đọc một tệp nhị phân cho đến khi gặp cuối tệp:
while (!feof(fp))
{
ch = fgetc(fp);
}
Chú ý: feof() trả về true sau khi đã đọc qua cuối tệp, vì vậy vòng lặp sẽ kết thúc sau khi đọc xong tất cả dữ liệu trong tệp.
Hàm ‘rewind()’
Hàm ‘rewind()’ đặt lại chỉ mục vị trí tệp ở đầu tệp. Nó nhận con trỏ tệp làm đối số của nó.
Cú pháp của hàm rewind() là:
rewind(fp);
Chương trình sau mở một tệp ở chế độ ghi/đọc, lấy chuỗi đầu vào bằng fgets(), đặt lại chỉ mục của tệp về đầu tệp và sau đó hiển thị các chuỗi tương tự bằng fputs().
Ví dụ 3:
#include <stdio.h>
int main()
{
FILE *fp;
char str[80];
/* Writing to File JAK */
if ((fp = fopen("Jak", "wt")) == NULL)
{
printf("Cannot open file \n\n");
return 1;
}
do
{
printf("Enter a string (CR to quit): \n");
gets(str);
if (str[0] != '\n')
{
strncat(str, "\n", 1); /* add a new line */
fputs(str, fp);
}
} while (str[0] != '\n');
/* Reading from File JAK */
printf("\n\nDisplaying Contents of File JAK\n\n");
rewind(fp);
while (!feof(fp))
{
fgets(str, 81, fp);
printf("%s", str);
}
fclose(fp);
return 0;
}
Mẫu chạy cho chương trình trên sẽ là:
Enter a string (CR to quit)
This is input line 1
Enter a string (CR to quit)
This is input line 2
Enter a string (CR to quit)
This is input line 3
Enter a string (CR to quit)
Displaying Contents of File JAK
This is input line 1
is is input line 2
This is input line 3
Hàm ‘ferror()’
Hàm ‘ferror()’ xác định xem một hoạt động tệp đã tạo ra một lỗi hay không. Nguyên mẫu của nó là:
int ferror(FILE *fp);
trong đó fp là con trỏ tệp hợp lệ. Nó trả về true nếu có lỗi xảy ra trong hoạt động tệp cuối cùng, nếu không, nó trả về false.
Mỗi hoạt động đặt điều kiện lỗi, nên ferror() nên được gọi ngay sau mỗi hoạt động nếu không, một lỗi có thể bị mất. Chương trình trước đó có thể được sửa đổi để kiểm tra và cảnh báo về bất kỳ lỗi nào trong quá trình ghi như sau:
...
printf("Nhập một chuỗi (Nhấn Enter để kết thúc): \n");
gets(str);
if (str[0] != '\n')
{
strncat(str, "\n", 1); /* thêm ký tự xuống dòng mới */
fputs(str, fp);
}
if (ferror(fp))
printf("\nLỖI trong quá trình ghi\n");
...
Xoá tập tin
Hàm remove() được sử dụng để xóa một tệp tin cụ thể. Nguyên mẫu của nó là:
int remove(char *filename);
Nó trả về 0 nếu thành công, ngược lại, nó trả về một giá trị khác không.
Ví dụ, xem đoạn mã sau:
printf("\nXóa tệp tin %s (Y/N): ", file1);
ans = getchar();
if (remove(file1))
{
printf("\nKhông thể xóa tệp tin");
exit(1);
}
Trong đoạn mã này, chúng ta sử dụng hàm remove() để xóa tệp file1. Nếu xóa thành công, nó trả về 0 và tệp tin sẽ được xóa. Ngược lại, nếu có lỗi, nó trả về một giá trị khác không và chúng ta hiển thị thông báo lỗi và thoát khỏi chương trình.
Xả bộ đệm của luồng
Thường thì tệp đầu ra tiêu chuẩn được đặt trong bộ đệm. Điều này có nghĩa rằng đầu ra tới tệp tin được thu thập trong bộ nhớ nhưng chưa được hiển thị cho đến khi bộ đệm đầy. Nếu chương trình gặp lỗi, một số ký tự có thể vẫn còn trong bộ đệm. Điều này có nghĩa rằng kết quả cho thấy rằng chương trình đã kết thúc sớm hơn thực tế. Hàm fflush() giải quyết vấn đề này. Như tên gọi của nó, nó làm cho bộ đệm trống. Hành động của việc làm trống phụ thuộc vào loại tệp tin. Một tệp tin mở để đọc sẽ làm cho bộ đệm đầu vào của nó được xóa, trong khi một tệp tin mở để ghi sẽ làm cho bộ đệm đầu ra của nó được ghi vào tệp tin.
Nguyên mẫu cho hàm này là:
int fflush(FILE *fp);
Hàm fflush() sẽ viết nội dung của bất kỳ dữ liệu nào trong bộ đệm vào tệp tin được liên kết với fp. Hàm fflush() với tham số null sẽ làm cho tất cả các tệp tin được mở để đầu ra được làm trống. Nó trả về 0 nếu thành công, ngược lại, nó trả về EOF.
Các luồng chuẩn
Khi một chương trình C bắt đầu thực thi dưới hệ điều hành DOS, năm luồng đặc biệt được mở tự động bởi hệ điều hành. Năm luồng này bao gồm:
- Luồng đầu vào tiêu chuẩn (stdin)
- Luồng đầu ra tiêu chuẩn (stdout)
- Luồng lỗi tiêu chuẩn (stderr)
- Luồng máy in tiêu chuẩn (stdaux)
- Luồng phụ tiêu chuẩn (stdprn)
Mặc định, chúng được gán cho giao diện console của hệ thống, trong đó stderr được gán cho cổng máy in song song đầu tiên (parallel printer port) và stdaux được gán cho cổng serial đầu tiên. Chúng được định nghĩa là con trỏ cố định với kiểu FILE, do đó chúng có thể được sử dụng bất cứ nơi nào mà việc sử dụng con trỏ FILE hợp lệ. Chúng cũng có thể dễ dàng chuyển hướng đến các luồng hoặc tệp thiết bị khác khi cần redirection.
Chương trình sau in nội dung của tệp lên máy in.
Ví dụ 4:
#include <stdio.h>
int main()
{
char buff[81], fname[13];
printf("Nhập tên Tệp Nguồn:");
gets(fname);
FILE *in;
if (!(in = fopen(fname, "r")))
{
fputs("\nKhông tìm thấy tệp", stderr);
/* hiển thị thông báo lỗi trên luồng lỗi tiêu chuẩn thay vì luồng đầu ra tiêu chuẩn */
exit(1);
}
while (!feof(in))
{
if (fgets(buff, 81, in))
{
fputs(buff, stdprn);
/* Gửi dòng đến máy in */
}
}
fclose(in);
return 0;
}
Chú ý việc sử dụng luồng stderr với hàm fputs() trong chương trình trên. Nó được sử dụng thay vì hàm printf() vì đích đến của hàm printf() là stdout, có thể bị điều hướng. Nếu đầu ra của một chương trình bị điều hướng và xảy ra lỗi trong quá trình thực thi, thì bất kỳ thông báo lỗi nào được đưa vào luồng stdout cũng sẽ bị điều hướng. Để tránh điều này, luồng stderr được sử dụng để hiển thị thông báo lỗi trên màn hình vì đích đến của luồng stderr cũng là màn hình, nhưng luồng stderr không thể bị điều hướng. Nó luôn hiển thị thông báo trên màn hình.
Con trỏ hoạt động hiện tại
Để theo dõi vị trí mà các thao tác I/O diễn ra, một con trỏ được duy trì trong cấu trúc FILE. Khi một ký tự được đọc từ hoặc viết vào luồng, con trỏ hiện tại hoạt động (được gọi là curp) được tăng. Hầu hết các hàm I/O tham chiếu đến curp và cập nhật nó sau khi quá trình đọc hoặc ghi trên luồng. Vị trí hiện tại của con trỏ hiện tại hoạt động có thể được tìm thấy với sự trợ giúp của hàm ftell(). Hàm ftell() trả về một giá trị kiểu long int dài mô tả vị trí của curp từ đầu của tệp trong luồng được chỉ định. Nguyên mẫu của hàm ftell() như sau:
long int ftell(FILE *fp);
Trích đoạn sau đây của một chương trình hiển thị vị trí của con trỏ hiện tại trên luồng fp.
printf("Vị trí hiện tại của con trỏ tệp là: %ld", ftell(fp));
Thiết lập vị trí hiện tại
Ngay sau khi mở luồng, vị trí của con trỏ hiện tại hoạt động được đặt thành không và trỏ vào byte đầu tiên của luồng. Như đã thấy trước đó, mỗi khi một ký tự được đọc từ hoặc viết vào luồng, con trỏ hiện tại hoạt động được di chuyển. Con trỏ có thể được đặt ở bất kỳ vị trí nào khác vị trí hiện tại tại bất kỳ thời điểm nào trong một chương trình. Hàm rewind() đặt vị trí con trỏ về đầu chương trình. Một hàm khác có thể được sử dụng để đặt vị trí con trỏ là fseek().
Hàm fseek() tái đặt vị trí của curp bằng số byte được chỉ định từ đầu, vị trí hiện tại hoặc cuối luồng tùy thuộc vào vị trí được chỉ định trong hàm fseek(). Nguyên mẫu của hàm fseek() như sau:
int fseek(FILE *fp, long int offset, int origin);
với offset là số byte vượt ra khỏi vị trí của tệp được xác định bởi origin.
Vị trí xuất phát cho biết vị trí khởi đầu của tìm kiếm và phải có giá trị là 0, 1 hoặc 2, biểu thị ba hằng số tượng trưng (được định nghĩa trong stdio.h) như trong bảng bên dưới:
Nguồn gốc | Vị trí tập tin |
SEEK_SET or 0 | Bắt đầu tập tin |
SEEK_CUR or 1 | Vị trí con trỏ tập tin hiện tại |
SEEK_END or 2 | Kết thúc tập tin |
Giá trị trả về bằng không có nghĩa là fseek() đã thành công và giá trị khác không có nghĩa là thất bại.
Mã lệnh sau tìm kiếm bản ghi thứ 6 trong tệp:
struct addr {
char name[40];
char street[40];
char city[40];
char state[3];
char pin[7];
};
FILE *fp;
.
.
.
fseek(fp, 5L * sizeof(struct addr), SEEK_SET);
Hàm sizeof() được sử dụng để tìm độ dài của mỗi bản ghi theo byte. Giá trị trả về được sử dụng để xác định số byte cần bỏ qua để bỏ qua 5 bản ghi đầu tiên.
‘fprintf() và fscanf()’
Ngoài các hàm I/O đã thảo luận, hệ thống I/O có bộ hàm fprintf() và fscanf(). Các hàm này tương tự như printf() và scanf(), nhưng chúng hoạt động với tệp.
Các hàm fprintf() và fscanf() có các nguyên mẫu như sau:
int fprintf(FILE *fp, const char *control_string, ...);
int fscanf(FILE *fp, const char *control_string, ...);
Trong đó, fp là con trỏ tệp được trả về bởi một cuộc gọi hàm fopen().
Các hàm fprintf() và fscanf() thực hiện các thao tác I/O của họ trên tệp mà con trỏ fp trỏ tới. Đoạn mã chương trình sau đọc một chuỗi và một số nguyên từ bàn phím, viết chúng vào một tệp đĩa, sau đó đọc thông tin và hiển thị nó trên màn hình:
printf("Nhập một chuỗi và một số: ");
fscanf(stdin, "%s %d", str, &sno); /* Đọc từ bàn phím */
fprintf(fp, "%s %d", str, sno); /* Ghi vào tệp */
fclose(fp);
fscanf(fp, "%s %d", str, &sno); /* Đọc từ tệp */
fprintf(stdout, "%s %d", str, sno); /* In trên màn hình */
Hãy nhớ rằng, mặc dù printf() và fscanf() thường là cách dễ dàng nhất để viết và đọc dữ liệu đa dạng vào và ra khỏi tệp, chúng không phải lúc nào cũng là phương pháp hiệu quả nhất. Lý do là mỗi cuộc gọi hàm mang theo chi phí nâng cao, vì dữ liệu được ghi dưới dạng dữ liệu ASCII định dạng (như nó sẽ xuất hiện trên màn hình) thay vì được ghi dưới dạng nhị phân. Do đó, nếu tốc độ hoặc kích thước tệp quan trọng, fwrite() và fread() là sự lựa chọn tốt hơn.
Bài tập
1. Viết chương trình nhập dữ liệu vào file và in theo thứ tự ngược lại.
2. Viết chương trình truyền dữ liệu từ file này sang file khác, không bao gồm các nguyên âm (a, e, i, 0, u) Loại trừ các nguyên âm ở cả chữ hoa và chữ thường. Hiển thị nội dung của tập tin mới.