----> Lời khuyên cho các bạn bắt đầu học lập trình: nếu có tham khảo code trên mạng thì các bạn nên xem nó và tự viết lại rồi tìm hiểu xem từng phần trong code ý nó có mục đích là gì chứ đừng nên copy và paste một cách máy móc mà mình chả hiểu được code ý nó viết gì.
Khi có nhu cầu lưu trữ thông tin sau mỗi lần thực thi, chúng ta không thể dùng RAM, vì dữ liệu sẽ bị mất đi. Thay vào đó, nội dung trên đĩa cứng lại được bảo toàn, cụ thể là tập tin. Vậy làm thế nào để sử dụng chúng trong chương trình?
Trước hết phải tìm hiểu về tập tin đã. Mặc dù nó là một thứ không hề xa lạ với ai, tôi thao tác với nó hằng ngày, bạn sử dụng liên tục,… đủ kiểu. Dân lập trình không biết thao tác với tập tin quả là một thiếu sót rất lớn. Dùng tập tin có nhiều điểm lợi, trước hết phải kể đến là chuyện nó sẽ còn tồn tại khi bạn tắt chương trình, hoặc thậm chí tắt máy tính đi. Một khi được tạo ra, nó sẽ vẫn ở đó cho đến khi bị delete!. Không giống như RAM bị mất hoàn toàn thông tin khi cúp điện, không giống bộ nhớ chương trình bị thu lại sau khi kết thúc.
Bạn có thể sử dụng tập tin để làm gì khi lập trình, tôi liệt kê một số công dụng cơ bản:
Tập tin được người ta chia làm hai loại: tập tin văn bản (text) và tập tin nhị phân (binary). Thực ra tập tin văn bản cũng là tập tin nhị phân thôi, nhưng nhờ cách thức lưu trữ của nó, bạn có thể dùng notepad hoặc một chương trình đọc nào đó để mở như một quyển sách vậy. Riêng các tập tin nhị phân, bạn vẫn mở được đó. Tuy nhiên, đó chỉ là một mớ lộn xộn.
Tập tin văn bản thường thấy có phần mở rộng là txt, ini.
Đối với các tập tin nhị phân như trên, bạn cần sử dụng chương trình riêng biệt để có thể đọc được chúng. Ví dụ .mp3 thì phải dùng chương trình nghe nhạc như Windows Media Player để nghe.
Riêng về phần truy cập tập tin, tôi sẽ không trình bày phần của ngôn ngữ C mà chỉ trình bày cách truy cập tập tin của C++. Tôi không thích cách mà C làm, tôi quen hơn với C++.
Để truy cập, trước tiên bạn cần phải có một kiểu dữ liệu có khả năng quản lý, trong C++, kiểu dữ liệu đó có tên là fstream trong thư viện fstream.
1 2 3 4 5 6 7 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; return 0; } |
fstream có thể hiểu nó là một cấu trúc (struct), lưu thông tin về tập tin mà nó đang mở. Một điều đặc biệt là nó không chỉ lưu trữ biến thành viên, mà nó có thể lưu trữ cả hàm thành viên. Bạn sẽ làm quen với việc này, nhanh thôi.
Tham khảo thêm: http://www.cplusplus.com/reference/fstream/fstream/
Các thao tác chính khi truy cập tập tin bao gồm Mở – Đọc/Viết – Đóng. Những việc này, fstream sẽ đảm nhiệm cho bạn cách hoàn hảo. Khi lập trình với tập tin, nhớ đóng tập tin lại sau khi sử dụng. Nếu quên, khả năng bạn bị mất dữ liệu rất cao.
Tại sao? Cần nhớ rằng, tất cả thao tác trên tập tin chúng ta đều dựa vào hệ thống. Khi bạn yêu cầu ghi thông tin lên tập tin, để đảm bảo hiệu năng, hệ thống sẽ ghi tạm thời dữ liệu đó vào RAM. Khi nào dữ liệu đó đủ lớn, hệ thống mới bắt đầu ghi vào đĩa. Hoặc khi đóng tập tin, hệ thống cũng sẽ bắt đầu ghi. Do đó, nếu bạn mở mà không đóng tập tin lại, những dữ liệu mà hệ thống chưa kịp ghi vào đĩa sẽ mất hoàn toàn. OK?
Thao tác 1: Mở tập tin
Sử dụng đối tượng (kiểu dữ liệu fstream) tôi đã khai báo ở trên, chúng ta gọi hàm fstream.open, trong đó fstream là đối tượng bạn đã khai báo (của tôi là f):
1 | void open(const char *_Filename, ios_base::openmode _Mode); |
_Filename là chuỗi, tên tập tin mà bạn muốn mở. _Mode là là cách thức bạn sẽ sử dụng tập tin (mở để viết, đọc, đọc nhị phân,…). Các tham số bạn có thể truyền cho _Mode bao gồm:
Bây giờ tôi sẽ tạo ra tập tin 123.txt để ghi:
1 | f.open("123.txt", ios_base::out); |
Nếu tập tin 123.txt không có trong đĩa, nó sẽ được tạo ra, nếu có rồi, hệ thống sẽ xoá trắng nó. Tập tin 123.txt nằm ở đâu? Trong thư mục project của bạn:
Để đọc từ tập tin 123.txt, bạn truyền tham số như sau:
1 | f.open("123.txt", ios_base::in); |
Để đọc nhị phân:
1 | f.open("123.txt", ios_base::in | ios_base::binary); |
Để viết theo kiểu nhị phân:
1 | f.open("123.txt", ios_base::out | ios_base::binary); |
Nếu không chỉ định là đọc nhị phân hay đọc văn bản, mặc định bạn sẽ được mở theo cách đọc văn bản. Chú ý tham số thứ hai, bạn có thấy tôi sử dụng phép toán bit OR không (không phải toán tử logic OR – ||), họ làm thế để khai thác bộ nhớ tốt hơn, thay vì phải dùng thêm 1 tham số nữa. Bạn chỉ nên sử dụng một trong hai kiểu mở file: một là để đọc, một là để ghi, không nên mở file theo cả hai kiểu, vừa đọc, vừa ghi. Nó là vấn đề của sự đồng bộ, các bạn sẽ hiểu khi nghiên cứu sâu hơn.
Khi mở tập tin ra để ghi, nếu tập tin không tồn tại, nó sẽ được tạo ra. Còn nếu bạn mở tập tin để đọc, mà nó không có trên máy thì phải xử lý thế nào?
fstream quản lý việc này, nếu tập tin không tồn tại, nó không báo lỗi mà sẽ lưu lại kết quả. Bạn có thể truy cập kết quả qua hàm:
1 | bool f.is_open() |
Kiểu trả về của hàm là bool, true có nghĩa là tập tin đã mở thành công, false nếu thất bại. Bạn luôn luôn phải làm động tác này, không thì chương trình của bạn sẽ bị đánh giá là thiếu chuyên nghiệp.
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("123.txt", ios_base::in); if (!f.is_open()){ // nếu không mở được file thì vô đây cout << "Khong mo duoc file!"; } f.close(); return 0; } |
Còn một vấn đề nữa, nếu đối tượng f đang mở một file, bạn yêu cầu nó mở file khác mà chưa đóng file đang mở hiện tại, nó cũng sẽ thất bại.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("123.txt", ios_base::in); // gọi lần 2 mà chưa đóng, không mở được f.open("345.txt", ios_base::in); if (!f.is_open()){ // nếu không mở được file thì vô đây cout << "Khong mo duoc file!"; } f.close(); return 0; } |
Cứ viết theo khuôn mẫu như thế này chương trình của bạn sẽ an toàn hơn rất nhiều.
Thao tác 3: đóng tập tin
Thao tác 2 tôi dành riêng 2 chương cho nó bên dưới, bây giờ chúng ta làm trước thao tác 3. Ngắn gọn thôi:
1 | f.close(); |
Mở nhiều tập tin cùng lúc
Mỗi biến f kiểu fstream bạn khai báo chỉ được sử dụng để mở một tập tin. Nếu muốn mở nhiều tập tin cùng 1 lúc, bạn cần khai báo thêm một biến fstream nữa.
1 | fstream f1, f2; |
Bạn cần nhớ rằng, các thao tác như open, close,… Khi sử dụng trên biến nào thì chỉ có tác dụng với biến đó. Hai biến này sẽ hoàn toàn độc lập với nhau.
1 2 3 4 5 6 | fstream f1, f2; f1.open("123.txt", ios_base::in); // biến f2 có thể mở 123.txt nếu 123.txt không bị khoá f2.open("345.txt", ios_base::out); // Hai hàm open ở trên thao tác với biến gọi nó, // và chúng không liên quan gì tới nhau |
Khi dùng xong, nhớ đóng cả hai lại nhé:
1 2 | f1.close(); f2.close(); |
Tập tin văn bản dễ xử lý hơn, tôi trình bày nó trước. C++ Console không thể hiển thị tiếng việt, nên bạn đừng bắt nó đọc tập tin có dấu, bị lỗi đấy.
Ghi dữ liệu
Tôi cần cái khung như thế này:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("123.txt", ios_base::out); // mở ra theo dạng văn bản if (!f.is_open()){ // nếu không mở được file thì vô đây cout << "Khong mo duoc file!"; } // NƠI VIẾT TIẾP f.close(); return 0; } |
Để ghi dữ liệu, bạn thao tác như khi sử dụng cout.
1 | f << "Welcome to C++ text file"; |
Ấn F5 và hưởng thụ thành quả.
Các thao tác ghi, xuống hàng mà bạn đã làm quen trong nhập xuất màn hình đều có thể áp dụng ở đây. Tuyệt quá phải không. Bây giờ bạn hãy viết hàng thứ hai nội dung “Bai hoc so 10”, nhớ 10 là số nguyên.
Viết đi.
1 2 | f << "Welcome to C++ text file" << endl; f << "Bai hoc so " << 10; |
Woaa, rất thú vị và quen thuộc. Bạn thử đoán xem, khi muốn nhập dữ liệu từ tập tin, chúng ta cũng thực hiện thao tác tương tự chứ? Đương nhiên là có rồi.
Trước hết, bạn hãy chỉnh lại sao cho f sẽ mở file 123.txt để đọc theo kiểu văn bản. Sau đó khai báo một mảng kí tự a có 100 phần tử. Xong chưa, chúng ta cùng đọc:
1 2 | f >> a; cout << a; |
Ấn F5 và kiểm tra màn hình, bạn sẽ thấy nó in dòng chữ Welcome. Chẳng khác gì với cin nhỉ, cũng chỉ đọc tới đấu cách. OK, để kiểm tra, bạn hãy đọc từ ‘to’ và in ra màn hình.
1 2 3 | f >> a; // đọc Welcome f >> a; // đọc to cout << a; |
Như vậy là, sau khi đọc, nếu gọi lần thứ hai, chúng ta sẽ đọc tiếp những gì con dang dở. Tới đây, bạn cần làm quen với khái niệm con trỏ tập tin.
Việc đọc lần thứ hai chúng ta đọc tiếp những gì còn dang dở, đó là do con trỏ tập tin. Con trỏ tập tin xác định vị trí mà chúng ta sẽ thao tác tiếp trên tập tin. Khi mới mở file, con trỏ tập tin sẽ nằm ở đầu, nên lệnh đọc đầu tiên sẽ đọc được chuỗi Welcome, sau khi đọc xong, con trỏ tập tin di chuyển tới kí tự t để chuẩn bị cho lần đọc tiếp theo. Do đó, lệnh đọc thứ hai cho chúng ta kết quả là chuỗi “to”. Con trỏ tập tin cũng tồn tại khi bạn mở file để viết, tác dụng tương tự luôn.
Đọc từng từ chẳng thú vị gì hết, tôi muốn đọc nguyên một hàng! Tôi dùng hàm getline (!).
1 | f.getline(a, 100); |
Tham số thứ hai là khả năng bạn có thể nhận dữ liệu, tôi truyền 100 cũng là kích thước tối đa của mảng a. Nếu 100 không đủ để đọc hết 1 hàng, hàm sẽ dừng lại tại vị trí đó và trả về kết quả (không đầy đủ) cho bạn, hơi bị thiếu linh động nhỉ! Tại hàng đó dài quá.
Còn gì nữa không? Ah, đọc số nguyên, bạn hãy chỉnh lại file 123.txt sao cho hàng đầu tiên chứa số 100. Sau đó chúng ta sẽ đọc nó vào số nguyên:
1 2 3 | int i; f >> i; cout << i; |
Nếu con trỏ tập tin đang ở vị trí có chứa số (đầu file như mong muốn), nó sẽ đọc và đưa giá trị đó vào biến i cho bạn. Giống như đang nhập từ màn hình vậy.
Khi đọc tập tin, bạn cần chú ý thêm một chuyện nữa, đó là kích cỡ của file đang đọc. Giả sử file đó là trống rỗng không có thông tin gì, hàm đọc sẽ xử lý như thế nào? Chúng ta hãy kiểm tra! Bạn mở file 123.txt lên, xoá hết nội dung trong đó đi, rồi thực hiện lại chương trình vừa nãy.
Kết quả của tôi là giá trị rác (biến i). Chắc của bạn cũng thế! Rồi, giờ vào tập tin 123.txt, viết số “100” vào đó để cho chương trình chúng ta đọc. Tại phần code, bạn viết lệnh đọc – in ra màn hình biến i hai lần.
1 2 3 4 5 | int i; f >> i; cout << i; f >> i; cout << i; |
Trước khi chạy, chúng ta thử đoán trước kết quả: lệnh đọc đầu tiên, giá trị 100 sẽ được gán vào biến i, không có gì để bàn cãi. Vấn đề là sau khi đọc, con trỏ cũng đã đi tới cuối tập tin. Vậy lệnh đọc thứ hai sẽ xử lý như thế nào? Tôi dự đoán:
Bạn chọn đi. Sau đó F5.
Kết quả là 100 100, sau lần đọc thứ hai, giá trị biến i không thay đổi. Hiểu rồi, vậy làm sao để tránh chuyện này, chúng ta cần biết khi nào thì con trỏ tập tin đã đi tới cuối file. Lại phải nhờ tới fstream hỗ trợ, đó là hàm eof (End Of File):
1 2 3 4 5 6 7 | int i; f >> i; cout << i; if (!f.eof()){ // nếu chưa đến cuối file f >> i; // thì đọc tiếp cout << i; } |
Chạy thử, hoan hô, chỉ một số 100 được in ra. OK, đó là những gì C++ đã cung cấp cho bạn, một trải nghiệm quen thuộc phải không. Đừng lo, tập tin nhị phân sẽ trở thành một vấn đề lớn.
Khuôn mẫu đây:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; char a[100]; // mở ra theo dạng nhị phân f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ // nếu không mở được file thì vô đây cout << "Khong mo duoc file!"; } // VIẾT CODE VÔ ĐÂY f.close(); return 0; } |
Trước hết, mấy cái thao tác như bên văn bản (f >> , f <<) gì gì đó vô tác dụng. Bạn cần làm quen với hai hàm:
1 2 | void read(char*_Str, int _Count); void write(const char*_Str, int _Count); |
Chào tụi nó đi. Hi (cheer!). Xem nào, bạn chọn nghiên cứu hàm nào trước? Theo tôi thì nên tìm hiểu hàm ghi, sau đó là đọc. Vì, nếu bạn đọc trước thì lấy đâu ra dữ liệu để đọc!
Với hàm write, tham số thứ nhất là chuỗi cần ghi có kiểu char*, tham số thứ hai là số lượng kí tự sẽ ghi. Tham số thứ hai không có gì để nói rồi, bạn hiểu mà. Thứ chúng ta quan tâm là cái mảng char* kia. Tại sao không có kiểu int*, mà lại là char*. Nhu cầu của chúng ta bao gồm cả việc in số và in kí tự. Visual không biết bạn định dạng file như thế nào, thư viện fstream cũng chẳng biết, nên họ sẽ không tạo hàm write có khả năng ghi kiểu int*. Quyết định cuối cùng, họ sẽ cung cấp cho bạn kiểu dữ liệu cơ bản nhất để thao tác. Trong C++, kiểu dữ liệu có kích thước nhỏ nhất là char, nên char được chọn. Kiểu char tương đương 1 byte, kiểu int tương đương 4 byte, nên để ghi 4 byte int vào tập tin, bạn cần sử dụng 4 phần tử char. Nhưng, không cần phải rắc rồi như thế, con trỏ, con trỏ có thể ép kiểu thoải mái, nghĩa là 4 byte số nguyên có thể bị ép thành mảng kí tự có 4 phần tử.
Tôi coi 4 byte trong bộ nhớ của biến i như là mảng 4 phần tử char, sau đó ghi vào tập tin:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <fstream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::out | ios_base::binary); if (!f.is_open()){ } int i = 67; f.write((char*)&i, sizeof(int)); f.close(); return 0; } |
Bạn thấy chứ, tập tin bin.txt được tạo ra, sau đó ghi 4 byte của biến i vào (tuỳ vào đời máy mà kích thước int có thể thay đổi). Rồi, chúng ta dùng notepad mở file bin.txt:
Không hề có số 67, thay vào đó là một kí tự C, một khoảng trắng. Nếu để ý, 67 chính là kí tự C trong bảng mã ASCII. Chúng ta không thể dùng notepad để xem nội dung tập tin nhị phân được, thay vào đó là một chương trình có khả năng đọc tập tin dưới dạng nhị phân nguyên thuỷ, thường gọi là Hex Editor. Tôi chọn chương trình HxD. Sử dụng HxD để xem nội dung tập tin bin.txt, tôi được kết quả:
(HxD hiển thị dưới dạng hex, nên cứ 2 kí tự là một byte)
Tải phần mềm tại: http://mh-nexus.de/en/hxd/
Bạn thấy nội dung tập tin bao gồm 4 byte, là 4 byte bộ nhớ lưu trữ giá trị i. 43 dạng hex nếu tính ra giá trị thập phân sẽ là 67, là giá trị của biến i. Do 67 quá nhỏ nên 3 byte còn lại nhận giá trị 0.
Bạn đã xem kết quả ghi biến i vào tập tin dạng nhị phân rồi đó, bây giờ chúng ta sẽ đọc bằng hàm read. Hàm read cũng có danh sách tham số gần giống như write, chức năng cũng tương tự. Nó sẽ đọc _Count byte kể từ vị trí của con trỏ tập tin, rồi lưu vào mảng char* _Str. Sử dụng lại phương pháp ép kiểu con trỏ, bạn không cần phải khai báo mảng char a[4] nữa, mà sử dụng chính biến i làm mảng char[4].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ } int i; f.read((char*)&i, sizeof(int)); cout << i; f.close(); return 0; } |
Chương trình trên sẽ mở file bin.txt theo dạng nhị phân, đọc 4 byte đầu (sizeof(int)), rồi lưu vào 4 ô nhớ của biến i (đang bị ép kiểu sang char*). Nên dãy hex bạn thấy ở trên 43 00 00 00 sẽ được gán vào vùng nhớ của biến i. Kết quả, giá trị 67 được in ra.
Tôi đã giới thiệu cho bạn cách đọc và ghi số nguyên ra tập tin, bây giờ tới lượt bạn, chúng ta sẽ ghi một mảng số nguyên vào tập tin rồi đọc chúng. Đầu tiên là ghi vào tập tin bin.txt, tôi có mảng số nguyên sau:
12 3 54 33 553 78 91 67 12 9
Bạn thực hiện theo các gợi ý sau:
Hãy làm trước khi tiếp tục!
Mỗi số nguyên có kích thước 4 byte, 10 số nguyên liên tiếp sẽ chiếm 40 byte bộ nhớ. Nhưng đừng bao giờ dùng cách này, chúng ta đã có hàm sizeof để lấy kích thước của biến int, sau đó nhân với 10 là xong, đúng không!
Kết quả cần đạt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::out | ios_base::binary); if (!f.is_open()){ } int i[] = { 12, 3, 54, 33, 553, 78, 91, 67, 12, 9 }; f.write((char*)&i, sizeof(int) * 10); f.close(); return 0; } |
Xong phần ghi, giờ tới việc đọc. Bạn có thấy khó khăn gì không? Xem thử code của tôi bên dưới:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ } int i[10]; //= { 12, 3, 54, 33, 553, 78, 91, 67, 12, 9 }; f.read((char*)&i, sizeof(int) * 10); for (int j = 0; j < 10; j++) cout << i[j] << " "; f.close(); return 0; } |
Tôi để cho bạn tự nghiên cứu.
Dưới đây là một số kết luận về việc đọc nhị phân bạn cần biết:
Đọc ghi chuỗi kí tự động
Đọc ghi một chuỗi kí tự tĩnh như “123456789” gì gì đó tôi để dành phần cho bạn. Cái quan trọng chúng ta cần phải biết là việc đọc, ghi chuỗi kí tự có độ dài bất kì. Hiện tại, tôi giới thiệu với bạn hai cách:
Hai cách, mỗi cách đều có điểm lợi và điểm hại. Cách thứ nhất, bạn không biết số lượng kí tự, nên phải khai báo mảng char ban đầu thật lớn. Sau đó, đọc từng kí tự một cho đến khi gặp kí tự kết thúc chuỗi. Việc đọc từng kí tự đó sẽ làm giảm tốc độ thực thi của chương trình. Thứ ba là lãng phí bộ nhớ. Chuyện lãng phí bộ nhớ và khai báo mảng ban đầu thật lớn có thể giải quyết theo phương án: đọc tới đâu, in ra màn hình tới đó.
Trước hết, chúng ta cần 1 tập tin nhị phân đã lưu một chuỗi. Bạn sử dụng đoạn mã sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include <fstream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::out | ios_base::binary); if (!f.is_open()){ } char* a = "Welcome to C++"; f.write(a, strlen(a) + 1); // + 1: in thêm kí tự kết thúc chuỗi f.close(); return 0; } |
OK, tôi dùng HxD để kiểm tra, đây là kết quả:
Bên phải là đoạn tôi vừa mới ghi kìa! Chuẩn bị đã xong, chuỗi của chúng ta đang lưu trong file bin.txt có cấu trúc như mong muốn: kết thúc bằng giá trị 0. (sao không dùng notepad coi thử nó như thế nào!)
Phần tiếp theo là phần của bạn. Hãy làm theo chỉ dẫn:
Làm đi thôi!
Chương trình tham khảo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ } char a; do{ f.read(&a, 1); if(a) cout << a; // nếu a != 0 mới in ra } while (a != 0); f.close(); return 0; } |
Bạn thấy ổn chưa? Nếu không, hãy xem lại, nghỉ ngơi đôi chút trước khi tiếp tục sang cách thứ hai.
Cách thứ hai, tôi dành 4 byte đầu tiên để lưu độ dài chuỗi kí tự, sau đó là phần nội dung. Với cách này, tôi có thể sử dụng được bộ nhớ Heap, tất cả các kí tự đều được lưu trữ chứ không in ra rồi mất đi như cách thứ nhất. Đầu tiên, chúng ta cần chuẩn bị tập tin bin.txt theo định dạng đã nói:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::out | ios_base::binary); if (!f.is_open()){ } char *a = "Welcome to C++"; int i = strlen(a); f.write((char*)&i, sizeof(int)); // in số lượng f.write(a, i); // in nội dung f.close(); return 0; } |
Chúng ta cùng xem nội dung tập tin bằng HxD:
4 byte được tôi đánh dấu ở trên chính là 4 byte giá trị của biến i. 0E dạng hex khi chuyển qua thập phân có giá trị 14, nghĩa là chuỗi của chúng ta có 14 kí tự. Bạn kiểm tra xem đúng không!
Bây giờ là phần đọc, tôi sẽ thực hiện các thao tác
Biến i chỉ lưu số lượng phần tử, không tính đến kí tự kết thúc chuỗi nên bạn phải yêu cầu hệ thống cấp i + 1 ô nhớ. Còn nữa, chúng ta chỉ đọc i kí tự, nhưng kí tự kết thúc chuỗi chưa xác định, bạn phải làm điều đó. Code tham khảo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <fstream> #include <iostream> using namespace std; int main(){ fstream f; f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ } char *a; int i; f.read((char*)&i, sizeof(int)); // đọc số lượng kí tự a = new char[i + 1]; f.read(a, i + 1); // đọc tiếp phần nội dung a[i] = 0; // gán kí tự kết thúc chuỗi cout << a; f.close(); delete[] a; return 0; } |
Tôi đã hướng dẫn cho bạn các thao tác cơ bản khi sử dụng tập tin có cấu trúc, không biết nó có khó với bạn không nữa. Thôi kệ, chưa hiểu thì nhớ lên trên, nghiên cứu từ từ chứ đừng đọc tiếp, khó quá thì comment bên dưới. Phần cuối cùng cũng là phần quan trọng nhất, chúng ta sẽ thao tác với struct. Đây là struct của tôi:
1 2 3 4 5 | struct MyStruct{ int i; char a[10]; // 10 kí tự float f; }; |
Tôi sẽ ghi struct này vào file, không chỉ một mà là nhiều phần tử. Trước khi vào công việc, tôi cần bàn với bạn một chút về con trỏ – hay còn gọi là chuỗi. Giả sử, biến a tôi khai báo là char *a, xong, trong hàm main tôi cấp phát dữ liệu động cho biến a. Như vậy a đang là một chuỗi kí tự đúng không. Chuyện gì sẽ xảy ra khi bạn ghi struct có chứa a vào file? Nhớ rằng, nếu khai báo a là con trỏ, thì giá trị mà a đang lưu trữ sẽ là giá trị con trỏ. Cho dù con trỏ này có trỏ tới vùng nhớ nào đi chăng nữa, thì với struct, nó vẫn chỉ là một con trỏ, không phải mảng. Trong bộ nhớ nơi mà struct đang được lưu trữ, giá trị a chỉ là một số nguyên (có dạng con trỏ) chứ không phải là mảng. Nên khi bạn ghi struct này vào file, thực tế bạn đang ghi giá trị địa chỉ a đang lưu vào file. Dữ liệu mà a đang trỏ tới không hề bị ảnh hưởng. Giả sử bạn cố tình làm như thế đi, trong file đích chỉ lưu địa chỉ của mảng mà a trỏ tới. Xong chương trình kết thúc, bộ nhớ giải phóng, giá trị đang lưu trữ trở thành vô nghĩa, đã bị huỷ, mất thông tin mất rồi.
Nếu bạn cần sử dụng chuỗi, tôi khuyên nên khai báo mảng tĩnh như trên, khi đó dữ liệu sẽ nằm trong struct chứ không nằm ngoài như con trỏ. Nhưng bạn lại gặp vấn đề: lãng phí bộ nhớ. Ôi thôi kệ nó đi, tính sau.
Lúc ghi vào file, bạn cũng chỉ coi struct là một mảng char*, giống như bao kiểu dữ liệu khác. Kích thước lấy từ hàm sizeof nên không còn gì phải lo lắng. Tôi ví dụ cho bạn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <fstream> #include <iostream> using namespace std; struct MyStruct{ int i; char a[10]; // 10 kí tự float f; }; int main(){ fstream f; f.open("bin.txt", ios_base::out | ios_base::binary); if (!f.is_open()){ } MyStruct s[3]; // mảng 3 struct s[0] = { 10, "1 st", 10.0f }; s[1] = { 9, "2 nd", 11.0f }; s[2] = { 8, "3 rd", 12.0f }; f.write((char*)s, sizeof(MyStruct)* 3); // ghi cả 3 struct vào file f.close(); return 0; } |
Tôi ghi cả 3 struct vào file. Nếu bạn không thích, có thể ghi 1 thôi, tuỳ ý bạn. OK, sau đây tôi sẽ đọc từng struct lên và in ra màn hình:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <fstream> #include <iostream> using namespace std; struct MyStruct{ int i; char a[10]; // 10 kí tự float f; }; int main(){ fstream f; f.open("bin.txt", ios_base::in | ios_base::binary); if (!f.is_open()){ } MyStruct s; // dùng 1 struct thôi for (int i = 0; i < 3; i++){ // đọc ? byte rồi lưu vào struct f.read((char*)&s, sizeof(MyStruct)); cout << s.i << " " << s.a << " " << s.f << endl; // in ra màn hình } f.close(); return 0; } |
Tôi ưu tiên sử dụng code ví dụ cho bạn dễ hiểu hơn. Giảm bớt sử dụng ngôn từ chán ngấy, hi vọng sẽ có tác dụng tốt. Đó, không giải thích nữa, thế thôi hen.
OK, bài trước, chúng ta đã làm quen với thao tác trên bộ nhớ rồi, lần này, thay vì tiếc nuối mất hết dữ liệu khi chương trình kết thúc, chúng ta sẽ lưu hết dữ liệu lên đĩa. Thế là không sợ mất.
Các thông tin cần lưu bao gồm:
Đó, và các struct tôi khai báo như sau:
1 2 3 4 5 6 7 8 9 10 11 12 | struct Date{ char day, month; // ngày tháng không thể vượt quá 127 int year; }; struct ContactInfo{ char name[50]; Date birthday; bool gender; // 1 là nam char cmnd[9]; char addr[100]; }; |
Nhiệm vụ của bạn là viết chương trình nhập thông tin của ít nhất 3 người, sau đó lưu vào tập tin info.txt – lưu theo kiểu nhị phân.
Để lưu trữ thông tin 3 người trên bộ nhớ, bạn sử dụng phương pháp sau:
Các hàm thao tác với vector có thể các bạn sẽ cần: size(), push_back(), truy cập từng phần tử như mảng.
OK, làm thôi.
Mở rộng (làm nếu thích): mã hoá thông tin trước khi ghi vào file info.txt. Sử dụng thuật toán biến đổi trên bit XOR. (gợi ý: XOR một lần là mã, XOR lần thứ hai là giải).