Bài 37.5: Bài tập thực hành xử lý file trong C++

Bài 37.5: Bài tập thực hành xử lý file trong C++
Chào mừng trở lại chuỗi bài học C++ của chúng ta! Hôm nay, chúng ta sẽ đi sâu vào một kỹ năng cực kỳ thiết yếu trong lập trình: xử lý file. Khả năng đọc dữ liệu từ file và ghi dữ liệu ra file cho phép chương trình của bạn tương tác với thế giới bên ngoài, lưu trữ thông tin lâu dài và xử lý các tập dữ liệu lớn.
Trong C++, thư viện <fstream>
cung cấp các công cụ mạnh mẽ để thực hiện điều này. Chúng ta có ba lớp chính cần nhớ:
ofstream
: Dùng để ghi dữ liệu ra file (output file stream).ifstream
: Dùng để đọc dữ liệu từ file (input file stream).fstream
: Dùng cho cả việc đọc và ghi file (file stream).
Bài hôm nay sẽ là chuỗi các bài tập thực hành nhỏ để giúp bạn làm quen và thành thạo các thao tác cơ bản này. Hãy cùng bắt tay vào nào!
Bài tập 1: Ghi dữ liệu đơn giản vào file
Bài tập đầu tiên là tạo một file mới và ghi một vài dòng văn bản vào đó. Đây là thao tác cơ bản nhất khi làm việc với file.
Yêu cầu: Tạo một file tên là chao_the_gioi.txt
và ghi dòng chữ "Xin chao, day la C++!" và "Chung ta dang hoc xu ly file." vào file này.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream o;
o.open("chao_the_gioi.txt");
if (o.is_open()) {
o << "Xin chao, day la C++!" << endl;
o << "Chung ta dang hoc xu ly file." << endl;
o.close();
cout << "Da ghi du lieu vao file chao_the_gioi.txt" << endl;
} else {
cerr << "Khong the mo file de ghi." << endl;
}
return 0;
}
Output:
Da ghi du lieu vao file chao_the_gioi.txt
Giải thích:
- Chúng ta bắt đầu bằng việc bao gồm các thư viện cần thiết:
<iostream>
cho nhập/xuất cơ bản ra console,<fstream>
cho xử lý file, và<string>
nếu cần làm việc với chuỗi. ofstream outFile;
khai báo một đối tượng luồng ghi file.outFile.open("chao_the_gioi.txt", ios::out);
mở filechao_the_gioi.txt
. Nếu file này chưa tồn tại, nó sẽ được tạo mới. Chế độios::out
đảm bảo rằng nội dung file sẽ bị ghi đè nếu nó đã tồn tại.if (outFile.is_open())
là một bước quan trọng để kiểm tra xem việc mở file có thành công không. File có thể không mở được vì nhiều lý do (ví dụ: không có quyền ghi, tên file không hợp lệ, ổ đĩa đầy...).- Chúng ta sử dụng toán tử
<<
để ghi dữ liệu vàooutFile
, giống hệt cách chúng ta dùngcout
.endl
thêm một ký tự xuống dòng. outFile.close();
đóng luồng file. Điều này giải phóng tài nguyên hệ thống và đảm bảo mọi dữ liệu trong bộ nhớ đệm đã được ghi hoàn toàn vào file. Luôn luôn đóng file khi bạn làm việc xong!
Sau khi chạy chương trình này, bạn sẽ thấy một file chao_the_gioi.txt
được tạo trong cùng thư mục với file mã nguồn của bạn, chứa hai dòng văn bản đã ghi.
Bài tập 2: Đọc dữ liệu từ file (đọc từng dòng)
Bây giờ chúng ta sẽ thực hành đọc lại nội dung từ file mà chúng ta vừa tạo.
Yêu cầu: Đọc nội dung từ file chao_the_gioi.txt
và in từng dòng ra màn hình console.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream i;
i.open("chao_the_gioi.txt");
if (i.is_open()) {
string s;
while (getline(i, s)) {
cout << s << endl;
}
i.close();
} else {
cerr << "Khong the mo file de doc. Kiem tra file chao_the_gioi.txt co ton tai khong?" << endl;
}
return 0;
}
Output:
Xin chao, day la C++!
Chung ta dang hoc xu ly file.
Giải thích:
- Chúng ta sử dụng
ifstream
để tạo đối tượng luồng đọc file. inputFile.open("chao_the_gioi.txt", ios::in);
mở file ở chế độ đọc.- Again, luôn luôn kiểm tra
inputFile.is_open()
! - Vòng lặp
while (getline(inputFile, line))
là cách phổ biến và hiệu quả để đọc file văn bản từng dòng.getline
đọc một dòng từinputFile
(bao gồm cả khoảng trắng) và lưu vào biếnline
cho đến khi gặp ký tự xuống dòng hoặc kết thúc file. Vòng lặp tiếp tục cho đến khigetline
không đọc được dòng nào nữa (do hết file hoặc lỗi), lúc đó nó trả về giá trị false và vòng lặp dừng lại. - Mỗi dòng đọc được sẽ được in ra màn hình.
- Cuối cùng, chúng ta đóng file bằng
inputFile.close();
.
Bài tập 3: Ghi dữ liệu có cấu trúc
Thông thường, chúng ta không chỉ ghi văn bản đơn thuần mà còn ghi các loại dữ liệu khác như số nguyên, số thực, v.v. Chúng ta có thể sử dụng toán tử <<
tương tự như cout
.
Yêu cầu: Tạo một file tên là du_lieu_hoc_sinh.txt
và ghi thông tin của một vài học sinh (tên và tuổi) vào file, mỗi học sinh một dòng.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream o("du_lieu_hoc_sinh.txt");
if (o.is_open()) {
string t1 = "Nguyen Van A"; int u1 = 15;
o << t1 << " " << u1 << endl;
string t2 = "Tran Thi B"; int u2 = 16;
o << t2 << " " << u2 << endl;
string t3 = "Le Van C"; int u3 = 15;
o << t3 << " " << u3 << endl;
o.close();
cout << "Da ghi du lieu hoc sinh vao file." << endl;
} else {
cerr << "Khong the mo file du_lieu_hoc_sinh.txt de ghi." << endl;
}
return 0;
}
Output:
Da ghi du lieu hoc sinh vao file.
Giải thích:
- Chúng ta sử dụng cách mở file ngắn gọn hơn: truyền tên file trực tiếp vào hàm tạo của
ofstream
. Đây là cách phổ biến nếu bạn chỉ cần mở file với các chế độ mặc định. - Chúng ta ghi tên (kiểu
string
), một khoảng trắng, và tuổi (kiểuint
) vào file. Toán tử<<
tự động chuyển đổi các kiểu dữ liệu này thành dạng văn bản phù hợp để ghi vào file. - Mỗi dòng kết thúc bằng
endl
để mỗi học sinh nằm trên một dòng riêng biệt.
Bài tập 4: Đọc dữ liệu có cấu trúc
Tiếp theo, chúng ta sẽ đọc lại file du_lieu_hoc_sinh.txt
và trích xuất tên và tuổi của từng học sinh.
Yêu cầu: Đọc file du_lieu_hoc_sinh.txt
, đọc tên và tuổi của từng học sinh, và in chúng ra màn hình.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ifstream i("du_lieu_hoc_sinh.txt");
if (i.is_open()) {
string t;
int u;
while (i >> t >> u) {
cout << "Hoc sinh: " << t << ", Tuoi: " << u << endl;
}
i.close();
cout << "Da doc xong du lieu hoc sinh tu file." << endl;
} else {
cerr << "Khong the mo file du_lieu_hoc_sinh.txt de doc." << endl;
}
return 0;
}
Output:
Hoc sinh: Nguyen, Tuoi: 15
Hoc sinh: Van, Tuoi: 16
Hoc sinh: Tran, Tuoi: 15
Da doc xong du lieu hoc sinh tu file.
Giải thích:
- Chúng ta dùng
ifstream
để đọc file. Vòng lặp
while (inputFile >> ten >> tuoi)
sử dụng toán tử>>
để đọc dữ liệu. Toán tử>>
hoạt động tương tự như khi đọc từcin
: nó đọc dữ liệu cho đến khi gặp khoảng trắng (space, tab, newline) và cố gắng chuyển đổi nó sang kiểu dữ liệu của biến nhận.- Lần đọc đầu tiên
inputFile >> ten
sẽ đọc "Nguyen" và lưu vàoten
. - Lần đọc thứ hai
inputFile >> ten
sẽ đọc "Van" và lưu vàoten
. - Lần đọc thứ ba
inputFile >> ten
sẽ đọc "A" và lưu vàoten
. Oops! - Cách đọc này không phù hợp nếu tên có khoảng trắng. Nó chỉ hoạt động tốt nếu mỗi "mục" dữ liệu được phân tách bằng khoảng trắng và không chứa khoảng trắng bên trong.
- Lần đọc đầu tiên
Cách khắc phục cho tên có khoảng trắng: Chúng ta cần đọc toàn bộ tên (có thể chứa khoảng trắng) bằng
getline
, sau đó mới đọc tuổi. Tuy nhiên, sau khi đọc tuổi bằng>>
, ký tự xuống dòng vẫn còn lại trong luồng, gây ảnh hưởng đếngetline
tiếp theo. Chúng ta cần "tiêu thụ" ký tự xuống dòng đó.
Mã nguồn (Phiên bản 2 - Xử lý tên có khoảng trắng):
#include <iostream>
#include <fstream>
#include <string>
#include <limits>
using namespace std;
int main() {
ifstream i("du_lieu_hoc_sinh.txt");
if (i.is_open()) {
string t;
int u;
while (getline(i, t, ' ')) {
if (i >> u) {
cout << "Hoc sinh: " << t << ", Tuoi: " << u << endl;
i.ignore(numeric_limits<streamsize>::max(), '\n');
} else {
cerr << "Loi doc tuoi cho hoc sinh: " << t << endl;
break;
}
}
i.close();
cout << "Da doc xong du lieu hoc sinh tu file (su dung getline ket hop >>)." << endl;
} else {
cerr << "Khong the mo file du_lieu_hoc_sinh.txt de doc." << endl;
}
return 0;
}
Output:
Hoc sinh: Nguyen, Tuoi: 15
Hoc sinh: Tran, Tuoi: 16
Hoc sinh: Le, Tuoi: 15
Da doc xong du lieu hoc sinh tu file (su dung getline ket hop >>).
Giải thích (Phiên bản 2):
- Cách này phức tạp hơn.
getline(inputFile, ten, ' ')
đọc dữ liệu vàoten
cho đến khi gặp ký tự khoảng trắng' '
. Điều này chỉ đọc được từ đầu dòng đến khoảng trắng đầu tiên. Đây vẫn chưa phải cách tốt nhất để đọc tên có nhiều từ.
Mã nguồn (Phiên bản 3 - Cách tốt hơn cho tên có khoảng trắng):
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#include <stdexcept>
using namespace std;
int main() {
ifstream i("du_lieu_hoc_sinh.txt");
if (i.is_open()) {
string d, t;
int u;
while (getline(i, d)) {
size_t vt = d.rfind(' ');
if (vt != string::npos) {
string sTuoi = d.substr(vt + 1);
try {
u = stoi(sTuoi);
t = d.substr(0, vt);
cout << "Hoc sinh: " << t << ", Tuoi: " << u << endl;
} catch (const invalid_argument& e) {
cerr << "Loi chuyen doi: " << d << endl;
} catch (const out_of_range& e) {
cerr << "Loi pham vi: " << d << endl;
}
} else {
cerr << "Loi dinh dang: " << d << endl;
}
}
i.close();
cout << "Da doc xong du lieu hoc sinh tu file (phan tich dong)." << endl;
} else {
cerr << "Khong the mo file du_lieu_hoc_sinh.txt de doc." << endl;
}
return 0;
}
Output:
Hoc sinh: Nguyen Van A, Tuoi: 15
Hoc sinh: Tran Thi B, Tuoi: 16
Hoc sinh: Le Van C, Tuoi: 15
Da doc xong du lieu hoc sinh tu file (phan tich dong).
Giải thích (Phiên bản 3):
- Đây là cách linh hoạt hơn khi làm việc với dữ liệu có cấu trúc trên mỗi dòng.
getline(inputFile, line)
vẫn đọc toàn bộ dòng vào biếnline
.stringstream ss(line);
tạo một luồng "trong bộ nhớ" từ chuỗiline
. Chúng ta có thể sử dụng toán tử>>
vàgetline
trênstringstream
này giống như làm vớiifstream
.- Cách phân tích chuỗi
line
để lấy tên và tuổi phụ thuộc rất nhiều vào định dạng chính xác của file. Trong ví dụ trên, tôi minh họa một cách là tìm khoảng trắng cuối cùng, giả định tuổi là số đứng sau khoảng trắng cuối cùng, và phần còn lại ở đầu dòng là tên. Cách này phức tạp hơn nhưng cần thiết khi định dạng dữ liệu không đơn giản. - Sử dụng
try-catch
vớistoi
để xử lý lỗi nếu phần tuổi không phải là một số hợp lệ.
Bài tập này cho thấy việc đọc dữ liệu có cấu trúc đòi hỏi bạn phải hiểu rõ định dạng của file và chọn cách đọc (dùng >>
, getline
, hay kết hợp, hoặc phân tích chuỗi) cho phù hợp.
Bài tập 5: Ghi tiếp dữ liệu (Append)
Khi ghi file, đôi khi bạn muốn thêm nội dung vào cuối file thay vì xóa bỏ nội dung cũ. Chế độ ios::app
(append) sẽ làm điều này.
Yêu cầu: Thêm thông tin của một học sinh khác vào cuối file du_lieu_hoc_sinh.txt
mà không làm mất dữ liệu cũ.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
ofstream o("du_lieu_hoc_sinh.txt", ios::out | ios::app);
if (o.is_open()) {
string t = "Pham Van D";
int u = 14;
o << t << " " << u << endl;
o.close();
cout << "Da them du lieu hoc sinh moi vao cuoi file." << endl;
} else {
cerr << "Khong the mo file du_lieu_hoc_sinh.txt de ghi them." << endl;
}
return 0;
}
Output:
Da them du lieu hoc sinh moi vao cuoi file.
Giải thích:
- Phần then chốt ở đây là mode mở file:
ios::out | ios::app
. Dấu gạch đứng|
là toán tử OR bitwise, dùng để kết hợp các chế độ mở file.ios::out
: Cho phép ghi vào file.ios::app
: Đảm bảo rằng tất cả các thao tác ghi sẽ diễn ra ở cuối file, bất kể con trỏ ghi hiện tại đang ở đâu. Nó cũng tự động tạo file nếu chưa tồn tại.
- Khi mở file với
ios::app
, nếu file đã có nội dung, nội dung mới sẽ được thêm vào sau nội dung cũ. Nếu file chưa tồn tại, nó sẽ được tạo mới và nội dung sẽ được ghi vào đó. - Các thao tác ghi (
<<
) sau đó diễn ra bình thường.
Sau khi chạy chương trình này, mở file du_lieu_hoc_sinh.txt
, bạn sẽ thấy thông tin của "Pham Van D 14" đã được thêm vào cuối file.
Bài tập 6: Kiểm tra sự tồn tại và quyền truy cập file
Trước khi cố gắng đọc hoặc ghi file, việc kiểm tra xem file có tồn tại và chương trình có quyền truy cập hay không là rất quan trọng để tránh các lỗi không mong muốn. Mặc dù is_open()
kiểm tra xem việc mở có thành công không, đôi khi bạn muốn kiểm tra trước đó.
Yêu cầu: Viết một đoạn code kiểm tra xem file chao_the_gioi.txt
có thể mở để đọc hay không.
Mã nguồn:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
string ten = "chao_the_gioi.txt";
ifstream f(ten);
if (f.is_open()) {
cout << "File '" << ten << "' co the mo de doc." << endl;
f.close();
} else {
cerr << "Khong the mo file '" << ten << "' de doc." << endl;
cerr << "Nguyen nhan co the la: file khong ton tai, khong co quyen doc, hoac loi khac." << endl;
}
string ten_ghi = "file_moi_de_ghi.txt";
ofstream o(ten_ghi);
if (o.is_open()) {
cout << "File '" << ten_ghi << "' co the mo de ghi." << endl;
o.close();
} else {
cerr << "Khong the mo file '" << ten_ghi << "' de ghi." << endl;
}
return 0;
}
Output:
File 'chao_the_gioi.txt' co the mo de doc.
File 'file_moi_de_ghi.txt' co the mo de ghi.
Giải thích:
- Cách đơn giản nhất để kiểm tra xem file có thể mở (nghĩa là nó tồn tại và có quyền truy cập phù hợp) là thử mở nó.
- Chúng ta tạo một đối tượng luồng (
ifstream
hoặcofstream
) và cố gắng mở file. - Sau đó, gọi phương thức
is_open()
. Nếu nó trả vềtrue
, việc mở file đã thành công. Nếu làfalse
, có vấn đề. - Quan trọng: Sau khi kiểm tra xong bằng cách mở file, nếu việc mở thành công, bạn nên đóng file ngay lập tức nếu bạn không định sử dụng luồng đó cho thao tác đọc/ghi tiếp theo. Việc giữ file mở khi không cần thiết có thể gây ra lỗi cho các chương trình khác hoặc chính chương trình của bạn sau này.
- Lưu ý rằng khi bạn mở một
ofstream
với chế độ mặc định (ios::out
), nếu file không tồn tại, nó sẽ được tạo. Nếu bạn chỉ muốn kiểm tra xem file có tồn tại hay không mà không muốn tạo mới, cách này không hoàn toàn phù hợp. Để kiểm tra sự tồn tại mà không tạo file, bạn có thể cần đến các hàm của hệ điều hành (ví dụ:stat
trên Linux/macOS,GetFileAttributes
trên Windows) hoặc đơn giản là thử mở vớiios::in
(chỉ để đọc) và kiểm trais_open()
.
Kết luận
Qua các bài tập thực hành này, hy vọng bạn đã cảm thấy tự tin hơn khi làm việc với file trong C++. Chúng ta đã cùng nhau thực hiện các thao tác:
- Ghi văn bản đơn giản vào file.
- Thêm nội dung vào cuối file.
- Đọc nội dung file theo từng dòng.
- Ghi và đọc dữ liệu có cấu trúc (và thấy được những thách thức khi định dạng phức tạp).
- Kiểm tra khả năng mở file.
Xử lý file là một chủ đề rộng lớn, còn nhiều chế độ mở file khác (ios::binary
, ios::trunc
, v.v.) và các phương thức nâng cao hơn. Tuy nhiên, những kỹ năng cơ bản được thực hành hôm nay sẽ là nền tảng vững chắc cho bạn.
Hãy thử tự mình viết thêm các chương trình nhỏ:
- Đếm số dòng trong một file.
- Đếm số từ trong một file.
- Sao chép nội dung từ file này sang file khác.
- Ghi và đọc các đối tượng (cần serialization, một chủ đề nâng cao hơn).
Comments