Đọc, ghi file trong C (Nâng cao)

Tài liệu slide phần đọc ghi file:

https://www.dropbox.com/s/kfu8onyip6k8358/Phan3_LapTrinhC_Chuong8_File.pdf?dl=0

  1. Lưu ý: Do trong slide đã mô tả đầy đủ và có ví dụ minh họa rõ ràng, nên mình sẽ mô tả vắn tắt các hàm thông dụng, và tập trung giải thích một số ví dụ điển hình. Mô tả các hàm các bạn có thể tham khảo ở http://www.cplusplus.com/, mình đã gửi link kèm theo ở dưới.
  2. Tổng quan về các hàm sử dụng trên file: Slide từ trang 14 – 15.

 Các hàm thao tác với file

I. Một số vấn đề chung

Muốn thay đổi các dữ liệu đã lưu trên file, có 2 cách tiếp cận:

  1. Thay đổi trực tiếp trên file.
  2. Đẩy dữ liệu từ file lên bộ nhớ và xử lý trên đấy, kết thúc phiên làm việc đẩy dữ liệu từ bộ nhớ xuống file. Trong trường hợp dữ liệu trong file quá lớn có thể dùng cách xử lý từng phần.
  • Cách thứ 2 có vẻ ưu điểm hơn do bộ nhớ trong nhanh hơn bộ nhớ ngoài rất nhiều, do đó thao tác trên bộ nhớ trong sẽ nhanh hơn so với thao tác ở bộ nhớ ngoài.
  • Cách thứ 1 có thể áp dụng khi dữ liệu mà ta cần thêm ở cuối file.

 – Làm thế nào để đọc file được dễ dàng nhất

        Dữ liệu ghi trên file cần có cấu trúc. File text thì giữa các phần tử cần có dấu space hoặc ‘\n’. File binary thì cần tổ chức theo array (int array, struct array, …). Tại sao cần làm vậy?

         File text, các phần tử ghi cách nhau bởi dấu space hoặc ‘\n’, sẽ tiết kiệm bộ nhớ hơn và con người sẽ đọc trực tiếp được, nhưng sẽ làm giảm tốc độ duyệt và khó tính toán vị trí.

         File binary, tốn bộ nhớ hơn, ví dụ 1 xâu 32 phần tử nhưng chúng ta chỉ lưu vào đó xâu “hello”, con người không đọc trực tiếp được, bù lại tốc độ đọc nhanh hơn, dễ tính toán vị trí do tổ chức thành các khối có độ dài bằng nhau.

II. Các loại file (Slide trang 6 – 7)

Có 2 loại file là: file văn bản và file nhị phân.

Chú ý đến sự khác nhau giữa 2 loại file này

  1. Ở cách xử lý 2 ký tự 10 (LF) (Line Feed – Xuống dòng) và 13 (CR) (Carriage Return – Về đầu dòng) (Ở ubuntu và windows sẽ có các cách ứng xử khác nhau).
    Các bạn có thể tham khảo thêm ở đây:

    http://discuss.eggclub.org/t/bai-t-p-tu-n-2-ph-n-2/169/10

  2. Ký tự 26 (End of file) (Do windows quy định và chỉ có tác dụng trên kiểu văn bản, trong môi trường console thì nó là tổ hợp phím Ctrl + Z).

III. Các hàm mở và đóng file (Slide trang 16 – 20)

fopen dùng để mở file. http://www.cplusplus.com/reference/cstdio/fopen/?kw=fopen
fclose dùng để đóng file. http://www.cplusplus.com/reference/cstdio/fclose/?kw=fclose

IV. Các hàm đọc, ghi file (Slide trang 30 – 50)

Chú ý: Trong các kiểu đọc/ghi, cần làm sạch vùng đệm trước khi chuyển từ đọc sang ghi hoặc ngược lại. Dùng các hàm fflush và di chuyển đầu từ.

putc và fputc (2 hàm này tương đương) http://www.cplusplus.com/reference/cstdio/fputc/?kw=fputc

  • Hàm ghi lên file fp một ký tự có mã bằng: m = ch%256, trong đó ch được xem là số nguyên không dấu. Nếu thành công hàm cho mã ký tự được ghi, trái lại hàm cho EOF
  • Trong kiểu văn bản, nếu m =10 thì hàm sẽ ghi lên file hai mã 13 và 10

getc và fgetc (2 hàm này tương đương) http://www.cplusplus.com/reference/cstdio/fgetc/?kw=fgetc

  • Hàm đọc 1 ký tự từ file fp. Nếu thành công hàm cho mã đọc được (có giá trị từ 0 đến 255). Nếu gặp cuối file hay có lỗi hàm cho EOF
  • Trong kiểu văn bản, hàm đọc một lượt cả hai mã 13, 10 và trả về giá trị 10; khi gặp mã 26 thì hàm không trả về 26 mà trả về EOF

Hàm fprintf: ghi dữ liệu theo khuôn dạng. http://www.cplusplus.com/reference/cstdio/fprintf/?kw=fprintf

Hàm fscanf: đọc dữ liệu từ file theo khuôn dạng. http://www.cplusplus.com/reference/cstdio/fscanf/?kw=fscanf

Hàm fputs: ghi một chuỗi ký tự lên file http://www.cplusplus.com/reference/cstdio/fputs/?kw=fputs

  • Ghi chuỗi s lên file f (dấu ‘\0’ không ghi lên file).

Hàm fgets: đọc một dãy ký tự từ file. http://www.cplusplus.com/reference/cstdio/fgets/?kw=fgets
Kết thúc khi

  • hoặc đã đọc n-1 ký tự
  • hoặc gặp dấu xuống dòng (cặp mã 13 10). Khi đó mã 10 được đưa vào xâu kết quả (xâu kết quả bao gồm cả ký tự ‘\n’, ta phải thực hiện thao tác xóa ký tự ‘\n’ đi).
  • hoặc kết thúc file.

Xâu kết quả sẽ được bổ sung thêm dấu hiệu kết thúc chuỗi ‘\0’. Khi thành công hàm trả về địa chỉ vùng nhận kết quả; khi có lỗi hoặc gặp cuối file, hàm cho giá trị NULL.


Kết luận: Ta hay dùng fgetc để đọc toàn bộ file hoặc phân tích cấu trúc file đó. fputc để ghi 1 ký tự không hiển thị lên file, fprintf ghi dữ liệu theo khuôn dạng lên file, fscanf để đọc dữ liệu mà cách nhau bởi khoảng trống (int, float, char) (không nên dùng fscanf để đọc xâu có dấu cách), fgets để đọc 1 xâu ký tự có dấu cách trên file.


Hàm fread, fwrite để ghi dữ liệu dạng nhị phân lên file, thích hợp với kiểu dữ liệu struct

http://www.cplusplus.com/reference/cstdio/fread/?kw=fread
http://www.cplusplus.com/reference/cstdio/fwrite/?kw=fwrite

Các bạn có thể tham khảo đọc ghi 1 struct ở đây:

 http://vncoding.net/2015/11/14/ham-fread-doc-du-lieu-tu-file-binary/

V. Nhập xuất ngẫu nhiên và các hàm di chuyển con trỏ chỉ vị (Slide trang 63 – 68)

–  Hàm rewind: chuyển con trỏ chỉ vị về đầu file http://www.cplusplus.com/reference/cstdio/rewind/?kw=rewind
Hàm fseek: di chuyển con trỏ chỉ vị đến vị trí mong muốn http://www.cplusplus.com/reference/cstdio/fseek/?kw=fseek

  • Chú ý: Không nên dùng fseek trên kiểu văn bản.

Hàm ftell: cho biết vị trí hiện tại của con trỏ chỉ vị http://www.cplusplus.com/reference/cstdio/ftell/?kw=ftell

Phân tích một số ví dụ về lưu ý khi đọc file

I.  Hàm feof ()

       Hàm này kiểm tra xem trong khối dữ liệu được đọc vào ở lần thực hiện gần nhất có phần tử EOF không (tức là phần tử EOF có được đọc trong lần đọc gần đây nhất hay không), nếu có thì hàm feof () trả về giá trị khác không (ở bài dưới ta sẽ coi nó bằng 1 để viết cho tiện), còn nếu chưa thì trả về giá trị 0.

http://www.cplusplus.com/reference/cstdio/feof/?kw=feof

II. Giá trị của 1 byte

Trong slide ở trang 3 và ở nhiều trang mạng có ghi “_Một tệp tin đơn giản chỉ là một dãy các byte (mỗi byte có giá trị từ 0 đến 255) ghi trên đĩa._”
Ý câu trên muốn nói 1 ký tự lưu trên đĩa sẽ có dạng nhị phân từ 0000 0000 đến 1111 1111.

  • Nếu ta đọc ký tự đó vào 1 biến không dấu (unsigned char) thì nó sẽ có giá trị từ 0 – 255.
  • Nếu ta đọc ký tự đó vào 1 biến có dấu (char) (Cái này tùy từng máy quy định là char có dấu hay không dấu, ở đây ta tạm hiểu char là có dấu) thì nó sẽ có giá trị từ -127 — 128 .

III.  Sự khác nhau giữa fscanf (f, "%c", &c)fscanf (f, "%d", &temp)

1. Hàm fscanf (f, "%c", &c)

Để đọc hết nội dung file, hàm này tương đương với:

 c = fgetc (f);

Hai hàm này chỉ khác ở chỗ fscanf (f, "%c", &c) không đọc phần tử EOF vào biến c, còn c = fgetc (f); sẽ cho ta giá trị là -1 nếu c kiểu char, giá trị 255 nếu c kiểu unsigned char.

Để đọc, ta dùng khối lệnh sau:
Khối lệnh 1:

Tại sao ta lại không dùng khối lệnh này:
Khối lệnh 2:

Bây giờ ta bắt đầu phân tích:

  1. Hàm fscanf (f, "%c", &c); đọc từ file 1 ký tự.
    Bao gồm cả các ký tự trống: dấu cách, xuống dòng… -> Đây là điểm khác biệt căn bản giữa fscanf (f, "%c", &c)fscanf (f, "%d", &temp) (Hàm này coi ký tự trống, EOF là kết thúc nhập và không đọc vào)
  2. Sau khi hàm fscanf (f, "%c", &c) đọc xong ký tự cuối cùng và ký tự cuối cùng được gán vào biến c, khi đó “con trỏ chỉ vị” được dịch chuyển trỏ đến vị trí đầu tiên của địa chỉ phần tử EOF. Lúc này thao tác đọc chưa diễn ra, do đó feof (f) = 0.

images

  • Ở khối lệnh 1, do feof (f) = 0 nên lệnh printf ("%c", c); được thực hiện, kết quả ký tự cuối cùng sẽ được in ra. Tiếp tục lệnh fscanf (f, "%c", &c); được gọi lần nữa, nhưng do lúc này phần tử đọc được là EOF nên hàm fscanf sẽ bỏ qua dẫn đến biến c giữ nguyên giá trị. Lúc này EOF đã được đọc do đó feof (f) = 1, lệnh break được thực hiện (giá trị của lúc này c sẽ không được in ra).
  • Ở khối lệnh 2,  sau khi ký tự cuối cùng sẽ được in ra lần đầu tiên. Thì do feof (f) = 0, nên vòng while sẽ được thực hiện thêm lần nữa lệnh fscanf (f, "%c", &c); lại được gọi, nhưng do lúc này phần tử đọc được là EOF nên hàm fscanf sẽ bỏ qua dẫn đến biến c giữ nguyên giá trị. Kết cục, ký tự cuối cùng sẽ được in ra lần 2. Lúc này EOF đã được đọc do đó feof (f) = 1, vòng while kết thúc.

eof

2. Hàm fscanf (f, "%d", &temp)

        Để đọc các giá trị đã lưu trong file lên mảng. Hàm này sẽ hiểu ký tự trống là kết thúc nhập dữ liệu cho biến temp. Điều này dẫn đến cách đọc file khác với hàm fscanf (f, "%c", &c);.

        Cụ thể là có 2 trường hợp trong slide thầy giáo nêu ra (Từ trang 42 đến trang 46). Bây giờ ta đi phân tích 2 trường hợp này:

Lưu ý hình ảnh minh họa trong slide.

Ví dụ 1 (Không có khoảng trống phía sau phần tử cuối cùng), dùng

Khối lệnh 1:

        Đúng, bởi vì khi đọc phần tử cuối cùng hàm fscanf (f, "%d", &c); sẽ đọc cả phần tử EOF và hiểu đây là phần tử kết thúc nhập dữ liệu cho phần tử cuối cùng, dẫn đến sau khi phần tử cuối cùng được in ra thì feof (f) = 1 kết thúc vòng lặp.

Ví dụ 1 (Không có khoảng trống phía sau phần tử cuối cùng), dùng
Khối lệnh 2:

        Sai, bởi vì sau khi đọc phần tử cuối cùng feof (f) = 1, vòng lặp bị break, giá trị phần tử cuối cùng sẽ không được in ra dẫn đến mất giá trị.

Ví dụ 2 (Sau phần tử cuối cùng có ký tự trống), dùng:
Khối lệnh 2:

        Đúng,  bởi vì khi đọc phần tử cuối cùng hàm fscanf (f, "%d", &c); sẽ đọc cả ký tự trống sau nó và hiểu đây là phần tử kết thúc nhập dữ liệu cho phần tử cuối cùng, dẫn đến sau khi phần tử cuối cùng được đọc thì feof (f) = 0, do đó sẽ thực hiện lệnh in phần tử cuối cùng ra màn hình. Tiếp đó khi hàm  fscanf (f, "%d", &c) chạy phần tử EOF sẽ được đọc, khi này  feof (f) = 1, lệnh break được thực hiện, kết thúc vòng lặp.

Ví dụ 2 (Sau phần tử cuối cùng có ký tự trống), dùng:
Khối lệnh 1

        Sai, bởi vì, như trên, sau khi phần tử cuối cùng được đọc và in ra. feof  (f ) vẫn bằng 0, vòng lặp tiếp tục, EOF được đọc bởi fscanf nhưng bị lờ đi (do là ký tự kết thúc nhập). Do đó c vẫn giữ nguyên giá trị và bị in ra lần nữa. Đến lúc này feof  (f) đã bằng 1, kết thúc vòng lặp.


Tổng kết: Để đọc file thuận tiện và đơn giản nhất, ta nên thiết kế cấu trúc cho file, như ở trường hợp trên thay vì đau đầu với EOF ta nên lưu lại số phần tử của file ở đầu file. Khi đọc, ta sẽ đọc số phần tử này vào môt biến n, rồi dùng vòng for thay vì while. Mọi chuyện sẽ trở nên đơn giản hơn rất nhiều nếu ban đầu ta có một thiết kế tốt.


 Tài liệu tham khảo
1, Giáo trình tin học đại cương (Viện công nghệ thông tin, HUST)
2, http://www.oktot.com/huong-dan-su-dung-tap-tin-file-trong-c/
3, https://en.wikipedia.org/wiki/ASCII
4, http://www.cplusplus.com/

The following two tabs change content below.

quanvu

Latest posts by quanvu (see all)