Bài 37.3: Ghi file trong C++

Chào mừng các bạn đến với hành trình khám phá thế giới C++ cùng FullhouseDev! Sau khi đã thành thạo việc nhập và xuất dữ liệu trên màn hình console, đã đến lúc chúng ta nâng cấp khả năng của chương trình bằng cách cho phép nó lưu trữ dữ liệu một cách lâu dài – tức là, ghi dữ liệu vào các tập tin (file). Đây là một kỹ năng cực kỳ quan trọng, mở ra cánh cửa cho việc xây dựng các ứng dụng thực tế hơn, từ việc lưu cài đặt, ghi lại nhật ký hoạt động (log), đến xử lý các tập dữ liệu lớn.

Trong bài viết này, chúng ta sẽ tập trung vào việc ghi dữ liệu vào file văn bản (text file) trong C++. Khái niệm cơ bản rất giống với việc sử dụng cout để in ra màn hình, nhưng thay vì gửi dữ liệu đến console, chúng ta sẽ gửi nó đến một đối tượng đại diện cho file trên ổ đĩa.

Tại sao phải ghi file?

Hãy tưởng tượng bạn viết một chương trình quản lý danh sách sinh viên. Nếu dữ liệu chỉ tồn tại trong bộ nhớ khi chương trình đang chạy, mọi thông tin sẽ biến mất ngay khi bạn đóng chương trình. Thật bất tiện phải không? Ghi file giúp chúng ta:

  • Lưu trữ lâu dài: Dữ liệu tồn tại ngay cả khi chương trình kết thúc.
  • Chia sẻ dữ liệu: File có thể được mở và đọc bởi các chương trình khác hoặc thậm chí là con người (đối với file văn bản).
  • Xử lý lượng lớn dữ liệu: Dễ dàng làm việc với dữ liệu không thể chứa hết trong bộ nhớ.

Thư viện cần thiết: <fstream> và Đối tượng ofstream

Để làm việc với file trong C++, chúng ta cần sử dụng thư viện <fstream> (file stream). Thư viện này cung cấp các lớp để thao tác với file như là các luồng (stream) dữ liệu, tương tự như cincout.

Đối tượng chính mà chúng ta sẽ sử dụng để ghi fileofstream. Chữ 'of' trong ofstream là viết tắt của "output file", nghĩa là luồng dữ liệu đầu ra đến một file.

Các bước cơ bản để ghi dữ liệu vào file là:

  1. Khai báo một đối tượng ofstream.
  2. Mở file (liên kết đối tượng ofstream với một file cụ thể trên ổ đĩa).
  3. Kiểm tra xem file có mở thành công không.
  4. Ghi dữ liệu vào file bằng cách sử dụng toán tử <<.
  5. Đóng file.

Nghe có vẻ đơn giản đúng không? Hãy đi sâu vào từng bước và xem các ví dụ cụ thể.

Bước 1 & 2: Khai báo và Mở file

Bạn có thể khai báo và mở file cùng lúc ngay khi tạo đối tượng ofstream, hoặc khai báo trước rồi mở sau.

Cách 1: Mở file bằng constructor (phổ biến nhất)

#include <fstream>
#include <iostream>

int main() {
    ofstream f("output.txt");
    return 0;
}

Khi bạn tạo ofstream outFile("output.txt");, chương trình sẽ cố gắng mở hoặc tạo một file có tên output.txt trong cùng thư mục với file thực thi của chương trình (hoặc theo đường dẫn đầy đủ nếu bạn cung cấp).

  • Nếu file output.txt chưa tồn tại, nó sẽ được tạo mới.
  • Nếu file output.txt đã tồn tại, toàn bộ nội dung cũ sẽ bị xóa sạch trước khi ghi dữ liệu mới vào (đây là chế độ mặc định - ios::out).

Cách 2: Mở file bằng phương thức open()

Cách này hữu ích khi bạn cần làm việc với nhiều file hoặc muốn trì hoãn việc mở file.

#include <fstream>
#include <iostream>

int main() {
    ofstream f;
    f.open("another_output.txt");
    return 0;
}

Bước 3: Kiểm tra xem file có mở thành công không

Đây là bước cực kỳ quan trọng mà nhiều người mới bắt đầu hay bỏ qua. Việc mở file có thể thất bại vì nhiều lý do (ví dụ: không đủ quyền ghi, tên file không hợp lệ, ổ đĩa đầy...). Nếu bạn cố gắng ghi vào một file không được mở thành công, chương trình có thể gặp lỗi hoặc hành vi không mong muốn.

Cách đơn giản nhất để kiểm tra là sử dụng toán tử ! hoặc phương thức is_open().

#include <fstream>
#include <iostream>

int main() {
    ofstream f("output.txt");

    if (!f.is_open()) {
        cerr << "Lỗi: Không thể mở file." << endl;
        return 1;
    }

    cout << "File output.txt đã mở." << endl;

    f.close();
    return 0;
}

Output:

File output.txt đã mở.

Bước 4: Ghi dữ liệu vào file

Khi file đã được mở thành công, bạn có thể sử dụng toán tử << để ghi dữ liệu vào file, giống hệt như cách bạn sử dụng với cout. Bạn có thể ghi các kiểu dữ liệu khác nhau như chuỗi (string), số nguyên (int), số thực (double), v.v.

#include <fstream>
#include <iostream>
#include <string>

int main() {
    ofstream f("my_data.txt");

    if (!f.is_open()) {
        cerr << "Lỗi: Không thể mở file my_data.txt." << endl;
        return 1;
    }

    f << "Xin chào từ C++!" << endl;

    int s = 100;
    f << "Gia tri so: " << s << endl;

    double p = 3.14159;
    f << "Gia tri Pi: " << p << endl;

    string t = "Alice";
    int u = 25;
    f << "Ten: " << t << ", Tuoi: " << u << endl;

    f.close();
    cout << "Da ghi du lieu vao file my_data.txt." << endl;

    return 0;
}

Output:

Da ghi du lieu vao file my_data.txt.

Giải thích:

  • outFile << "..." hoạt động tương tự cout << "...".
  • Toán tử << được quá tải (overloaded) để xử lý nhiều kiểu dữ liệu khác nhau.
  • endl không chỉ chèn ký tự xuống dòng (\n) mà còn thực hiện thao tác flush luồng đệm (buffer). Điều này đảm bảo rằng dữ liệu bạn vừa ghi sẽ được đẩy ngay lập tức từ bộ nhớ đệm của chương trình ra file trên ổ đĩa. Điều này quan trọng nếu bạn muốn đảm bảo dữ liệu được lưu ngay lập tức, nhưng có thể làm chậm chương trình nếu thực hiện quá thường xuyên. Chỉ chèn \n thường hiệu quả hơn nếu không cần flush ngay.

Bước 5: Đóng file

Sau khi hoàn thành việc ghi dữ liệu, bạn phải đóng file. Việc đóng file có một số mục đích quan trọng:

  • Đẩy dữ liệu còn lại: Đảm bảo mọi dữ liệu còn sót lại trong bộ nhớ đệm của luồng được ghi ra file trên ổ đĩa.
  • Giải phóng tài nguyên hệ thống: Hệ điều hành giới hạn số lượng file mà một chương trình có thể mở cùng lúc. Đóng file sẽ giải phóng tài nguyên này.
  • Hoàn tất quá trình ghi: Một số hệ điều hành có thể không cập nhật file hoàn chỉnh cho đến khi nó được đóng.

Bạn đóng file bằng cách gọi phương thức close() của đối tượng ofstream.

#include <fstream>
#include <iostream>

int main() {
    ofstream f("temp.txt");

    if (!f.is_open()) {
        cerr << "Lỗi: Không thể mở file temp.txt." << endl;
        return 1;
    }

    f << "Day la du lieu tam thoi." << endl;

    f.close();

    cout << "Da ghi va dong file temp.txt." << endl;

    return 0;
}

Output:

Da ghi va dong file temp.txt.

Lưu ý quan trọng: Đối tượng ofstream được thiết kế theo nguyên tắc RAII (Resource Acquisition Is Initialization). Điều này có nghĩa là khi đối tượng outFile bị hủy (ví dụ: khi nó ra khỏi phạm vi hàm main), destructor của nó sẽ tự động gọi close(). Do đó, trong nhiều trường hợp đơn giản, bạn không cần gọi close() một cách tường minh. Tuy nhiên, việc gọi close() tường minh đôi khi giúp bạn kiểm tra xem việc đóng file có thành công không (mặc dù việc kiểm tra lỗi khi đóng file ít phổ biến hơn so với khi mở file) và giúp mã nguồn rõ ràng hơn về thời điểm tài nguyên file được giải phóng.

Ghi dữ liệu vào cuối file (Append Mode)

Như đã đề cập, chế độ mặc định khi mở file bằng ofstream là ghi đè (xóa sạch nội dung cũ). Nếu bạn muốn thêm dữ liệu vào cuối file mà không làm mất nội dung đã có, bạn cần mở file ở chế độ nối thêm (append mode) bằng cách sử dụng cờ ios::app.

Bạn truyền cờ này làm tham số thứ hai cho constructor hoặc phương thức open().

#include <fstream>
#include <iostream>
#include <string>
#include <chrono>
#include <ctime>

int main() {
    ofstream f("application.log", ios::app);

    if (!f.is_open()) {
        cerr << "Loi: Khong mo duoc file application.log." << endl;
        return 1;
    }

    auto tg = chrono::system_clock::now();
    time_t t = chrono::system_clock::to_time_t(tg);
    char str_tg[100];
    strftime(str_tg, sizeof(str_tg), "%Y-%m-%d %H:%M:%S", localtime(&t));

    f << "[" << str_tg << "] Ung dung da chay." << endl;
    f << "[" << str_tg << "] Ghi them mot dong log." << endl;

    f.close();

    cout << "Da ghi log vao file application.log." << endl;

    return 0;
}

Output:

Da ghi log vao file application.log.

Giải thích:

  • ofstream logFile("application.log", ios::app);: Tham số thứ hai ios::app yêu cầu luồng file mở file ở chế độ nối thêm. Con trỏ ghi sẽ được đặt ở cuối file ngay khi mở.
  • Nếu file application.log chưa tồn tại, chế độ ios::app vẫn sẽ tạo mới file đó.
  • Ví dụ này minh họa việc sử dụng ios::app để ghi log, một ứng dụng rất phổ biến của chế độ nối thêm. Chúng ta cũng thêm thông tin thời gian để làm cho log hữu ích hơn.

Tóm kết (cho phần này)

Ghi file trong C++ với <fstream>ofstream là một quá trình trực quan, tương tự như sử dụng cout. Các bước cốt lõi bao gồm:

  1. Bao gồm header <fstream>.
  2. Tạo đối tượng ofstream và mở file (truyền tên file và optionally cờ mode như ios::app).
  3. Luôn kiểm tra xem file có mở thành công không (!outFile.is_open()).
  4. Sử dụng toán tử << để ghi dữ liệu.
  5. Đóng file (outFile.close()) khi hoàn thành hoặc để destructor tự xử lý.

Thành thạo việc ghi file là bước đệm vững chắc để bạn tiếp tục khám phá các thao tác file phức tạp hơn, bao gồm cả việc đọc file, xử lý file nhị phân và cấu trúc file phức tạp hơn.

Comments

There are no comments at the moment.