Chào các bạn đang theo dõi khóa học lập trình trực tuyến từ ngữ C++.

Trong bài học này, tất cả chúng ta sẽ cùng tìm hiểu về các khái niệm về File và phương pháp để thao tác với File trong từ ngữ lập trình C/C++.

File

Nếu PC của các bạn có ổ cứng, hoặc các bạn có USB hoặc bất kỳ thiết bị lưu trữ nào thì chắc nịch các bạn đã từng làm việc với File. Khi các bạn chơi một game offline, thông tin nhân vật, điểm số, … sẽ được lưu trữ trong File để khi chương trình game bị tắt đi thì các bạn không phải chơi lại từ đầu. Khi các bạn thiết lập cấu hình cho một PM và tắt đi, cấu hình đó được lưu vào File để lần làm việc tiếp theo sẽ sử dụng. Hay khi các bạn biên dịch một chương trình C++ trên Visual Studio 2015, C++ Compiler của Visual studio sẽ đọc mã nguồn các bạn đã viết trong các file *.cpp để xác minh lỗi và dịch chúng sang file *.obj. Ngay cả hệ điều hành Windows mà các bạn đang sử dụng cũng là tập hợp của rất nhiều file được lưu trữ bên trong phân vùng ổ đĩa dùng cho Hệ điều hành…

Này là một vài ví dụ cho thấy sự tồn tại của File trong PC. Vậy thì tất cả chúng ta đã thao tác với những File đó như vậy nào?

Làm việc với File tất cả chúng ta chỉ có các thao tác cơ bản như: tạo file mới, đọc dữ liệu trong file, ghi dữ liệu vào file, xóa file… Và tất cả chúng ta làm điều đó hằng ngày, khi tất cả chúng ta chơi game, khi xem phim trên PC, … và ngay cả khi tất cả chúng ta lập trình, mã nguồn của tất cả chúng ta được lưu xuống File mã nguồn khi nhấn tổ hợp phím Ctrl + S.

Theo khái niệm trên Wikipedia về computer file: Một file trên PC là một tài nguyên dùng để lưu trữ thông tin lâu dài, sử dụng cho các chương trình PC.

Không khác gì như việc lưu trữ dữ liệu tạm thời trên RAM, file cũng lưu trữ dữ liệu dưới dạng nhị phân (0 hoặc 1), tuy nhiên tùy vào định dạng của file và cách chuyển hóa của mỗi PM đọc file mà tất cả chúng ta có những kiểu thông tin khác nhau. Ví dụ file .png thì được chuyển về dạng hình ảnh, PM Microsoft Word chuyển dãy bit nhị phân về dạng text…

Trong từ ngữ lập trình C/C++: File là kiểu đối tượng, nó xác nhận một stream và chứa các thông tin thiết yếu để điều khiển, bao gồm một con trỏ trỏ đến buffer của nó, các chỉ mục và trạng thái của nó.

Các bạn có thể hiểu File (trong từ ngữ lập trình C/C++) là một kiểu đối tượng mà thông qua nó tất cả chúng ta có thể thao tác với dữ liệu được lưu trữ bên trong File (chứ không phải là một File trên PC).

Để các bạn không bị nhầm lẫn, mình đang nói về kiểu dữ liệu FILE được khái niệm trong thư viện cstdio (hay stdio.h) mà có thể các bạn đã từng học trong từ ngữ C. Tất cả chúng ta sẽ học cách sử dụng các Stream để thao tác với file thay vì sử dụng kiểu dữ liệu FILE trong các bài học sau, nhưng mình nghĩ kiểu dữ liệu FILE trong thư viện cstdio cũng có những ưu thế riêng của nó nên mình không bỏ qua bài học này.

Làm việc với FILE type trong C/C++

Trong bài học này, mình sẽ hướng dẫn các bạn thực hiện các thao tác như mở file, đọc và ghi dữ liệu trong file… Tất cả chúng ta cần làm việc trên một file rõ ràng nào đó nên mình sẽ tạo một file với tên file là my_document.txt trong thư mục Desktop có đường dẫn trên máy mình là: C:/Users/ADMIN/Desktop/my_document.txt

Để làm việc với file, tất cả chúng ta cần biết vị trí của file (thông qua đường dẫn) để con trỏ kiểu FILE có thể tạo được luồng dữ liệu giữa người dùng và file trên thiết bị lưu trữ.




int

main

()

{

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file;

return

; }

Open file

Để mở một file, các bạn có thể sử dụng hàm fopen được khái niệm trong thư viện cstdio:

FILE

* fopen(

const

char

*

file

,

const

char

*mode);

Hàm fopen cho phép tạo một kết nối đến file với đường dẫn được lưu trữ bởi tham số thứ nhất. Nếu file không tồn tại, file mới sẽ được tạo ra với tên file như trong đường dẫn. Tham số thứ hai xác nhận kiểu truy cập vào file. Bảng dưới đây liệt kê các mode dùng để mở một file trong C:

Nếu mở file thành công, một địa chỉ của một đối tượng kiểu FILE sẽ được trả về. Nếu mở file thất bại thì trả về NULL.

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"r"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

;

Trong đoạn chương trình trên, mình mở file đã tạo sẵn trong thư mục Desktop với mode “r” (chỉ dùng để đọc dữ liệu).

See also  Hướng dẫn chuyển đổi CSV sang Excel mới nhất [nam] – Lava

Các bạn cần lưu ý rằng file trong PC tồn tại ở 2 dạng: file văn bản và file bị mã hóa.

File văn bản là những file mà các bạn có thể đọc được khi mở bằng các trình soạn thảo văn bản, thông thường những file này được định dạng Unicode (hoặc những định dạng dùng cho văn bản khác).

File bị mã hóa (thường gọi là file nhị phân) không thể đọc được khi mở file bằng các trình soạn thảo văn bản. Sử dụng File bị mã hóa giúp tất cả chúng ta bảo mật dữ liệu tốt hơn File văn bản.

Các mode mà mình đã liệt kê ở bảng trên chỉ dùng để thao tác với file văn bản. Khi thao tác với file bị mã hóa (file nhị phân), các bạn cần nối thêm kí tự b (binary) vào ngay sau mode mà các bạn chọn. Ví dụ: “rb”, “wb”, “ab”, “rb+”, “r+b”, …

Close file

Sau thời điểm thao tác với file xong, các bạn cần đóng file lại để tránh những lỗi phát sinh ngoài ý muốn. Để đóng file, tất cả chúng ta sử dụng hàm fclose:

int

fclose(

FILE

*

file

);

Trong số đó, file là con trỏ được dùng để lưu trữ địa chỉ của đối tượng FILE đang mở. Nếu đóng file thành công thì trả về giá trị 0, trái lại trả về EOF (End of file).

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"r"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; fclose(file);

Hàm fclose sẽ giải phóng toàn bộ dữ liệu chưa được xử lý trên file nếu chúng vẫn còn lưu trong buffer, đóng file lại, và giải phóng toàn bộ vùng nhớ mà đối tượng FILE sử dụng.

Write data to file

Các bạn đã thực hiện được thao tác mở và đóng file, nhưng lúc này, file mới tạo ra vẫn chưa có dữ liệu nên mình sẽ thực hiện thao tác ghi dữ liệu vào file trước. Để mở file cho cơ chế ghi file, tất cả chúng ta có các mode “w”, “r+”, “w+”, “a”, “a+”. Mình chỉ muốn ghi dữ liệu nên mình sẽ chọn mode “w”.

Nhưng trước hết, tất cả chúng ta nên tách thao tác ghi file ra một hàm riêng có dạng:

void

writeToFile(

FILE

*

file

);

Hàm này sẽ được gọi sau thời điểm mở file và trước khi đóng file.

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"w"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; writeToFile(file); fclose(file);

Hiện giờ, tất cả chúng ta chỉ quan tâm đến nội dung bên trong hàm writeToFile.

Để ghi dữ liệu vào file, tất cả chúng ta có các hàm đã được khái niệm sẵn trong thư viện cstdio như sau:

  • fputc:

    int

    fputc

    (

    int

    c, FILE *f)

    ;

    Hàm fputc sẽ ghi ký tự có mã ASCII là c vào file được trỏ đến bởi con trỏ f. Giá trị trả về là EOF nếu ghi dữ liệu thất bại, trả về mã ASCII của kí tự được ghi vào nếu thực hiện thành công.

    Ví dụ:

    void

    writeToFile

    (FILE *file)

    {

    int

    c = fputc(

    'A'

    , file);

    std

    ::

    cout

    << c <<

    std

    ::

    endl

    ; }

    Sau thời điểm chạy chương trình xong, các bạn mở file my_document.txt trên Desktop lên sẽ thấy kí tự ‘A’ đã được ghi vào, đồng thời trên console cũng in ra mã ASCII của kí tự ‘A’.

  • fputs:

    int

    fputs

    (

    const

    char

    *str, FILE *f)

    ;

    Hàm fputs ghi một C-Style string vào file được trỏ đến bởi con trỏ f cho đến khi gặp kí tự ‘’.

    Ví dụ:

    void

    writeToFile(

    FILE

    *

    file

    ) {

    int

    c = fputs(

    "hello"

    ,

    file

    ); }

    Sau thời điểm chạy chương trình, các bạn mở file my_document.txt ở thư mục Desktop sẽ thấy kí tự ‘A’ lúc nãy không còn tiếp, thay vào này là chuỗi kí tự “hello”.

  • fprintf:

    int

    fprintf

    (FILE *f,

    const

    char

    *format, ...)

    ;

    Hàm fprintf tương tự hàm printf trong từ ngữ C. Tuy nhiên, hàm printf được mặc định link với đối tượng FILE có tên là stdout nên sử dụng hàm printf sẽ ghi nội dung ra màn hình.

    Hàm printf tương tự với cách sử dụng fprintf(stdout, format, ...).

    Nhưng lúc này, tất cả chúng ta muốn ghi dữ liệu vào file nên tất cả chúng ta sẽ truyền vào tham số thứ nhất là một con trỏ kiểu FILE khác với các đối tượng stdout, stdin hay stderr.

    Cách định dạng cho tham số format trong hàm fprintf cũng tương tự với hàm printf, các bạn có thể tham khảo ở đây:

    http://www.cplusplus.com/reference/cstdio/printf/

    Ví dụ:

    void

    writeToFile(

    FILE

    *

    file

    ) {

    for

    (

    int

    i =

    1

    ; i <=

    5

    ; i++) fprintf(

    file

    ,

    "This is an example line %dn"

    , i); }

    Sau thời điểm chạy đoạn chương trình trên, các bạn mở lại file my_document.txt trong thư mục Desktop để xem lại kết quả.

    Sử dụng hàm fprintf giúp tất cả chúng ta đơn giản định dạng cho dữ liệu được ghi vào file, từ đó có thể ghi nhiều dòng vào file bằng vòng lặp nếu các dòng đó có cùng định dạng.

See also  Cách ghép file PDF online, offline đơn giản trong nháy mắt

Read data from file

Trước tiên mình sẽ tạo một hàm khác có tên là readFromFile như sau:

void

readFromFile(

FILE

*

file

) { }

Để làm các ví dụ trong phần này, mình sẽ gọi hàm này sau thời điểm đã gọi hàm writeToFile.

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"w+"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; writeToFile(file); readFromFile(file); fclose(file);

Lúc này, file của tất cả chúng ta được mở để vừa đọc và ghi file, nên mình sẽ sử dụng mode “w+” (hoặc “r+”).

Và dưới đây là một số hàm được khái niệm sẵn trong thư viện cstdio trợ giúp tất cả chúng ta đọc dữ liệu văn bản từ file.

  • fgetc:

    int

    fgetc

    (FILE *f)

    ;

    Hàm fgetc đọc ra một kí tự trong file, internal file position indicator sẽ chuyển đến kí tự tiếp theo. Giá trị trả về là mã ASCII của kí tự đã đọc được.

    Ví dụ:

    void

    readFromFile

    (FILE *file)

    {

    std

    ::

    cout

    << (

    char

    )fgetc(file) <<

    std

    ::

    endl

    ; }
  • fgets:

    char

    *

    fgets

    (

    char

    *buf,

    int

    n, FILE *f)

    ;

    Hàm fgets đọc từ file ra (n – 1) kí tự, việc đọc dữ liệu sẽ bị dừng nếu đọc được kí tự new line ‘n’ hoặc EOF. Chuỗi kí tự đọc được sẽ lưu vào vùng nhớ được quản lý bởi con trỏ buf, nếu đọc dữ liệu thành công thì trả về địa chỉ của buf, trái lại trả về NULL.

    Ví dụ:

    void

    readFromFile

    (FILE *file)

    {

    char

    str[

    255

    ];

    std

    ::

    cout

    << fgets(str,

    255

    , file) <<

    std

    ::

    endl

    ;

    std

    ::

    cout

    << str <<

    std

    ::

    endl

    ; }

    Kết quả đọc file được lưu vào mảng kí tự str.

  • fscanf:

    Tương tự như ghi dữ liệu vào file với định dạng cho trước, tất cả chúng ta cũng có thể đọc dữ liệu từ file với một định dạng nào đó.

    int

    fscanf

    (FILE *f,

    const

    char

    *format, ...)

    ;

    fscanf cũng hoạt động tương tự hàm scanf trong từ ngữ C, tuy nhiên, hàm scanf được thiết lập link mặc định đến file stdin. Như vậy, fscanf(stdin, format, ...) sẽ tương tự với scanf(format, ...).

    Hàm fscanf cũng hoạt động tương tự scanf nên các bạn có thể tham khảo thêm tại đây:

    http://www.cplusplus.com/reference/cstdio/scanf/

    Ví dụ:

    void

    readFromFile

    (FILE *file)

    {

    char

    str[

    255

    ];

    fscanf

    (file,

    "%[^n]"

    , str);

    std

    ::

    cout

    << str <<

    std

    ::

    endl

    ; }

    Lưu ý, sử dụng hàm fscanf không khiến internal file position indicator di chuyển đến các vị trí tiếp theo nên tất cả chúng ta cần thêm một số kĩ thuật khác liên quan tới việc di chuyển con trỏ trong file (internal file position indicator).

Reposition stream position indicator

Ghép các ví dụ ở trên lại, tất cả chúng ta có một chương trình đơn giản minh họa cho việc ghi file và đọc từng dòng dữ liệu (line by line) đã được ghi vào file như sau:





void

writeToFile

(FILE *file)

{

for

(

int

i =

1

; i <=

5

; i++)

fprintf

(file,

"This is an example line %dn"

, i); }

void

readFromFile

(FILE *file)

{

char

str[

255

];

while

(fgets(str,

255

, file) !=

NULL

) {

std

::

cout

<< str; } }

int

main

()

{

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"w+"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; writeToFile(file); readFromFile(file); fclose(file);

return

; }

Tuy nhiên, kết quả cho ra màn hình không như muốn.

Nguyên nhân là khi tất cả chúng ta gọi hàm writeToFile và truyền vào đó con trỏ file, việc ghi file đã khiến internal file position indicator trỏ đến vị trí cuối cùng trong file. Sau thời điểm quay trở lại hàm main, tất cả chúng ta tiếp tục gọi hàm readFromFile với cùng một con trỏ file. Như vậy, lúc tất cả chúng ta đọc file thì tất cả chúng ta lại khởi đầu đọc tại vị trí kết thúc file.

Để khắc phục điều này, tất cả chúng ta cần đóng con trỏ file lại và mở tạo một link mới bằng hàm fopen với mode dùng để đọc file. Tuy nhiên, làm như vậy thì code xử lý của tất cả chúng ta sẽ dài hơn. Thư viện cstdio đã trợ giúp cho tất cả chúng ta hàm fseek để thay đổi vị trí trỏ đến trong file của internal file position indicator.

int

fseek

(FILE *f,

long

int

offset,

int

origin)

;

Trong số đó:

  • f là con trỏ trỏ đến đối tượng FILE đang mở.

  • offset là số bytes được cộng thêm tính từ vị trí origin.

  • origin là vị trí đặt con trỏ trong file:

Như vậy, sau thời điểm gọi hàm writeToFile xong, tất cả chúng ta cần di chuyển internal file position indicator về đầu file bằng cách như sau:

writeToFile(

file

) fseek(

file

,

, SEEK_SET) readFromFile(

file

)

Sau đó chạy chương trình thì thấy dữ liệu in ra màn hình đúng như những gì tất cả chúng ta đã ghi vào file.

Determine size of content of file

Thỉnh thoảng tất cả chúng ta cần đọc toàn thể nội dung của file vào một vùng nhớ trên Heap, tất cả chúng ta sẽ cần biết trước kích thước nội dung có trong file để cấp phát đủ vùng nhớ trước khi đọc file. Thư viện cstdio chỉ phân phối cho tất cả chúng ta hàm ftell:

See also  Tải Phần Mềm Chuyển File Âm Thanh Sang Văn Bản, Chuyển Đổi Âm Thanh Sang Văn Bản Cho Ứng Dụng

long

int

ftell

(FILE *f)

;

Hàm này sẽ trả về vị trí của file indicator đang trỏ đến trong file (số bytes của nội dung file mà indicator đã duyệt qua).

Như vậy, các bạn có thể đọc kích thước của nội dung trong file bằng cách dịch internal file position indicator về vị trí cuối cùng trong file rồi gọi hàm ftell:

__int64 size_of_file(

FILE

*

file

) { fseek(

file

,

, SEEK_END); __int64

size

= ftell(

file

); fseek(

file

,

, SEEK_SET);

return

size

; }

Binary I/O functions

Dưới đây là 2 hàm dùng để đọc và ghi dữ liệu chỉ dùng cho mode nhị phân.

size_t

fwrite(

const

void

*ptr,

size_t

size,

size_t

count, FILE *f);

Hàm fwrite dùng để ghi dãy bit trong vùng nhớ được quản lý bởi con trỏ ptr vào file đang được trỏ bởi f, size là số bytes sẽ sao chép từ vùng nhớ của ptr và count là số lần ghi vùng nhớ đó xuống file.

Hàm fwrite không quan tâm vùng nhớ của các bạn có định dạng gì, nó quan tâm kích thước vùng nhớ cần đọc và cứ thế sao chép toàn bộ các bits và file, mỗi lần sẽ sao chép 1 block of bit.

size_t

fread(

void

*ptr,

size_t

size,

size_t

count, FILE *f);

Hàm fread sẽ sao chép count lần block of bits có kích thước là size, mang vào vùng nhớ được trỏ đến bởi ptr, từ file đang được quản lý bởi f.

Sau thời điểm gọi hàm fread, internal file position indicator sẽ di chuyển tới (size * count) bytes từ vị trí khởi đầu đọc file.

Ví dụ:





void

writeToFile

(FILE *file)

{

char

*s =

"Hello everyone!"

; fwrite(s,

strlen

(s),

1

, file); }

void

readFromFile

(FILE *file)

{

void

*ptr =

operator

new

(

255

); fread(ptr,

255

,

1

, file); (

static_cast

<

char

*>(ptr))[

255

] =

''

;

std

::

cout

<<

static_cast

<

char

*>(ptr) <<

std

::

endl

; }

int

main

()

{

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"w+b"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; writeToFile(file); fseek(file,

, SEEK_SET); readFromFile(file); fclose(file);

return

; }

Chạy chương trình trên cho ra kết quả là rất nhiều kí tự rác.

Như các bạn thấy, hàm fread đọc đúng 255 bytes trong file để mang vào vùng nhớ của ptr nên các giá trị thừa xuất hiện. Trong trường hợp này, dùng hàm fread và fwrite không thích hợp. Hàm fread và fwrite thường được dùng để đọc và ghi dữ liệu kiểu struct vào file.

Write and read structs

Mình vẫn giữ nguyên cấu trúc chương trinh như trên và chỉ thay đổi code trong hàm writeToFile và readFromFile.

Trước hết, mình tạo một struct đơn giản như sau:

struct Employee
{
	__int32 ID
	char name[

50

] }

Kích thước của struct này là 56 bytes (không phải là 54 bytes do cách tổ chức dữ liệu trong struct còn liên quan đến khái niệm struct alignment). Như vậy là mỗi unit có kiểu Employee được tạo ra đều chiếm một vùng nhớ có kích thước 56 bytes.

Các bạn thử tưởng tượng nếu tất cả chúng ta sử dụng các hàm ghi file như fputs, fprintf… thì kích thước tên của mỗi người sẽ khác nhau kéo theo tất cả chúng ta không có một định dạng chung để dễ quản lý nhiều Employee trong file. Việc đặt chúng vào trong 1 struct giúp tất cả chúng ta đọc và ghi file đơn giản hơn nhiều.

Dưới đây là một đoạn chương trình mẫu cho việc xử lý file để quản lý 3 Employee:





struct

Employee {

__

int32 ID;

char

name[

50

]; }; Employee emps[

3

] = { {

1

,

"Le Tran Dat"

}, {

2

,

"Ngo Doan Tuan"

}, {

3

,

"Le Dinh Huy"

} };

void

writeToFile

(FILE *file)

{

for

(

int

i =

; i <

3

; i++) { fwrite(&emps[i],

sizeof

(Employee),

1

, file); } }

void

readFromFile

(FILE *file)

{ Employee emp;

for

(

int

i =

; i <

3

; i++) { fread(&emp,

sizeof

(Employee),

1

, file);

std

::

cout

<< emp.ID <<

std

::

endl

;

std

::

cout

<< emp.name <<

std

::

endl

;

std

::

cout

<<

"================================"

<<

std

::

endl

; } }

int

main

()

{

const

char

*filePath =

"C:/Users/ADMIN/Desktop/my_document.txt"

; FILE *file; file = fopen(filePath,

"w+b"

);

if

(!file)

std

::

cout

<<

"Can not open this file"

<<

std

::

endl

;

else

std

::

cout

<<

"File is opened"

<<

std

::

endl

; writeToFile(file); fseek(file,

, SEEK_SET); readFromFile(file); fclose(file);

return

; }

Các bạn chạy thử đoạn chương trình trên để xem kết quả.

Hiện giờ tất cả chúng ta cùng mở file my_document.txt trong thư mục Desktop để xem thử nội dung trong file như vậy nào:

Vậy là nội dung file đã bị mã hóa dưới dạng nhị phân nên không thể đọc hoàn toàn nội dung lưu trong file được.

Việc sử dụng hàm fread và fwrite cho các kiểu dữ liệu struct giúp tất cả chúng ta thao tác đơn giản hơn khi kích thước của các biến struct là giống nhau.

Hẹn hội ngộ các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.

Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt thắc mắc trực tiếp tại diễn đàn.

www.daynhauhoc.com