Trong bài này chúng ta sẽ học đọc ghi file trong C++ với thư viện <fstream>: ghi file, ghi thêm vào cuối, đọc từng dòng, kiểm tra file mở được không, và lưu, đọc cả danh sách sách dưới dạng file CSV. Tới giờ dữ liệu của chương trình thư viện mất sạch mỗi khi tắt chương trình, đọc ghi file giải quyết đúng việc đó.
Bài 27 trong Serie Lập trình C++ từ A tới Z
Stream: đọc ghi file trong C++ giống nhập xuất màn hình
C++ coi màn hình, bàn phím và file đều là stream (luồng dữ liệu). Thư viện <fstream> có 2 lớp chính:
std::ofstream(output file stream): ghi dữ liệu ra file.std::ifstream(input file stream): đọc dữ liệu từ file.
Điều hay là chúng dùng đúng các toán tử <<, >> và hàm std::getline như std::cout và std::cin. Viết file << "..." y như std::cout << "...", chỉ khác là chữ đi vào file thay vì hiện lên màn hình. Toán tử << mà chúng ta nạp chồng cho class ở bài nạp chồng toán tử cũng ghi được thẳng ra file luôn.
Ghi file với ofstream
#include <fstream>
#include <iostream>
int main()
{
std::ofstream file("nhat_ky.txt"); // mo de ghi, file cu bi xoa
if (!file)
{
std::cout << "Khong mo duoc file\n";
return 1;
}
file << "An muon Lap trinh C++\n";
file << "Binh muon Clean Code\n";
std::cout << "Da ghi xong\n";
return 0; // file tu dong dong khi ra khoi ham
}
std::ofstream file("nhat_ky.txt")mở file để ghi. File chưa có thì tạo mới, đã có thì nội dung cũ bị xóa sạch.if (!file)kiểm tra mở được không. Ghi có thể thất bại khi thư mục không tồn tại, không có quyền ghi, hoặc ổ đĩa đầy.- Không cần gọi
close(): khi biếnfilera khỏi phạm vi, hàm hủy tự đóng file. Đây là RAII như bài xử lý ngoại lệ trong C++, kể cả khi có ngoại lệ thì file vẫn được đóng đúng.
Ghi thêm và đọc từng dòng
Muốn ghi tiếp vào cuối file thay vì xóa nội dung cũ, mở với chế độ std::ios::app (append):
#include <fstream>
#include <iostream>
int main()
{
std::ofstream file("nhat_ky.txt", std::ios::app); // mo de ghi them vao cuoi
file << "Chi tra Clean Code\n";
std::cout << "Da ghi them\n";
return 0;
}
Đọc file từng dòng bằng std::ifstream và std::getline:
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ifstream file("nhat_ky.txt");
if (!file)
{
std::cout << "Khong mo duoc file\n";
return 1;
}
std::string dong;
int soDong = 0;
while (std::getline(file, dong)) // doc tung dong toi het file
{
soDong++;
std::cout << soDong << ": " << dong << '\n';
}
return 0;
}
while (std::getline(file, dong)) là mẫu chuẩn để đọc file theo dòng. getline trả về chính stream, và stream được coi là false khi lần đọc vừa rồi thất bại, tức là đã hết file. Vòng lặp dừng đúng lúc, không thừa không thiếu dòng nào.
Luôn kiểm tra file có mở được không
Mở một file không tồn tại để đọc thì C++ không báo lỗi gì cả:
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ifstream file("kho_sach.csv");
std::string dong;
std::getline(file, dong);
std::cout << "Dong dau tien: '" << dong << "'\n";
if (!file.is_open())
std::cout << "Thuc ra file kho_sach.csv khong ton tai\n";
return 0;
}
Chương trình chạy bình thường, đọc ra chuỗi rỗng. Nếu không kiểm tra, các bạn sẽ tưởng file trống chứ không phải file không có. Đây là lỗi rất hay gặp khi chạy chương trình ở thư mục khác: đường dẫn tương đối "kho_sach.csv" được tính từ thư mục đang đứng khi chạy, không phải thư mục chứa file .cpp.
Lưu ý: Ngay sau khi mở file, luôn kiểm tra if (!file) hoặc if (!file.is_open()). Chương trình của các bạn chạy đúng trong VS Code nhưng đọc file rỗng khi chạy từ chỗ khác thì gần như chắc chắn là sai thư mục làm việc.
Lỗi kinh điển: vòng lặp while (!file.eof())
Rất nhiều tài liệu dạy đọc file bằng while (!file.eof()). Thử đọc file chứa 3 điểm 8, 7, 9:
#include <fstream>
#include <iostream>
int main()
{
{
std::ofstream ghi("diem.txt");
ghi << "8\n7\n9\n";
}
std::ifstream file("diem.txt");
int diem, tong = 0, dem = 0;
while (!file.eof()) // sai: eof chi dung SAU khi doc that bai
{
file >> diem;
tong += diem;
dem++;
}
std::cout << "Doc " << dem << " diem, tong = " << tong << '\n';
return 0;
}
File có 3 điểm, tổng 24, nhưng chương trình đọc ra 4 điểm và tổng 33: điểm 9 bị cộng 2 lần. Sau khi đọc số 9, vẫn còn ký tự \n cuối file nên eof() vẫn là false. Vòng lặp chạy thêm lần nữa, file >> diem thất bại và giữ nguyên giá trị cũ là 9, rồi bị cộng thêm.
eof() chỉ trở thành true sau khi một lần đọc đã thất bại, chứ không phải trước. Cách đúng là đặt chính lệnh đọc làm điều kiện: while (file >> diem) hoặc while (std::getline(file, dong)), như các ví dụ khác trong bài.
Lưu và đọc danh sách sách dạng CSV
CSV (Comma-Separated Values) là định dạng file văn bản, mỗi dòng là một bản ghi, các trường ngăn cách bằng dấu phẩy. Excel và Google Sheets mở được trực tiếp. Lưu kho sách của chúng ta ra CSV rồi đọc lại:
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
struct Sach
{
int ma;
std::string ten;
int soLuong;
};
bool luuKho(const std::vector<Sach>& kho, const std::string& tenFile)
{
std::ofstream file(tenFile);
if (!file)
return false;
for (const Sach& s : kho)
file << s.ma << ',' << s.ten << ',' << s.soLuong << '\n';
return true;
}
std::vector<Sach> docKho(const std::string& tenFile)
{
std::vector<Sach> kho;
std::ifstream file(tenFile);
std::string dong;
while (std::getline(file, dong))
{
std::stringstream ss(dong);
std::string ma, ten, soLuong;
std::getline(ss, ma, ','); // doc toi dau phay
std::getline(ss, ten, ',');
std::getline(ss, soLuong, ',');
kho.push_back({std::stoi(ma), ten, std::stoi(soLuong)});
}
return kho;
}
int main()
{
std::vector<Sach> kho = {{1001, "Clean Code", 2}, {1002, "Lap trinh C++", 5}};
kho.push_back({1003, "Cau truc du lieu", 1});
if (luuKho(kho, "kho_sach.csv"))
std::cout << "Da luu " << kho.size() << " dau sach\n";
std::vector<Sach> docLai = docKho("kho_sach.csv");
std::cout << "Doc lai tu file:\n";
for (const Sach& s : docLai)
std::cout << " [" << s.ma << "] " << s.ten << " - con " << s.soLuong << " cuon\n";
return 0;
}
- Ghi: mỗi cuốn sách một dòng, các trường nối bằng dấu phẩy.
- Đọc: đọc cả dòng bằng
getline, rồi đưa dòng đó vàostd::stringstream(thư viện<sstream>), một stream đọc từ chuỗi thay vì từ file.std::getline(ss, ma, ',')với tham số thứ 3 là ký tự ngăn cách, đọc tới dấu phẩy thì dừng. std::stoiđổi chuỗi sang số. File bị sửa tay sai định dạng thìstoinémstd::invalid_argument, nên chương trình thật cần bọc trongtrynhư bài xử lý ngoại lệ.
Cách làm này có một giới hạn: tên sách chứa dấu phẩy, ví dụ “Sach A, tap 2”, sẽ bị tách sai. CSV chuẩn xử lý bằng cách đặt trường trong ngoặc kép, nhưng phần đó vượt ra ngoài bài này. Với dữ liệu tự mình tạo ra, chọn ký tự ngăn cách ít gặp như | là cách đơn giản nhất.
File văn bản và file nhị phân
File trong bài này là file văn bản: mở bằng Notepad đọc được, số 1001 được lưu thành 4 ký tự '1' '0' '0' '1'. Ngoài ra còn file nhị phân, mở bằng std::ios::binary và ghi thẳng các byte trong bộ nhớ bằng file.write(...), nhỏ và nhanh hơn nhưng không đọc được bằng mắt, và phụ thuộc vào kích thước kiểu dữ liệu trên từng máy.
Với dữ liệu cấu hình, danh sách, nhật ký, file văn bản là lựa chọn an toàn. File nhị phân dùng cho ảnh, âm thanh, hoặc khi dung lượng thật sự quan trọng.
Bài tập
- Viết chương trình cho người dùng nhập nhiều dòng ghi chú, nhập dòng trống thì dừng, rồi ghi thêm tất cả vào file
ghi_chu.txt. - Đếm số dòng, số từ và số ký tự của một file văn bản, tên file nhập từ bàn phím. Báo lỗi rõ ràng nếu file không tồn tại.
- Sửa hàm
docKhođể bỏ qua các dòng sai định dạng (dùngtry/catchquanhstd::stoi) và in ra số dòng bị bỏ qua. - Đổi ký tự ngăn cách trong ví dụ CSV thành
|, thêm cột tác giả, và kiểm tra file đọc lại đúng.
Lời giải sẽ có trong bài tập tổng hợp của chương nhé.
Kết
Đọc ghi file trong C++ dùng std::ofstream để ghi, std::ifstream để đọc, với đúng các toán tử và hàm quen thuộc từ std::cout, std::cin. Ba điều cần nhớ: luôn kiểm tra file mở được không, đọc bằng while (std::getline(file, dong)) chứ không dùng eof(), và để RAII tự đóng file. Với std::stringstream, các bạn tách được dữ liệu trong từng dòng để lưu cả danh sách đối tượng.
Tham khảo thêm ở cppreference: std::basic_fstream. Bài tiếp theo chúng ta học lập trình đa luồng với std::thread, cho chương trình làm nhiều việc cùng lúc. Nếu thấy bài viết hữu ích, hãy chia sẻ cho bạn bè nhé.







