[Lập trình C] Xử lý file - file handling

Posted on January 11th, 2021

Trong các bài viết trước, bạn đã quen với việc nhập dữ liệu từ bàn phím và xuất dữ liệu ra màn hình. Bài viết này giới thiệu với bạn một loại I/O khác trong lập trình, đó chính là file.

Kiến thức cơ bản về xử lý file trong lập trình C

Cơ bản về file input/output

Mọi hoạt động nhập/xuất dữ liệu trong ngôn ngữ lập trình C đều sử dụng hàm trong thư viện chuẩn stdio.h. Trong đó, có việc nhập và xuất dữ liệu với file.

Dữ liệu trao đổi có 2 loại là:

  • Dạng binary
  • Dạng text (con người có thể đọc được)

Cơ bản về stream

Hệ thống file trong C có thể được sử dụng ở nhiều loại device khác nhau như máy in, ổ đĩa, terminal,... Mặc dù, các loại device này là khác nhau nhưng hệ thống file buffer để truyền dữ liệu đến những device này là giống nhau, và gọi chung là một loại stream.

Tương ứng với 2 loại dữ liệu thì sẽ có 2 loại stream là: binary stream và text stream.

Text stream

Text stream là chuỗi các kí tự, có thể tổ chức thành dòng và kết thúc bởi kí tự dòng mới \n.

Đối với text stream, việc phiên dịch các kí tự có thể được xảy ra tùy thuộc vào môi trường.

Vì vậy, sẽ không có mối quan hệ giữa một - một giữa các kí tự được viết trên file với trên các thiết bị. Do đó, số lượng kí tự cũng không giống nhau giữa file và các thiết bị.

Binary stream

Binary stream là chuỗi các bytes với mối quan hệ một - một giữa file và trên các thiết bị mà không có sự phiên dịch. Vì vậy, số lượng bytes trên file và trên các thiết bị là giống nhau.

Binary stream không có cờ để kết thúc một file hay kết thúc một bản ghi. Kết thúc của file được xác định bởi kích thước của file.

File trong lập trình C

Một file có thể liên kết tới bất kỳ thiết bị nào như ổ đĩa, máy in hay terminal...

Để liên kết file với một stream thì dùng hàm open(). Ngược lại, để ngắt liên kết file với một stream thì dùng hàm close().

Khi một chương trình được tắt bình thường, tất cả các files đều bị đóng. Tuy nhiên, khi chương trình bị lỗi (crash) thì file vẫn sẽ mở. Điều này dẫn đến việc đọc ghi file bởi chương trình khác sẽ không diễn ra bình thường.

Một số hàm cơ bản về file

  • fopen(): mở một file
  • fclose(): đóng một file
  • fputc(): ghi 1 kí tự ra file
  • fgetc(): đọc 1 kí tự từ file
  • fread(): đọc từ file ra một buffer
  • fwrite(): ghi một buffer ra file
  • fseek(): di chuyển đến một vị trí trên file
  • fprintf(): tương tự như printf(), nhưng trên một file
  • fscanf(): tương tự như scanf(), nhưng trên một file
  • feof(): trả về true nếu gặp kết thúc file (EOF)
  • rewind(): reset vị trí về đầu file
  • remove(): xóa file
  • fflush(): ghi dữ liệu từ 1 buffer ra một file xác định trước

Con trỏ file

Con trỏ file là một con trỏ cần thiết để thực hiện việc đọc và ghi file.

Nó là một con trỏ, trỏ vào một struct File bao gồm: tên file, vị trí hiện tại trên file, file đang được đọc hay ghi, có lỗi gì xảy ra với việc đọc file hay không.

Để khai báo một con trỏ file, bạn dùng cú pháp:

FILE* fp

Mở file

Để mở file, bạn dùng hàm fopen() với cú pháp:

FILE* fopen(const char *filename, const char  *mode);

Trong đó:

  • Hàm trả về một con trỏ file
  • filename: là tên file
  • mode: là chế độ mở file

Các chế độ mở file là:

  • r: mở file để đọc
  • w: mở file để ghi
  • a: mở file để viết thêm vào file
  • r+: mở file để đọc và ghi
  • w+: tạo file mới để đọc và ghi
  • a+f: viết thêm vào file hoặc tạo thêm file mới để đọc và ghi

Ví dụ để mở một file tên input.txt với quyền ghi thì bạn làm như sau:

FILE* fp = fopen("input.txt", "w");

Đóng file

Đóng file là việc cần thiết sau khi file đã được sử dụng xong. Việc đóng file giúp giải phóng tài nguyên và giảm nguy cơ bị lỗi do file đang mở.

Đóng file sẽ đóng stream, giúp giải phóng toàn bộ dữ liệu buffer được liên kết.

Để đóng file - đã mở bởi hàm fopen(), bạn dùng hàm fclose(), với cú pháp:

int  fclose(FILE *fp);

Ví dụ:

// Mo file
FILE* fp = fopen("input.txt", "w");

// Dong file da mo
fclose(fp);

Ngoài ra, để đóng hết tất cả các stream đã open, bạn có thể dùng hàm fcloseall().

Ghi một kí tự ra file

Để ghi một kí tự ra file, bạn dùng hàm fputc() với cú pháp:

int fputc(int ch, FILE *fp);

Ví dụ:

// Mo file voi quyen ghi
FILE* fp = fopen("input.txt", "w");

// Ghi mot ki tu 'a' ra file
fputc('a', fp);

Đọc một kí tự từ file

Để đọc một kí tự từ file (đã mở với quyền đọc), bạn dùng hàm fgetc() với cú pháp:

int fgetc(FILE *fp);

Ví dụ:

// Mo file voi quyen doc
FILE* fp = fopen("input.txt", "r");

// Doc mot ki tu tu file
int ch = fgetc(fp);

printf("ki tu da doc: %c", ch); // ki tu da doc: 'a'

Ghi một chuỗi kí tự ra file

Để ghi một chuỗi kí tự ra file, bạn dùng hàm fputs() với cú pháp:

int fputs(const char *str, FILE *fp);

Ví dụ:

// Mo file voi quyen ghi
FILE* fp = fopen("input.txt", "w");

// Ghi mot ki tu "hello" ra file
fputs("hello", fp);

Đọc một chuỗi kí tự từ file

Để đọc một chuỗi kí tự từ file, bạn dùng hàm fgets() với cú pháp:

char* fgets(char *str, int length, FILE *fp);

Hàm này đọc một chuỗi string từ file cho đến khi gặp kí tự dòng mới \n hoặc length-1 kí tự đã được đọc.

Trong đó:

  • Hàm trả về con trỏ, con trỏ này có giá trị NULL khi không đọc được kí tự nào mà đã gặp EOF
  • str là mảng chứa kết quả đọc được
  • length là số lượng muốn đọc bao gồm kí tự kết thúc xâu '\0'. Nghĩa là nếu bạn muốn đọc N kí tự thì phải truyền vào giá trị length = N + 1
  • fp là con trỏ file

Ví dụ đọc 2 kí tự từ file đã được ghi chữ hello ở ví dụ trên.

// Mo file voi quyen doc
FILE* fp = fopen("input.txt", "r");

// Doc mot chuoi ki tu file
char ketQua[3];
char* p = fgets(ketQua, 3, fp);
if (p != NULL) {
	printf("ketqua=%s", ketQua); // ketqua=he
}

Hàm fprintf() và hàm fscanf()

Hai hàm fprintf() và fscanf() tương tự như hàm printf() và scanf() mà đối với file.

Cú pháp của 2 hàm này là:

int fprintf(FILE * fp, const char *control_string,..);
int fscanf(FILE *fp, const char *control_string,...);

Ví dụ in một đoạn text ra file:

// Mo file voi quyen ghi
FILE* fp = fopen("input.txt", "w");

// Ghi mot chuoi ki tu ra file, co truyen tham so
fprintf(fp, "hello %d", 2021); 

// noi dung trong file la: hello 2021

Ví dụ đọc một đoạn text từ file:

// Mo file voi quyen doc
FILE* fp = fopen("input.txt", "r");

// Doc 1 chuoi ki tu tu file da ghi ben tren
char str[10];
fscanf(fp, "%s", &str);

// kiem tra thu mang str
printf("mang str=%s", str); // mang str=hello

Hàm feof()

Hàm feof() trả về true nếu như gặp kết thúc file, ngược lại sẽ trả về false(0).

Cú pháp:

int feof(FILE *fp);

Hàm rewind()

Hàm rewind() reset vị trí hiện tại về đầu file.

Cú pháp:

rewind(fp);

Hàm ferror()

Hàm ferror() dùng để xác định xem có lỗi gì xảy ra khi tương tác với file hay không.

Vì vậy, hàm này nên được gọi sau mỗi lần có tương tác với file.

Cú pháp:

int ferror(FILE *fp);

Hàm remove()

Hàm remove() dùng để xóa file, với cú pháp:

int remove(char *filename);

Trong đó: filename là tên file cần xóa.

Chú ý: để xóa được file, file đó phải được đóng (không được mở bởi chương trình khác). Nếu bạn đã mở file bằng hàm fopen() thì phải gọi hàm fclose() để đóng file trước khi xóa.

Flushing stream

Để flushing stream, bạn dùng hàm fflush() với cú pháp:

int fflush(FILE *fp);

Chú ý:

  • Nếu file đang được mở để đọc dữ liệu từ buffer thì toàn bộ buffer sẽ bị xóa.
  • Nếu file đang được mở để ghi dữ liệu vào từ buffer thì toàn bộ nội dung trên buffer sẽ ngay lập tức được ghi lên file.
  • Hàm fflush() với con trỏ NULL sẽ flush toàn bộ buffer với file cho việc ghi dữ liệu.

Một số stream cơ bản

Trong lập trình C, có 5 loại stream cơ bản là:

  • Standard input: stdin
  • Standard output: stdout
  • Standard error: stderr
  • Standard printer: stdprn
  • Standard auxiliary: stdaux

Current active pointer

Current active pointer (CURP) là một con trỏ trong cấu trúc FILE dùng để xác định vị trí hiện tại ở file mà các hàm I/O sẽ thực hiện.

Bất cứ khi nào một kí tự được đọc hoặc ghi vào stream, CURP sẽ tăng.

Để xác định vị trí của CURP, bạn có thể dùng hàm ftell() với cú pháp:

long int ftell(FILE *fp);

Để di chuyển CURP, bạn có thể dùng hàm fseek() với cú pháp:

int fseek (FILE *fp, long int offset, int origin);

Trong đó:

  • fp là con trỏ file
  • offset là số lượng bytes dịch chuyển
  • origin là biến dùng để xác định mốc dịch chuyển

Các mốc dịch chuyển:

  • SEEK_SET (0): tính từ đầu file
  • SEEK_CUR (1): tính từ vị trí hiện tại
  • SEEK_END (2): tính từ cuối file

Mở file dạng binary

Để mở file dạng binary, bạn cũng dùng hàm fopen() tương tự như với file text, cú pháp:

FILE *fopen(const char *filename, const char  *mode);

Chỉ khác ở các mode sử dụng:

  • rb: Mở file dạng binary để đọc
  • wb: Mở file dạng binary để ghi
  • ab: Mở file dạng binary để append thêm dữ liệu
  • r+b: Mở file dạng binary để đọc và ghi
  • w+b: Tạo file dạng binary để đọc và ghi
  • a+b: Đọc hoặc ghi thêm vào file

Đóng file binary

Để đóng file dạng binary, bạn cũng có thể dùng hàm fclose() với cú pháp:

int  fclose(FILE *fp); 

Đọc bytes từ file

Để đọc bytes từ file, bạn dùng hàm fread() như sau:

size_t fread(void *buffer, size_t num_bytes, size_t count, FILE *fp);

Trong đó:

  • buffer: là một con trỏ đến buffer có kích thước ít nhất là bằng num_bytes*count
  • num_bytes: là số lượng byte của một phần tử
  • count: là số lượng phần tử
  • fp: là con trỏ file

Ghi bytes ra file

Để ghi bytes ra file, bạn dùng hàm fwrite() như sau:

size_t fwrite(const void *buffer, size_t num_bytes, size_t count, FILE *fp);

Trong đó:

  • buffer: là một con trỏ đến buffer có kích thước ít nhất là bằng num_bytes*count
  • num_bytes: là số lượng byte của một phần tử
  • count: là số lượng phần tử
  • fp: là con trỏ file

Bài tập thực hành về xử lý file trong lập trình C

Bài 1

Cho file input.txt với định dạng như sau:

N
number1
number2
number3
.
.
.
numberN

Trong đó: N là 1 số tự nhiên (N <= 50). Theo sau là N số nguyên: number1, number2,..., numberN.

Hãy đọc file trên rồi thực hiện các yêu cầu sau:

  • Hãy tính và in ra tổng các số number1, number2,..., numberN
  • Hãy tính và in ra trung bình cộng của các số lẻ
  • Sắp xếp các số theo thứ tự tăng dần rồi ghi lại vào file theo đúng định dạng ban đầu
#include <stdio.h>

int main() {
	FILE *fp = fopen("input.txt", "r");
	
	int N, i, j, a[50];
	
	// Doc N
	fscanf(fp, "%d", &N);
	
	// Doc N so tiep theo
	for(i = 0; i < N; i++) {
		fscanf(fp, "%d", &a[i]);
	}	
	
	// 1. Tinh va in ra tong cac so
	long long tong;
	for(i = 0; i < N; i++) {
		tong += a[i];
	}
	printf("Tong cac so la: %lld\n", tong);
	
	// 2. Tinh va in ra trung binh cong cac so le
	long long tongLe = 0;
	int demLe = 0;
	for(i = 0; i < N; i++) {
		if (a[i] % 2 != 0) {
			tongLe += a[i];
			demLe += 1;
		}
	}
	printf("Trung binh cong cac so le la: %g", (float)tongLe/demLe);
	
	// 3. Sap xep va in lai ra file
	for(i = 0; i < N - 1; i++) {
		for (j = i + 1; j < N; j++) {
			if (a[i] > a[j]) {
				int temp = a[i];
				a[i] = a[j];
				a[j] = temp;
			}
		}
	}
	
	// Open file with "w" mode to write data to the file
	fp = fopen("input.txt", "w");
	fprintf(fp, "%d\n", N);
	for(i = 0; i < N; i++) {
		fprintf(fp, "%d\n", a[i]);
	}
	
  fclose(fp);
	return 0;
}

Bài 2

Định nghĩa struct Book bao gồm các thông tin:

  • Tên: char ten[50]
  • Giá tiền: int giaTien
  • Năm xuất bản: int namXuatBan

Hãy thực hiện các yêu cầu sau:

  • Nhập vào số tự nhiên N (N <= 10) là số sách muốn nhập
  • Nhập vào thông tin chi tiết của N cuốn sách
  • Sắp xếp và in ra danh sách các cuốn sách theo thứ tự giảm dần của giá tiền
  • Tạo file book.dat, rồi lưu tất cả thông tin của từng cuốn sách vào file
#include <stdio.h>

int main() {
	typedef struct {
		char ten[50];
		int giaTien;
		int namXuatBan;
	} Book;

	int N, i, j;
	Book bookList[10];

	printf("Nhap N: ");
	scanf("%d", &N);

	printf("\nNhap thong tin N cuon sach:\n");
	for(i = 0; i < N; i++) {
		printf("\n+ Nhap cuon sach %d:\n", i + 1);
		printf("++ ten: ");
		scanf("%s", bookList[i].ten);
		printf("++ gia tien: ");
		scanf("%d", &bookList[i].giaTien);
		printf("++ nam xuat ban: ");
		scanf("%d", &bookList[i].namXuatBan);
	}

	// Sap xep theo tang dan gia tien
	for(i = 0; i < N - 1; i++) {
		for (j = i + 1; j < N; j++) {
			if (bookList[i].giaTien < bookList[j].giaTien) {
				Book temp = bookList[i];
				bookList[i] = bookList[j];
				bookList[j] = temp;
			}
		}
	}

	printf("\nDanh sach cuon sach sau khi sap xep la: \n");
	for(i = 0; i < N; i++) {
		printf("++ ten: %s\n", bookList[i].ten);
		printf("++ gia tien: %d\n", bookList[i].giaTien);
		printf("++ nam xuat ban: %d\n\n", bookList[i].namXuatBan);
	}

	// In cac cuon sach ra file
	FILE *fp = fopen("book.dat", "w");
	for(i = 0; i < N; i++) {
		fwrite(&bookList[i], sizeof(Book), 1, fp);
	}
	fclose(fp);

	return 0;
}

Bài 3

Đọc file book.dat từ bài 2 rồi in ra danh sách chi tiết các cuốn sách đã được ghi vào từ bài 2.

#include <stdio.h>

int main() {
	typedef struct {
		char ten[50];
		int giaTien;
		int namXuatBan;
	} Book;

	// Doc thong tin cac cuon sach ra file
	int count = 0;
	Book bookList[10];
	FILE *fp = fopen("book.dat", "r");
	while(fread(&bookList[count], sizeof(Book), 1, fp)) {
		count++;
	}
	fclose(fp);
	
	printf("Danh sach %d cuon sach la:\n", count);
	int i;
	for(i = 0; i < count; i++) {
		printf("++ ten: %s\n", bookList[i].ten);
		printf("++ gia tien: %d\n", bookList[i].giaTien);
		printf("++ nam xuat ban: %d\n\n", bookList[i].namXuatBan);
	}

	return 0;
}