Tổng quát về xử lý file - Lập trình C : Bài 17

Tổng quát về xử lý file - Lập trình C : Bài 17
access_time 10/16/2019 12:00:00 AM
person Đào Minh Giang

Tổng quát xử lý File - Lập trình C:  Bài 17

 

1. Giới thiệu

Một tệp dữ liệu (file) là một tập hợp dữ liệu, chúng thường được lưu trữ tại các thiết bị như Ổ đĩa, USB, Đĩa cứng hay trên các đám mây. Với các chương trình C từ bài đầu tiên đến nay, dữ liệu nhập vào trong một chương trình được nhập từ thông qua các bàn phím máy tính ...Tuy nhiên có nhiều phương pháp để nhập dữ liệu đầu vào cũng như xuất dữ liệu đầu cho chương trình đó là thông qua tệp dữ liệu. Chương trình C sẽ đọc dữ liệu từ tệp dữ liệu, sau khi phân tích, xử lý và tính toán với khối dữ liệu, thì chúng sẽ được lưu trữ trở lại tệp dữ liệu.

Vậy tệp dữ liệu là nơi lưu trữ thông tin dữ liệu đã được xử lý bởi chương trình. Tệp dữ liệu không chỉ là nơi lưu trữ dữ liệu mà xét theo khía cạnh mở rộng thì nó còn là nơi chứa chương trình thực thi. Ví dụ như các tệp có đuôi mở rộng là .exe đây là các tệp thực thi của một chương trình.

Để sử dụng tệp dữ liệu chúng ta sẽ tìm hiểu các vấn đề liên quan tới vào/ra (I/O) của tệp.. Cách thức ghi thông tin vào một file, các thức đọc thông tin từ một file. Có thể nhìn nhận các thao tác vào/ra một file cũng tương tự như các thao tác vào/ra trên một thiết bị đầu cuối, tuy nhiên điểm khác biệt cơ bản của thao tác vào/ra với file và thiết bị đầu cuối đó là chương trình cần phải xác định một file cụ thể bởi vì có nhiều file trong ổ cứng. Xác định chính xác một file với các thao tác với file như đọc file, ghi file hoặc với cả hai thao tác đọc/ghi file.

Một khái niệm quan trọng nữa liên quan đó là stream (Luồng dữ liệu). C cung cấp khai niệm luồng dữ liệu (stream) là một khái niệu trừu tượng hóa nằm giữa người lập trình vào thiết bị vào/ra. Các thiết bị vào/ra trong lập trình C đó là màn hình, máy in, bàn phím và tệp dữ liệu (file). Trong trường hợp này thiết bị vào/ra cụ thể là file và một luồng dữ liệu là một giao diện logic có liên quan tới file. Mặc dù File có nhiều kiểu mẫu khác nhau nhưng với luồng như liệu thì tất cả đều giống nhau. Luồng dữ liệu cung cấp tính giao diện nhất quán cho lập trình viên. Luồng dữ liệu vào/ra sử dụng vùng dữ liệu tạm để lưu trữ, vùng dữ liệu tạm gọi là buffer hay gọi là bộ nhớ đệm và được sử dụng để đọc và ghi file.

Xét một cách cụ thể, các thiết bị ngoại vi như ổ đĩa, băng từ và các thiết bị ngoại vi là hoàn toàn khác nhau. Song hệ thống file với bộ nhớ đệm sẽ chuyển đổi chúng thành một thiết bị logic được gọi là luồng dữ liệu (Stream) do vậy cùng một hàm ghi dữ liệu ra file trên đĩa thì có thể sử dụng nó ra các thiết bị khác như màn hình, máy in.

Hình minh họa mô hình vào/ra của luồng dữ liệu:


Hình số 1 : Mô hình vào/ra luồng dữ liệu

Mô hình trên là mô hình vào/ra hiệu quả. Khi tạo ra luồng dữ liệu liên kết với một file trên ổ đĩa, thì bộ nhớ đệm được tự động tạo ra và nó được gắn kết với luồng dữ liệu. Bộ nhớ đệm là một khối bộ nhớ tạm được sử dụng để lưu trữ thông tin phục vụ cho các thao tác đọc và ghi file. Vùng bộ nhớ tạm là cần thiết bởi vì đặc trưng của ổ đĩa là thiết bị có định hướng xử lý theo khối, điều này có nghĩa là ổ đĩa hoạt động hiệu quả nhất hiệu quả nhất khi dữ liệu được đọc và ghi vào khối có kích thước nhất định. Mỗi thiết bị phần cứng của các hãng khác nhau thì có kích cỡ khối dữ liệu là khác nhau, nó có thể có giá trị từ vài trăm đến vài nghìn bytes. Tuy nhiên thì không cần xác định chính xác kích cỡ của khối.

Bộ nhớ đệm liên kết với luồng dữ liệu file đóng vai trò là một giao tiếp giữa luồng dữ liệu với phần cứng ổ đĩa. Như chương trình sẽ ghi dữ liệu vào luồng, dữ liệu sẽ được nghi vào bộ nhớ đệm cho đến khi bộ nhớ đệm bị đầy và các thực thể nội dung của bộ nhớ đệm sẽ được nghi, như một khối, vào ổ đĩa. Tiến trình đọc dữ liệu được thực hiện tương tự như tiến ghi dữ liệu. Quá trình tạo và xử lý của bộ nhớ đệm được quản lý tự động bởi hệ điều hành, lập trình viên không cần quan tâm tới vấn đề này. C cung cấp một số hàm để làm việc với bộ nhớ đệm.

Trong thực tế, các xử lý của bộ nhớ đệm xảy ra trong suốt quá trình thực thi chương trình, đôi khi có thể dữ liệu mà chương trình ghi vào trong ổ đĩa vẫn còn trong bộ nhớ đệm. Nếu chương trình bị cheo vì một lý do nào đó, trong trường hợp này có thể dữ liệu chưa bị bị mất, nó có thể vẫn được lưu trong bộ nhớ đệm, người sử dụng có thể không biết vì dữ liệu đó không có trong ổ đĩa. Dữ liệu nằm trong bộ đệm cho đến khi dữ liệu trong bộ đệm được làm tươi hoặc là được ghi trong thành file trong ổ đĩa.

Luồng dữ liệu được liên kết với file trong quá trình thực hiện xử lý đọc file. Luồng dữ liệu ngắt gắn kết với file khi thực hiện thao tác đóng file. Vị trí hiện tại khi làm việc với file là vị trí xuất hiện tại thao tác tiếp theo khi thực hiện hành động đọc hoặc ghi file (Cái này sẽ được làm rõ ở các phần tiếp theo).

Có 2 loại luồng dữ liệu : Văn bản (Text) và nhị phân (binary).

Luồng dữ liệu văn bản là một dãy các kí tự (characters).

Một tệp văn bản có thể coi là một luồng dữ liệu các ký tự. Luồng dữ liệu văn bản gắn kết với các file văn bản. Luồng dữ liệu văn bản được tổ chức thành các dòng, mỗi dòng có thể có chứa một hoặc nhiều các kí tự và được đánh dấu bởi ký tự đặc để kết thúc dòng. Một dòng có Số lượng kí tự lớn nhất là 255 kí tự. Chú ý khái niệm dòng ở đây là khái niệm của file chứ không phải khai niệm string trong C. Khi một luồng dữ liệu kiểu văn bản được sử dụng, C sử dụng ký tự xuống dòng ‘\n’ và bất kỳ ký tự nào đánh dấu sự kết thúc dòng mà hệ điều hành hỗ trợ.

Trong Hệ điều hành Dos sử dụng tổ hợp (CR-LF) để sang dòng mới. Khi dữ liệu được ghi (write) vào file theo chế độ văn bản, mỗi ‘\n’ sẽ tự động chuyển đổi sang CR-LF. Ngược lại khi đọc (read) dữ liệu, mỗi khi gặp CR-LF nó sẽ tự động chuyển đổi sang ‘\n’. Trong UNIX, sự chuyển đổi không được thực hiện, Ký tự xuống dòng mới là không thay đổi.

Khi một file dữ liệu văn bản được sử dụng, có hai kiểu trình diễn thông tin cho dữ liệu đó là : internal (Nội bộ)external (mở rộng).  Ví dụ : Giá trị có kiểu int xét theo khía cạnh nội bộ (bên trong) của chính int thì kích cỡ lưu trữ của nó sẽ là 2 bytes hoặc 4 bytes. Nếu xét theo khía cạnh external (mở rộng), thì dữ liệu int có thể được lưu trữ và trình diễn dưới dạnh là một xâu ký tự hoặc có thể là dữ liệu kiểu hexadecimal.

Việc chuyển đổi dữ liệu giữa internal và external là rất dễ dàng. Ví dụ sử dụng printf để thực hiện các thao tác hiển thị thông tin và fprintf để ghi thông tin ra file. Thì printf là một ví dụ về internal và fprintf là đại diện cho external, việc chuyển đổi này được thực hiện thường xuyên. Trong thao tác đọc dữ liệu thì external scanf or fscanf là đại diện cho việc chuyển đổi giữa internal và external.

File nhị phân chứa một tập hợp các bytes. Trong C, một byte và một ký tự (charater) là tương đương. Khi làm việc với file nhị phân thì cũng sẽ tham chiếu tới luông dữ liệu nhị phân, so với luồng dữ liệu văn bản thì luồng dữ liệu nhị phân có 2 điểm khác:

Thứ nhất: Luồng dữ liệu nhị phân là dãy các bytes tương ứng 1-1 trên các thiết bị ghi/đọc. Điều đó có nghĩa là không có sự chuyển đổi. Số bytes được ghi và đọc bằng đúng số bytes nằm trên thiết bị ngoài. Không có sự phân tách giữa các dòng và không sử dụng ký tự đánh dấu sự kết thúc dòng. Các ký tự NULL và ký tự cuối dòng không có sự phân biệt với các ký tự khác.

Thứ hai: C không thiết lập cấu trúc trên file nhị phân, và việc đọc và ghi file theo bất kỳ cách nào sẽ được lập trình viên lựa chọn.

Trong C, xử lý một file bằng kỹ thuật truy cập ngẫu nhiên liên quan tới việc di chuyển vị trí hiện tại sang vị trí thích hợp trong file trước khi thực hiện các thao tác đọc/ghi dữ liệu. Điều này chỉ ra một đặc điểm thứ 2 của file nhị phân, file nhị phân có thể thực hiện các thao tác đọc/ghi đồng thời.

Ví dụ file cơ sở dữ liệu có thể được tạo và xử lý như một file nhị phân. Thao tác cập nhật một bản ghi (record) liên quan tới việc định vị vị trí của record, đọc record vào trong bộ nhớ, chỉnh sửa thông tin record, cuối cùng là ghi trở lại record đó vào file trên ổ đĩa. Các thao tác này thường xảy ra trên file nhị phân, nhưng hiếm khi tìm thấy trong các ứng dụng xử lý file văn bản.

2. Sử dụng file trong C

Có 4 hành động cơ bản nhất đối với file. Các hành động đó bao gồm:

+ Khai báo một biến con trỏ có kiểu FILE

+ Mở file thông qua hàm fopen để ghi hoặc đọc

+ Ghi hoặc đọc file (Xử lý file) thông qua một hàm nào đó

+ Sử dụng hàm fclose để đóng file.

2.1 Khai báo con trỏ kiểu FILE

Bởi vì một số lượng khác nhau các file có thể được sử dụng trong chương trình. Khi đọc và ghi một loại file nào đó thì cần phải xác định.

Điều này được thực hiện thông qua biến con trỏ trỏ đến cấu trúc file. FILE là một cấu trúc được khai báo trong stdio.h . Các thành phần trong cấu trúc FILE được chương trình sử dụng trong các hoạt động truy cập khác nhau. Tuy nhiên lập trình viên không nhất thiết phải quan tâm đến vấn đề đó. Tuy nhiên trước khi mỗi một file được mở, thì cần khai báo một biến con trỏ có kiểu FILE.

Khi hàm fopen() được gọi, hàm sẽ tạo ra một thể hiện của cấu trúc file và tạo ra một con trỏ trỏ đến cấu trúc file đó. Con trỏ này được sử dụng trong tất cả các hoạt động tiếp theo trên file. Cú pháp khai báo của con trỏ có kiểu FILE như sau :

Cú pháp
FILE *fi le_pointer_name,…;

Trog đó :

Ví dụ

FILE *ifp;

FILE *ofp;

Khi đó con trỏ ifp và ofp là 2 con trỏ có kiểu file.

le_pointer_name: Tên con trỏ có kiểu FILE.

2.2. Mở file

Để mở một file thì chúng ta sử dụng hàm fopen. Cú pháp hàm như sau :

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

Trong đó :

fname : Tên file trên đĩa.

mode: Kiểu xử lý file.

Ví dụ:

FILE *fptr = fopen(“example.txt”, “r”);

Mở tệp để thực hiện đọc dữ liệu

Với thao tác mở tệp để thực hiện ghi dữ liệu, chúng ta thay “r” thành “w”.

Nguyên mẫu hàm fopen trong stdio.h. trong đó cũng bao gồm các hàm xử lý file cần thiết khác. Tên của file có kiểu xâu ký tự do vậy *fname sẽ là một hằng con trỏ có kiểu char. Tên của file cần phải xác định chính xác.

Các file trong ổ đĩa bao giờ cũng có tên. Tên file được đặt theo quy định của hệ điều hành. Trong DOS, tên file có độ dài từ 1 đến 8 ký tự. Với hệ điều hành Windows và Unix thì tên file có độ dài lên đến 256 ký tự.

Tên file trong C có thể có chứa đường dẫn đầy đủ của file.

Ví dụ:

FILE *fptr = fopen(“c:\\examdata\\example.txt”, “r”);

Kiểu xử lý file : Là một trong các xâu ký tự có trong danh sách sau. Nó quy định cách xử lý đối với các file đã mở

Cho file văn bản


Bảng số 1 : Kiểu xử lý file văn bản

Cho tệp nhị phân


Bảng số 2 : Kiểu xử lý file nhị phân

Ví dụ:

char filename[80];

FILE *fp;

printf(“Nhập thông tin tên file !”);

gets(filename);

fp = fopen(filename,“w”);

2.3. Kiểm tra kết quả hàm fopen

Hàm fopen trả về một con trỏ kiểu FILE *, con trỏ trỏ đến cấu trúc FILE và được sử dụng để truy cập vào file. Khi file không thể mở vì một lý do nào đấy hàm fopen sẽ trả về NULL. Các lý do có thể bao gồm :

+ Tên file bị sai

+ Cố gắng mở file trên ổ đĩa, tuy nhiên file đó không thể đọc được.

+ Cố gắng mở file trên thư mục không tồn tại trên ổ đĩa hoặc ổ đĩa không tồn tại.

+ File đó không cho phép xử lý ở chế độ ghi hoặc ở chế độ đọc ...

Ví dụ

FILE *fp = fopen(“data.dat”,“r”);

If(fp == NULL){

printf(“Không mở được data.dat !”);

exit(1);

}

Sử dụng cơ chế kiểm tra hàm fopen với giá trị NULL, cách này không phải là cách tìm ra chính xác nguyên nhân tại sao lỗi xuất hiện. Nhưng có thể hiển thị thông báo lỗi cho người dùng để thực hiện cố gắng mở file lần nữa hoặc kết thúc quá trình mở file.

2.4. Đóng file và giải phóng bộ nhớ đệm

Sau khi hoàn thành quá trình xử lý với file, sử dụng hàm fclose() để thực hiện đóng file.

Cú pháp:

int fclose(FILE *fp);

Giá trị trả về :

0 : Nếu đóng file thành công

-1: Nếu đóng file bị lỗi

Khi chương trình chạy, nếu hàm main() kết thúc hoặc khi gặp hàm exit() thì toàn bộ các luồn dữ liệu sẽ tự động giải phóng và đóng. Thực tế trong một chương trình đơn giản không nhất thiết phải đóng file, bởi vì hệ thống sẽ tự đóng toàn bộ trước khi kết thúc chương trình trước khi trở về hệ điều hành. Tuy nhiên với lập trình viên, chúng ta tạo thói quen tốt thực hiện đóng file khi kết thúc các tiến trình xử lý với file.

Khi một file được đóng, bộ nhớ đệm gắn với file sẽ được giải phóng (làm tươi), Tất cả các luồn thao tác sẽ được đóng khi sử dụng hàm fcloseall(). Hàm này có nguyên mẫu là fcloseall(void).

Hàm fcloseall sẽ giải phóng vùng bộ nhớ đếm (buffer) gắn với luồng thông tin (stream) và kết quả trả về số các luồng thông tin bị đóng.

Bộ nhớ đệm gắn kết với luồng dữ liệu có thể được giải phóng mà không cần phải đóng nó, bằng cách sử dụng hàm fflush() hoặc hàm fushall(). Sử dụng hàm fflush() khi bộ nhớ đệm gắn với file là đã được ghi vào file trên ổ đĩa, trong khi file đó đang dùng. Sử dụng hàm fushall() để giải phóng bộ nhớ đệm cho tất cả các luồng thông tin.

Cú pháp

int fflush(FILE *fp);

int flushall(void);

Mô tả hàm fflush

Tham số hình thức *fp là con trỏ kiểu FILE, được trả về bởi hàm fopen khi file đó đã được mở. Nếu một file được mở ở chế độ xử lý ghi thông tin (writing), thì fflush sẽ tiến hành ghi thông tin từ bộ nhớ đệm vào ổ đĩa. Nếu file được mở ở chế độ xử lý đọc thông tin (reading) thì khi đó bộ nhớ đệm sẽ bị xóa.

Hàm fflush trả về 0 nếu thành công, trả về EOF nếu bị lỗi.  

Mô tả hàm flushall

Hàm flushall trả về số lượng các luồng dữ liệu đang mở và tiến hành giải phóng bộ nhớ đệm cho tất cả các luồng dữ liệu đang mở.

Vậy một chương trình xử lý file có dạng chung như sau:

#include stdio.h

int main(){

          FILE *fp = fopen(“tên file”,“kiểu xử lý file”);

If(fp == NULL){

printf(“Không mở được data.dat !”);

}

Else{

          // Xử lý ghi hoặc đọc dữ liệu gắn với file

          ...

                    fflush(fp);

                    fclose(fp);

}

}

 

 

 

 

 

 

 

 

 

 

 

 


vertical_align_top
share
Chat...