Bài 37.1: Giới thiệu xử lý file trong C++

Chào mừng bạn đến với bài viết tiếp theo trong chuỗi blog về C++! Hôm nay, chúng ta sẽ bước vào một chủ đề cực kỳ quan trọng: xử lý file. Tại sao lại quan trọng? Vì dữ liệu mà chương trình của bạn tạo ra hoặc sử dụng thường cần được lưu trữ một cách lâu dài, không chỉ tồn tại trong bộ nhớ khi chương trình đang chạy. Đây chính là lúc file phát huy vai trò của mình!

Xử lý file cho phép chương trình của bạn đọc dữ liệu từ các tập tin đã có sẵn trên máy tính, hoặc ghi dữ liệu mà chương trình xử lý ra thành các tập tin mới. Điều này mở ra cánh cửa cho rất nhiều ứng dụng thực tế, từ việc lưu cấu hình game, ghi log hoạt động, xử lý các tập tin văn bản lớn, cho đến làm việc với cơ sở dữ liệu đơn giản.

Trong C++, thư viện chuẩn cung cấp một bộ công cụ mạnh mẽ và linh hoạt để làm việc với file, tập trung chủ yếu trong header <fstream>. Chúng ta sẽ tìm hiểu về các lớp chính trong thư viện này và cách sử dụng chúng để thực hiện các thao tác đọc/ghi file cơ bản.

Thư viện <fstream> và các lớp cơ bản

Thư viện <fstream> là trái tim của việc xử lý file trong C++. Nó cung cấp các lớp được thiết kế để làm việc với luồng (stream) dữ liệu đi vào hoặc đi ra từ file, tương tự như cách bạn sử dụng cincout để làm việc với luồng vào/ra chuẩn (bàn phím/màn hình).

Ba lớp chính mà chúng ta sẽ làm quen là:

  1. ofstream (Output File Stream): Lớp này dùng để ghi dữ liệu ra file. Nếu file chưa tồn tại, nó sẽ được tạo mới. Nếu file đã tồn tại, nội dung cũ thường bị ghi đè (mặc định ở chế độ văn bản).
  2. ifstream (Input File Stream): Lớp này dùng để đọc dữ liệu từ file.
  3. fstream (File Stream): Lớp này có thể dùng để đọc và ghi dữ liệu vào cùng một file. Nó linh hoạt hơn nhưng thường được sử dụng cho các tình huống phức tạp hơn hoặc khi cần chuyển đổi giữa đọc và ghi trên cùng một luồng.

Nguyên tắc chung khi làm việc với file là:

  • Mở file: Kết nối chương trình của bạn với file trên ổ đĩa.
  • Thao tác: Đọc hoặc ghi dữ liệu.
  • Đóng file: Ngắt kết nối để đảm bảo dữ liệu được lưu trữ đúng cách và giải phóng tài nguyên hệ thống.
Ghi dữ liệu ra file với ofstream

Để ghi dữ liệu ra file, bạn cần tạo một đối tượng thuộc lớp ofstream. Bạn có thể truyền tên file (hoặc đường dẫn đầy đủ) trực tiếp vào constructor của đối tượng, hoặc tạo đối tượng rồi dùng phương thức open() sau.

Quan trọng: Sau khi mở file, bạn luôn phải kiểm tra xem việc mở file có thành công hay không trước khi cố gắng ghi dữ liệu. Nếu file không thể mở được (ví dụ: do thiếu quyền truy cập, đường dẫn sai, đĩa đầy...), các thao tác ghi sau đó sẽ thất bại âm thầm nếu bạn không kiểm tra.

Chúng ta sử dụng toán tử << để ghi dữ liệu, tương tự như khi dùng cout.

Hãy xem một ví dụ đơn giản:

#include <fstream> // Bao gồm thư viện fstream
#include <iostream> // Bao gồm thư viện iostream để in ra màn hình

int main() {
    // Tạo đối tượng ofstream và cố gắng mở file "output.txt" để ghi
    // Nếu file chưa tồn tại, nó sẽ được tạo. Nếu tồn tại, nội dung mặc định bị ghi đè.
    ofstream outFile("output.txt");

    // *** Bước 1: Kiểm tra xem file có mở thành công không ***
    if (outFile.is_open()) {
        // *** Bước 2: Nếu mở thành công, thực hiện ghi dữ liệu ***
        outFile << "Day la dong dau tien duoc ghi vao file.\n";
        outFile << "Day la dong thu hai.\n";
        outFile << "Ghi mot so nguyen: " << 123 << "\n";
        outFile << "Va mot so thuc: " << 45.67 << endl; // endl cũng tự động xuống dòng

        // *** Bước 3: Đóng file sau khi hoàn thành ***
        outFile.close();
        cout << "Du lieu da duoc ghi vao file output.txt\n";
    } else {
        // Nếu không mở được file
        cout << "Khong the mo file output.txt de ghi.\n";
    }

    return 0;
}

Giải thích code:

  • Chúng ta khai báo #include <fstream> để sử dụng các lớp xử lý file.
  • Đối tượng outFile kiểu ofstream được tạo, cố gắng mở file có tên "output.txt". File này sẽ được tạo trong cùng thư mục với file thực thi chương trình (nếu bạn không chỉ định đường dẫn đầy đủ).
  • Dòng if (outFile.is_open())cực kỳ quan trọng. Nó kiểm tra trạng thái của luồng file. Nếu is_open() trả về true, nghĩa là file đã sẵn sàng để ghi.
  • Bên trong khối if, chúng ta sử dụng toán tử << giống như với cout để ghi các chuỗi và số ra file.
  • outFile.close() đóng luồng file. Điều này đảm bảo mọi dữ liệu đệm (buffer) được ghi hoàn toàn xuống đĩa và giải phóng tài nguyên. Lưu ý rằng destructor của đối tượng ofstream cũng sẽ tự động gọi close() khi đối tượng ra khỏi phạm vi (scope), nhưng việc gọi close() tường minh là một practice tốt.
  • Nếu is_open() trả về false, chương trình in ra thông báo lỗi.

Sau khi chạy chương trình này, bạn sẽ thấy một file mới tên là output.txt xuất hiện trong thư mục làm việc của bạn, chứa nội dung mà chúng ta đã ghi.

Đọc dữ liệu từ file với ifstream

Tương tự như ghi, để đọc dữ liệu từ file, bạn cần tạo một đối tượng thuộc lớp ifstream.

Cách mở file và kiểm tra thành công cũng tương tự.

Để đọc dữ liệu, bạn có thể sử dụng toán tử >> (giống như cin) để đọc từng từ (ngăn cách bởi khoảng trắng, tab, hoặc xuống dòng), hoặc sử dụng hàm getline() để đọc từng dòng.

Hãy xem ví dụ đọc từng từ từ file output.txt mà chúng ta vừa tạo:

#include <fstream> // Bao gồm thư viện fstream
#include <iostream> // Bao gồm thư viện iostream
#include <string>   // Bao gồm thư viện string để sử dụng string

int main() {
    // Tạo đối tượng ifstream và cố gắng mở file "output.txt" để đọc
    ifstream inFile("output.txt");

    // Biến để lưu trữ từng từ đọc được
    string word;

    // *** Bước 1: Kiểm tra xem file có mở thành công không ***
    if (inFile.is_open()) {
        // *** Bước 2: Nếu mở thành công, thực hiện đọc dữ liệu ***
        cout << "Dang doc du lieu tu file output.txt (tung tu):\n";

        // Vòng lặp đọc từng từ cho đến khi gặp cuối file (End-of-File - EOF)
        while (inFile >> word) {
            cout << word << endl; // In từ vừa đọc ra màn hình
        }

        // *** Bước 3: Đóng file sau khi hoàn thành ***
        inFile.close();
        cout << "\nDa doc xong file output.txt\n";

    } else {
        // Nếu không mở được file (ví dụ: file không tồn tại)
        cout << "Khong the mo file output.txt de doc.\n";
    }

    return 0;
}

Giải thích code:

  • Chúng ta khai báo #include <fstream>#include <string>.
  • Đối tượng inFile kiểu ifstream được tạo, cố gắng mở file "output.txt".
  • Kiểm tra inFile.is_open() vẫn là bước thiết yếu.
  • Vòng lặp while (inFile >> word) là cách phổ biến để đọc dữ liệu từ luồng file. Toán tử >> sẽ đọc dữ liệu từ luồng inFile và lưu vào biến word. Nó dừng lại khi gặp khoảng trắng, tab hoặc ký tự xuống dòng. Quan trọng hơn, biểu thức (inFile >> word) sẽ trả về một giá trị có thể được đánh giá thành true miễn là việc đọc thành công (chưa gặp cuối file hoặc lỗi đọc). Khi gặp cuối file (EOF) hoặc có lỗi, biểu thức này sẽ trả về giá trị tương đương false, kết thúc vòng lặp.
  • cout << word << endl; in từng từ vừa đọc ra màn hình.
  • inFile.close() đóng luồng file.

Khi chạy chương trình này (sau khi đã chạy chương trình ghi file), bạn sẽ thấy nội dung của output.txt được in ra màn hình, mỗi từ trên một dòng riêng biệt.

Bây giờ, hãy thử đọc từng dòng thay vì từng từ, sử dụng getline():

#include <fstream> // Bao gồm thư viện fstream
#include <iostream> // Bao gồm thư viện iostream
#include <string>   // Bao gồm thư viện string để sử dụng string

int main() {
    // Tạo đối tượng ifstream và cố gắng mở file "output.txt" để đọc
    ifstream inFile("output.txt");

    // Biến để lưu trữ từng dòng đọc được
    string line;

    // *** Bước 1: Kiểm tra xem file có mở thành công không ***
    if (inFile.is_open()) {
        // *** Bước 2: Nếu mở thành công, thực hiện đọc dữ liệu ***
        cout << "Dang doc du lieu tu file output.txt (tung dong):\n";

        // Vòng lặp đọc từng dòng cho đến khi gặp cuối file (EOF)
        // getline đọc cả dòng, bao gồm cả khoảng trắng, cho đến ký tự xuống dòng '\n'
        while (getline(inFile, line)) {
            cout << line << endl; // In dòng vừa đọc ra màn hình
        }

        // *** Bước 3: Đóng file sau khi hoàn thành ***
        inFile.close();
        cout << "\nDa doc xong file output.txt\n";

    } else {
        // Nếu không mở được file
        cout << "Khong the mo file output.txt de doc.\n";
    }

    return 0;
}

Giải thích code:

  • Code này rất giống với ví dụ đọc từng từ, điểm khác biệt chính là cách đọc dữ liệu trong vòng lặp.
  • Chúng ta sử dụng getline(inFile, line). Hàm này đọc toàn bộ ký tự từ luồng inFile (bao gồm cả khoảng trắng) cho đến khi gặp ký tự xuống dòng (\n), hoặc gặp cuối file, và lưu trữ kết quả vào biến line kiểu string.
  • Cũng như toán tử >>, hàm getline trả về luồng inFile, biểu thức này được đánh giá là true khi việc đọc thành công và false khi gặp lỗi hoặc cuối file.
  • Kết quả in ra màn hình sẽ giữ nguyên cấu trúc dòng ban đầu của file output.txt.
Về fstream và các chế độ mở file

Lớp fstream có thể được sử dụng cho cả đọc và ghi trên cùng một đối tượng luồng. Khi mở file với fstream, bạn thường cần chỉ định rõ chế độ mở file bằng cách sử dụng các cờ (flags) từ ios_base::openmode.

Một số chế độ phổ biến:

  • ios::in: Mở file để đọc. (Đây là chế độ mặc định cho ifstream)
  • ios::out: Mở file để ghi. (Đây là chế độ mặc định cho ofstream)
  • ios::app: Mở file để ghi, nhưng tất cả các thao tác ghi sẽ được thêm vào cuối file (append). Nếu file không tồn tại, nó sẽ được tạo.
  • ios::trunc: Nếu file đã tồn tại khi mở ở chế độ ghi (out), nội dung cũ sẽ bị xóa sạch trước khi ghi. (Đây là hành vi mặc định cho ofstream khi không có app).
  • ios::ate: Di chuyển con trỏ file đến cuối file ngay sau khi mở. Bạn vẫn có thể di chuyển con trỏ về vị trí khác để đọc hoặc ghi ở giữa file.
  • ios::binary: Mở file ở chế độ nhị phân (binary), thay vì chế độ văn bản (text). Chế độ này quan trọng khi làm việc với dữ liệu không phải là văn bản thuần túy, vì nó ngăn chặn C++ tự động chuyển đổi các ký tự đặc biệt (như \n có thể được chuyển đổi thành \r\n trên một số hệ điều hành ở chế độ văn bản).

Bạn có thể kết hợp các cờ bằng toán tử | (OR bitwise), ví dụ:

// Mở file để đọc VÀ ghi
fstream myFile("data.bin", ios::in | ios::out);

// Mở file để ghi và thêm vào cuối (append), ở chế độ nhị phân
fstream logFile("app.log", ios::out | ios::app | ios::binary);

Trong các ví dụ cơ bản ở trên, chúng ta đã ngầm sử dụng các chế độ mặc định (ios::in cho ifstreamios::out cho ofstream ở chế độ text), đó là lý do không cần chỉ định cờ. Tuy nhiên, khi cần các hành vi đặc biệt hơn như ghi tiếp vào cuối file, hoặc làm việc với file nhị phân, bạn cần sử dụng các cờ này.

Kết thúc một luồng file

Như đã đề cập, việc gọi phương thức close() là quan trọng để đảm bảo tất cả dữ liệu đệm được ghi xuống đĩa và giải phóng tài nguyên hệ thống liên kết với file. Mặc dù destructor của đối tượng luồng file sẽ tự động làm điều này khi đối tượng bị hủy, việc đóng file tường minh ngay sau khi bạn hoàn thành các thao tác với nó là một practice tốt, đặc biệt trong các chương trình lớn hoặc khi làm việc với nhiều file. Nó giúp bạn kiểm soát rõ ràng hơn vòng đời của tài nguyên file.

Comments

There are no comments at the moment.