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> // Bao gồm thư viện xử lý file
#include <string>
int main() {
// 1. Tạo đối tượng ofstream để ghi file
ofstream outFile;
// 2. Mở file để ghi. Nếu file chưa tồn tại, nó sẽ được tạo.
// ios::out là mode mặc định khi dùng ofstream, nhưng viết rõ ra cũng tốt
outFile.open("chao_the_gioi.txt", ios::out);
// 3. Kiểm tra xem file đã được mở thành công chưa
if (outFile.is_open()) {
// 4. Ghi dữ liệu vào file sử dụng toán tử << giống như cout
outFile << "Xin chao, day la C++!" << endl;
outFile << "Chung ta dang hoc xu ly file." << endl;
// 5. Đóng file sau khi hoàn thành
outFile.close();
cout << "Da ghi du lieu vao file chao_the_gioi.txt" << endl;
} else {
// Xử lý trường hợp không mở được file
cerr << "Khong the mo file de ghi." << endl;
}
return 0;
}
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> // Thư viện xử lý file
#include <string> // Để sử dụng string và getline
int main() {
// 1. Tạo đối tượng ifstream để đọc file
ifstream inputFile;
// 2. Mở file để đọc.
// ios::in là mode mặc định khi dùng ifstream.
inputFile.open("chao_the_gioi.txt", ios::in);
// 3. Kiểm tra xem file đã được mở thành công chưa
if (inputFile.is_open()) {
string line; // Biến để lưu từng dòng đọc được
// 4. Đọc file từng dòng cho đến khi hết file
// getline(luong_doc, bien_string) đọc một dòng (đến ký tự xuống dòng)
// và trả về true nếu đọc thành công, false nếu gặp lỗi hoặc hết file.
while (getline(inputFile, line)) {
// 5. In dòng vừa đọc ra màn hình
cout << line << endl;
}
// 6. Đóng file sau khi hoàn thành
inputFile.close();
} else {
// Xử lý trường hợp không mở được file (ví dụ: file không tồn tại)
cerr << "Khong the mo file de doc. Kiem tra file chao_the_gioi.txt co ton tai khong?" << endl;
}
return 0;
}
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>
int main() {
ofstream outFile("du_lieu_hoc_sinh.txt"); // Cách mở file trực tiếp khi tạo đối tượng
if (outFile.is_open()) {
// Ghi dữ liệu của học sinh 1
string ten1 = "Nguyen Van A";
int tuoi1 = 15;
outFile << ten1 << " " << tuoi1 << endl; // Ghi tên, khoảng trắng, tuổi, xuống dòng
// Ghi dữ liệu của học sinh 2
string ten2 = "Tran Thi B";
int tuoi2 = 16;
outFile << ten2 << " " << tuoi2 << endl;
// Ghi dữ liệu của học sinh 3
string ten3 = "Le Van C";
int tuoi3 = 15;
outFile << ten3 << " " << tuoi3 << endl;
outFile.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;
}
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>
int main() {
ifstream inputFile("du_lieu_hoc_sinh.txt"); // Mở file để đọc
if (inputFile.is_open()) {
string ten;
int tuoi;
// Đọc dữ liệu theo cặp (tên, tuổi) cho đến khi hết file
// Toán tử >> sẽ tự động phân tách dữ liệu dựa trên khoảng trắng
while (inputFile >> ten >> tuoi) {
// In dữ liệu đã đọc
cout << "Hoc sinh: " << ten << ", Tuoi: " << tuoi << endl;
}
inputFile.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;
}
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> // Cần cho numeric_limits
int main() {
ifstream inputFile("du_lieu_hoc_sinh.txt"); // Mở file để đọc
if (inputFile.is_open()) {
string ten;
int tuoi;
// Vòng lặp đọc từng dòng
// Do định dạng file là Tên Tuổi trên mỗi dòng
while (getline(inputFile, ten, ' ')) { // Đọc tên đến khoảng trắng đầu tiên
// Sau khi đọc tên đến khoảng trắng, phần còn lại của dòng là tuổi
// Chúng ta cần đọc tuổi và ký tự xuống dòng
if (inputFile >> tuoi) { // Đọc tuổi
cout << "Hoc sinh: " << ten << ", Tuoi: " << tuoi << endl;
// Bỏ qua phần còn lại của dòng (ký tự xuống dòng)
// để getline tiếp theo hoạt động đúng
inputFile.ignore(numeric_limits<streamsize>::max(), '\n');
} else {
// Xử lý lỗi nếu không đọc được tuổi (ví dụ: cuối file đột ngột)
cerr << "Loi doc tuoi cho hoc sinh: " << ten << endl;
break; // Thoát vòng lặp nếu có lỗi đọc tuổi
}
}
// Lưu ý: Cách này vẫn chưa hoàn hảo nếu tên có nhiều hơn 1 từ cách nhau bởi khoảng trắng
// Một cách tiếp cận tốt hơn là đọc toàn bộ dòng bằng getline, sau đó phân tích dòng đó
// Nhưng ví dụ này minh họa cách kết hợp >> và getline.
inputFile.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;
}
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> // Cần cho stringstream
int main() {
ifstream inputFile("du_lieu_hoc_sinh.txt"); // Mở file để đọc
if (inputFile.is_open()) {
string line; // Biến để đọc toàn bộ dòng
string ten;
int tuoi;
// Đọc từng dòng của file
while (getline(inputFile, line)) {
// Sử dụng stringstream để phân tích dòng vừa đọc
stringstream ss(line);
// Đọc tên (toàn bộ phần đầu cho đến số tuổi) và tuổi từ stringstream
// Cách này yêu cầu định dạng file cụ thể: tên (có thể có khoảng trắng) theo sau là tuổi.
// Có thể cần một dấu phân cách rõ ràng hơn nếu tên và tuổi không có cấu trúc cố định.
// Ví dụ đơn giản: Đọc tên đến khoảng trắng cuối cùng trước số.
// Cách chắc chắn nhất là tìm vị trí của tuổi (ví dụ: luôn là số cuối cùng trên dòng).
// Đối với định dạng "Tên Tuổi" giả định, chúng ta có thể thử đọc tuổi trước
// hoặc phân tích chuỗi tìm số.
// Một cách đơn giản hóa cho định dạng "Tên Tuổi" (tên có thể có khoảng trắng):
// Đọc tuổi từ cuối dòng trước, sau đó lấy phần còn lại làm tên.
// Hoặc, nếu tuổi luôn là số nguyên cuối cùng, chúng ta có thể làm thế này:
size_t lastSpacePos = line.rfind(' '); // Tìm vị trí khoảng trắng cuối cùng
if (lastSpacePos != string::npos) {
// Trích xuất tuổi từ phần sau khoảng trắng cuối cùng
string tuoiStr = line.substr(lastSpacePos + 1);
try {
tuoi = stoi(tuoiStr); // Chuyển đổi chuỗi tuổi sang số nguyên
// Trích xuất tên từ phần trước khoảng trắng cuối cùng
ten = line.substr(0, lastSpacePos);
cout << "Hoc sinh: " << ten << ", Tuoi: " << tuoi << endl;
} catch (const invalid_argument& ia) {
cerr << "Loi: Khong chuyen doi duoc tuoi tu chuoi: " << tuoiStr << " tren dong: " << line << endl;
} catch (const out_of_range& oor) {
cerr << "Loi: Tuoi qua lon/nho: " << tuoiStr << " tren dong: " << line << endl;
}
} else {
cerr << "Loi: Dong khong co dinh dang mong muon (thieu khoang trang): " << line << endl;
}
}
inputFile.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;
}
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>
int main() {
// Mở file ở chế độ ghi (out) và nối thêm (app)
ofstream outFile("du_lieu_hoc_sinh.txt", ios::out | ios::app);
if (outFile.is_open()) {
// Ghi dữ liệu của học sinh mới
string ten4 = "Pham Van D";
int tuoi4 = 14;
outFile << ten4 << " " << tuoi4 << endl; // Ghi tên, khoảng trắng, tuổi, xuống dòng
outFile.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;
}
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>
int main() {
string tenFile = "chao_the_gioi.txt";
// Tạo đối tượng ifstream và thử mở file
ifstream testFile(tenFile);
// is_open() sẽ cho biết việc mở file có thành công không
if (testFile.is_open()) {
cout << "File '" << tenFile << "' co the mo de doc." << endl;
testFile.close(); // Đóng file ngay sau khi kiểm tra thành công
} else {
cerr << "Khong the mo file '" << tenFile << "' de doc." << endl;
cerr << "Nguyen nhan co the la: file khong ton tai, khong co quyen doc, hoac loi khac." << endl;
}
string tenFileGhi = "file_moi_de_ghi.txt";
// Tạo đối tượng ofstream và thử mở file để ghi
ofstream testFileGhi(tenFileGhi); // Mặc định là ios::out
if (testFileGhi.is_open()) {
cout << "File '" << tenFileGhi << "' co the mo de ghi." << endl;
testFileGhi.close(); // Đóng file ngay sau khi kiểm tra thành công
// Lưu ý: Mở ofstream mặc định sẽ tạo file nếu nó không tồn tại.
// Đoạn code này sẽ tạo ra một file rỗng tên là file_moi_de_ghi.txt nếu chạy thành công.
} else {
cerr << "Khong the mo file '" << tenFileGhi << "' de ghi." << endl;
}
return 0;
}
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