Bài 37.4: Xử lý ngoại lệ với file trong C++

Chào mừng bạn trở lại với chuỗi bài viết về C++! Hôm nay, chúng ta sẽ đào sâu vào một khía cạnh quan trọng bậc nhất khi làm việc với file: xử lý lỗi.

Bạn đã học cách mở file, đọc dữ liệu từ file và ghi dữ liệu ra file. Nhưng điều gì sẽ xảy ra nếu...

  • File bạn muốn đọc không tồn tại?
  • Bạn không có quyền ghi vào một thư mục nào đó?
  • Ổ đĩa bị đầy khi bạn đang cố gắng ghi dữ liệu?
  • Dữ liệu trong file bị hỏng hoặc không đúng định dạng bạn mong đợi?

Trong thế giới lập trình thực tế, những tình huống "không mong muốn" này xảy ra như cơm bữa. Nếu chương trình của bạn không được chuẩn bị để xử lý chúng, nó có thể crash (sập), hoạt động sai lệch, hoặc tệ hơn là làm mất dữ liệu của người dùng.

May mắn thay, C++ cung cấp cho chúng ta những công cụ mạnh mẽ để đối phó với các vấn đề này. Chúng ta sẽ khám phá hai cách chính: sử dụng cờ trạng thái (state flags) và sử dụng ngoại lệ (exceptions).

Khi Mọi Thứ Không Như Ý: Các Cờ Trạng Thái của Stream

Khi bạn làm việc với các luồng (streams) như ifstream, ofstream hay fstream, mỗi luồng đều mang trong mình một bộ các cờ trạng thái cho biết tình hình hiện tại của luồng đó. Việc kiểm tra các cờ này sau mỗi thao tác (mở, đọc, ghi) là cách truyền thống để phát hiện lỗi.

Các cờ trạng thái chính bao gồm:

  • goodbit: Bit này được bật khi không có lỗi nào xảy ra. Nếu goodbit được bật, tất cả các bit trạng thái lỗi khác đều tắt. Phương thức good() trả về true nếu goodbit được bật.
  • eofbit: Bit này được bật khi thao tác đọc đạt đến cuối file (end-of-file). Phương thức eof() trả về true nếu eofbit được bật.
  • failbit: Bit này được bật khi thao tác đọc hoặc ghi không thành công, ví dụ: cố gắng đọc một số vào biến kiểu chuỗi, hoặc file không tìm thấy khi mở. Thường là lỗi liên quan đến định dạng hoặc logic thao tác. Phương thức fail() trả về true nếu failbit được bật.
  • badbit: Bit này được bật khi có một lỗi nghiêm trọng xảy ra làm cho luồng không còn đáng tin cậy nữa, ví dụ: lỗi đọc/ghi cấp độ hệ thống tệp. Phương thức bad() trả về true nếu badbit được bật.

Bạn cũng có thể kiểm tra trạng thái tổng quát của luồng bằng các phương thức:

  • stream.good(): Trả về true nếu luồng tốt, sẵn sàng cho các thao tác tiếp theo. Tương đương với kiểm tra xem không có cờ lỗi nào (eofbit, failbit, badbit) được bật.
  • stream.fail(): Trả về true nếu failbit hoặc badbit được bật. Đây là cách phổ biến nhất để kiểm tra xem thao tác gần nhất có thành công không.
  • stream.bad(): Trả về true nếu badbit được bật.
  • stream.eof(): Trả về true nếu eofbit được bật.
  • !stream: Toán tử ! (NOT) được nạp chồng cho các luồng, nó trả về true nếu failbit hoặc badbit được bật (giống như stream.fail()). Đây là cách rất phổ biến để kiểm tra lỗi sau một thao tác.
Ví dụ 1: Kiểm tra lỗi khi mở file bằng cờ trạng thái

Đây là bước kiểm tra bắt buộc sau khi bạn gọi open() hoặc khai báo đối tượng stream với tên file.

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

int main() {
    // Cố gắng mở một file không tồn tại
    ifstream inputFile("file_khong_ton_tai.txt");

    // Cách 1: Sử dụng toán tử !
    if (!inputFile) {
        cerr << "Lỗi: Không thể mở file bằng toán tử !." << endl;
        // Thoát khỏi chương trình hoặc xử lý lỗi khác
        return 1;
    }

    // Cách 2: Sử dụng phương thức is_open() - thường dùng để kiểm tra mở file
    // if (!inputFile.is_open()) {
    //     cerr << "Lỗi: Không thể mở file bằng is_open()." << endl;
    //     return 1;
    // }

    // Cách 3: Sử dụng phương thức fail() - kiểm tra failbit hoặc badbit
    // if (inputFile.fail()) {
    //      cerr << "Lỗi: Không thể mở file bằng fail()." << endl;
    //      return 1;
    // }


    string line;
    // Đọc file (lúc này sẽ không xảy ra vì file không mở được)
    while (getline(inputFile, line)) {
        cout << line << endl;
    }

    // Đóng file (an toàn khi gọi ngay cả khi file không mở được)
    inputFile.close();

    cout << "Chương trình kết thúc." << endl;

    return 0;
}

Giải thích: Chúng ta khai báo ifstream với tên file. Nếu file không tồn tại hoặc có lỗi khác khi mở, đối tượng inputFile sẽ ở trạng thái lỗi (failbit hoặc badbit được bật). Câu lệnh if (!inputFile) kiểm tra trạng thái lỗi này. Nếu là true, chúng ta in ra thông báo lỗi và thoát chương trình. Việc kiểm tra này giúp ngăn chặn các thao tác đọc/ghi sau đó trên một luồng bị lỗi, điều có thể dẫn đến hành vi không xác định.

Ví dụ 2: Kiểm tra lỗi và cuối file khi đọc dữ liệu

Khi đọc dữ liệu trong vòng lặp, bạn cần kiểm tra trạng thái của luồng để biết khi nào nên dừng và liệu việc dừng đó có phải do lỗi hay không.

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

int main() {
    // Giả định file_co_du_lieu.txt tồn tại và có một vài dòng văn bản
    // Ví dụ nội dung:
    // Hello World
    // C++ File Handling
    // Error Handling

    ifstream inputFile("file_co_du_lieu.txt");

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

    string word;
    vector<string> words;

    cout << "Dang doc tu file..." << endl;
    // Vòng lặp đọc từng từ
    while (inputFile >> word) {
        words.push_back(word);
    }

    // Sau khi vòng lặp kết thúc, kiểm tra lý do dừng
    cout << "Doc file xong. Kiem tra trang thai:" << endl;

    if (inputFile.eof()) {
        cout << "- Da gap cuoi file (eofbit)." << endl;
    }
    if (inputFile.fail()) {
         // Failbit bat khi doc dinh dang sai, hoac doc het file (doi voi >>)
         cerr << "- Loi khi doc du lieu (failbit)." << endl;
    }
    if (inputFile.bad()) {
         cerr << "- Loi nghiem trong voi stream (badbit)." << endl;
    }
     if (inputFile.good()) {
         cout << "- Stream van dang o trang thai tot (goodbit) - Khong xay ra sau vong lap doc het file." << endl;
     }


    // In ra các từ đã đọc (nếu có)
    cout << "\nCac tu da doc:";
    if (words.empty()) {
        cout << " (khong doc duoc tu nao)";
    } else {
        for (const auto& w : words) {
             cout << " " << w;
        }
    }
    cout << endl;

    inputFile.close();
    cout << "File da duoc dong." << endl;


    return 0;
}

Giải thích: Vòng lặp while (inputFile >> word) sẽ tiếp tục đọc các từ cho đến khi gặp lỗi (ví dụ: dữ liệu không phải là từ, hoặc lỗi I/O) hoặc đến cuối file. Khi vòng lặp dừng, chúng ta kiểm tra các cờ eof(), fail(), bad() để xác định nguyên nhân dừng. Nếu eof()true, đó là điểm dừng bình thường. Nếu fail() hoặc bad()true, đã có lỗi xảy ra trong quá trình đọc.

Việc sử dụng cờ trạng thái đòi hỏi bạn phải kiểm tra sau hầu hết các thao tác quan trọng. Điều này có thể khiến code trở nên dài dòng và dễ bỏ sót kiểm tra ở một vài chỗ, đặc biệt trong các chương trình phức tạp. Đây là lúc ngoại lệ trở nên hữu ích.

Sử dụng Ngoại lệ (Exceptions) để Xử lý Lỗi Stream

C++ streams có một cơ chế mạnh mẽ hơn để xử lý lỗi: ném ngoại lệ. Thay vì kiểm tra cờ trạng thái sau mỗi thao tác, bạn có thể cấu hình stream để nó tự động ném ra một ngoại lệ khi một cờ trạng thái cụ thể nào đó được bật. Điều này cho phép bạn tập trung xử lý lỗi ở một nơi duy nhất bằng cách sử dụng khối try-catch.

Phương thức exceptions() của các lớp stream (istream, ostream, fstream) cho phép bạn chỉ định những cờ trạng thái nào sẽ gây ra việc ném ngoại lệ khi chúng được bật.

Cú pháp: stream.exceptions(mask);

Trong đó, mask là sự kết hợp của các cờ trạng thái mà bạn muốn bật chế độ ngoại lệ cho chúng, sử dụng toán tử OR (|). Các cờ thường dùng:

  • ios_base::badbit: Ném ngoại lệ khi badbit được bật.
  • ios_base::failbit: Ném ngoại lệ khi failbit được bật.
  • ios_base::eofbit: Ném ngoại lệ khi eofbit được bật (ít phổ biến hơn vì đọc hết file là điều bình thường, nhưng có thể hữu ích trong một số trường hợp).

Khi một ngoại lệ được ném ra, nó thường là kiểu ios_base::failure, kế thừa từ exception.

Ví dụ 3: Mở file và bắt ngoại lệ khi lỗi

Hãy xem lại ví dụ mở file, nhưng lần này sử dụng ngoại lệ.

#include <fstream>
#include <iostream>
#include <string>
#include <exception> // Cần thiết để bắt exception
#include <ios>       // Cần thiết cho ios_base::failure

int main() {
    ifstream inputFile;

    try {
        // Bật chế độ ném ngoại lệ khi cờ failbit hoặc badbit được bật
        // failbit sẽ được bật nếu file không tìm thấy hoặc không có quyền truy cập
        inputFile.exceptions(ifstream::failbit | ifstream::badbit);

        cout << "Dang mo file 'file_khong_ton_tai_exception.txt'..." << endl;
        inputFile.open("file_khong_ton_tai_exception.txt");

        // Nếu đến được đây, file đã mở thành công (điều này sẽ không xảy ra
        // nếu file không tồn tại và exception được ném).
        cout << "File mo thanh cong (Neu ban thay dong nay, co le file da ton tai)." << endl;

        // ... Code xử lý file nếu mở thành công sẽ ở đây ...

        // Đảm bảo đóng file nếu mọi thứ suôn sẻ
        if (inputFile.is_open()) {
            inputFile.close();
            cout << "File da duoc dong sau khi xu ly." << endl;
        }

    } catch (const ios_base::failure& e) {
        // Ngoại lệ ios_base::failure sẽ bị bắt tại đây nếu failbit hoặc badbit bật
        cerr << "**LOI NGOAI LE:** Loi I/O khi mo file: " << e.what() << endl;
        // e.what() cung cấp thông tin chi tiết hơn về lỗi hệ thống

        // Kiểm tra cờ bên trong catch block (tùy chọn) để phân biệt loại lỗi cụ thể hơn
        if (inputFile.fail() && !inputFile.bad()) {
            cerr << "   -> Day la loi failbit (vi du: file khong ton tai)." << endl;
        } else if (inputFile.bad()) {
             cerr << "   -> Day la loi badbit (vi du: loi he thong)." << endl;
        }

    } catch (const exception& e) {
        // Bắt các ngoại lệ khác có thể xảy ra (ít gặp hơn với stream errors)
        cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
    }

    // Luôn kiểm tra và đóng file sau khối try-catch
    // Điều này quan trọng nếu file được mở thành công trong try
    // nhưng sau đó xảy ra một ngoại lệ KHÁC không liên quan đến stream I/O.
    if (inputFile.is_open()) {
        inputFile.close();
        cout << "File da duoc dong o cuoi chuong trinh." << endl;
    }


    cout << "Ket thuc chuong trinh." << endl;

    return 0;
}

Giải thích: Chúng ta sử dụng khối try để bao bọc các thao tác có thể gây lỗi (ở đây là open()). Trước khi mở, chúng ta gọi inputFile.exceptions(ifstream::failbit | ifstream::badbit); để yêu cầu luồng ném ngoại lệ nếu failbit hoặc badbit được bật. Nếu file không tồn tại, open() sẽ thất bại, failbit sẽ bật, và luồng sẽ ném ra ngoại lệ ios_base::failure. Ngoại lệ này được bắt trong khối catch, nơi chúng ta in ra thông báo lỗi.

Ví dụ 4: Đọc dữ liệu và bắt ngoại lệ khi gặp lỗi hoặc cuối file

Sử dụng ngoại lệ có thể làm cho vòng lặp đọc file trông gọn gàng hơn.

#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <exception>
#include <ios>

int main() {
    // Giả định file_co_du_lieu.txt tồn tại và có một vài dòng văn bản/số
    // Ví dụ nội dung:
    // 123 abc 456
    // line 2
    // 789

    ifstream inputFile("file_co_du_lieu.txt");

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

    vector<int> numbers; // Chúng ta sẽ thử đọc số

    try {
        // Bật chế độ ném ngoại lệ cho failbit (lỗi định dạng, lỗi đọc) và eofbit (cuối file)
        // Khi vòng lặp >> kết thúc do failbit hoặc eofbit, ngoại lệ sẽ được ném.
        inputFile.exceptions(ifstream::failbit | ifstream::eofbit);

        int num;
        cout << "Dang doc so tu file..." << endl;
        // Vòng lặp đọc số
        while (inputFile >> num) {
            numbers.push_back(num);
            cout << "Doc duoc so: " << num << endl;
        }

        // Lưu ý: Dòng này thường KHÔNG chạy nếu eofbit hoặc failbit được bật và ném ngoại lệ
        // Vòng lặp kết thúc, và ngoại lệ sẽ được ném TRƯỚC khi đến đây
        cout << "Vong lap doc xong (Bat thuong)." << endl;


    } catch (const ios_base::failure& e) {
        // Ngoại lệ sẽ bị bắt tại đây khi gặp cuối file (eofbit)
        // HOẶC khi đọc dữ liệu sai định dạng (failbit)
        // HOẶC khi có lỗi nghiêm trọng (badbit - nếu được bao gồm trong exceptions mask)
        cerr << "**LOI NGOAI LE:** Loi I/O khi doc file: " << e.what() << endl;

        // Kiểm tra cờ trạng thái bên trong catch block để biết chi tiết
        if (inputFile.eof()) {
            cerr << "   -> Vong lap ket thuc do gap cuoi file (eofbit)." << endl;
        }
        if (inputFile.fail()) {
            cerr << "   -> Vong lap ket thuc do loi dinh dang hoac doc (failbit)." << endl;
             // Có thể muốn bỏ qua lỗi này và tiếp tục? (Phức tạp hơn)
             // inputFile.clear(); // Xóa cờ lỗi để tiếp tục
             // inputFile.ignore(...); // Bỏ qua dữ liệu gây lỗi
        }
        if (inputFile.bad()) {
             cerr << "   -> Vong lap ket thuc do loi nghiem trong (badbit)." << endl;
        }


    } catch (const exception& e) {
        cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
    }

    // In ra các số đã đọc
    cout << "\nCac so da doc truoc khi xay ra loi/het file:";
    if (numbers.empty()) {
         cout << " (khong doc duoc so nao)";
    } else {
        for (int n : numbers) {
             cout << " " << n;
        }
    }
    cout << endl;


    // Đảm bảo file được đóng ngay cả khi có lỗi trong catch block
    // (Đối tượng stream vẫn tồn tại sau khi catch, miễn là nó được khai báo trước try)
    if (inputFile.is_open()) {
        inputFile.close();
        cout << "File da duoc dong o cuoi chuong trinh." << endl;
    }


    cout << "Ket thuc chuong trinh." << endl;


    return 0;
}

Giải thích: Trong ví dụ này, chúng ta bật ngoại lệ cho cả failbiteofbit. Vòng lặp while (inputFile >> num) cố gắng đọc từng số nguyên.

  • Nếu đọc thành công, nó tiếp tục.
  • Nếu gặp cuối file, eofbit bật, ngoại lệ được ném, và khối catch được thực thi với inputFile.eof()true.
  • Nếu gặp dữ liệu không phải số (ví dụ: "abc" trong file), thao tác >> num thất bại, failbit bật, ngoại lệ được ném, và khối catch được thực thi với inputFile.fail()true.

Sử dụng ngoại lệ giúp gom tất cả logic xử lý lỗi vào một nơi (khối catch), làm cho logic chính (khối try) trông gọn gàng hơn. Tuy nhiên, bạn vẫn cần kiểm tra cờ trạng thái bên trong khối catch nếu muốn phân biệt chính xác loại lỗi đã xảy ra (ví dụ: là cuối file hay lỗi định dạng?).

Ví dụ 5: Ghi dữ liệu ra file và bắt ngoại lệ

Tương tự như đọc, lỗi khi ghi file cũng có thể ném ngoại lệ.

#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include <exception>
#include <ios>

int main() {
    ofstream outputFile;
    vector<string> data_to_write = {"Dong thu nhat.", "Dong thu hai.", "Dong thu ba."};

    try {
        // Bật chế độ ném ngoại lệ khi cờ failbit hoặc badbit được bật cho output stream
        // failbit có thể bật nếu đĩa đầy, badbit nếu có lỗi hệ thống nghiêm trọng
        outputFile.exceptions(ofstream::failbit | ofstream::badbit);

        cout << "Dang mo file 'file_ghi_exception.txt' de ghi..." << endl;
        outputFile.open("file_ghi_exception.txt");

        // Nếu đến được đây, file đã mở thành công
        cout << "File mo thanh cong." << endl;

        cout << "Dang ghi du lieu vao file..." << endl;
        for (const auto& line : data_to_write) {
            outputFile << line << endl;
            // Với exception, KHÔNG cần kiểm tra outputFile.fail() sau mỗi lần ghi
            // Nếu lỗi xảy ra trong lúc ghi, exception sẽ được ném ngay lập tức
        }
        cout << "Ghi du lieu hoan tat (Neu khong co exception)." << endl;


        // Đảm bảo đóng file nếu mọi thứ suôn sẻ
        if (outputFile.is_open()) {
             outputFile.close();
             cout << "File da duoc dong sau khi ghi xong." << endl;
        }


    } catch (const ios_base::failure& e) {
        // Ngoại lệ sẽ bị bắt tại đây nếu có lỗi khi mở hoặc ghi file
        cerr << "**LOI NGOAI LE:** Loi I/O khi ghi file: " << e.what() << endl;

        // Kiểm tra cờ trạng thái bên trong catch block để biết chi tiết
        if (outputFile.fail() && !outputFile.bad()) {
             cerr << "   -> Co the la do het dung luong dia hoac loi dinh dang (failbit)." << endl;
        } else if (outputFile.bad()) {
             cerr << "   -> Day la loi nghiem trong voi stream (badbit)." << endl;
        }

    } catch (const exception& e) {
        cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
    }

    // Đảm bảo file được đóng ngay cả khi có lỗi
    if (outputFile.is_open()) {
        outputFile.close();
        cout << "File da duoc dong o cuoi chuong trinh." << endl;
    }


    cout << "Ket thuc chuong trinh." << endl;

    return 0;
}

Giải thích: Tương tự, chúng ta bật ngoại lệ cho failbitbadbit trên ofstream. Nếu có lỗi xảy ra trong quá trình open() hoặc ghi dữ liệu (<<), ngoại lệ ios_base::failure sẽ được ném và bắt trong khối catch.

Nên dùng Cờ Trạng thái hay Ngoại lệ?

Cả hai phương pháp đều có ưu và nhược điểm:

  • Cờ trạng thái:
    • Ưu điểm: Đơn giản cho các kiểm tra cơ bản (ví dụ: chỉ kiểm tra is_open() hoặc !stream). Cung cấp kiểm soát chi tiết sau mỗi thao tác.
    • Nhược điểm: Dễ bị bỏ sót các bước kiểm tra trong code phức tạp. Code có thể trở nên lặp đi lặp lại với nhiều câu lệnh if.
  • Ngoại lệ:
    • Ưu điểm: Gom logic xử lý lỗi vào một nơi (catch block), làm code trong try block gọn gàng hơn (tập trung vào logic chính). Mạnh mẽ cho việc xử lý các lỗi không mong muốn hoặc nghiêm trọng.
    • Nhược điểm: Có thể làm code khó theo dõi luồng thực thi khi ngoại lệ được ném. Cần cẩn thận để không bật ngoại lệ cho các sự kiện "bình thường" như cuối file (trừ khi bạn thực sự muốn xử lý cuối file như một ngoại lệ). Việc kiểm tra chi tiết loại lỗi cụ thể vẫn cần kiểm tra cờ trạng thái bên trong khối catch.

Khuyến nghị:

  • Luôn luôn kiểm tra việc mở file. Sử dụng !stream hoặc !stream.is_open() là cách nhanh chóng và hiệu quả nhất để kiểm tra xem bạn có thể bắt đầu làm việc với file hay không.
  • Đối với các thao tác đọc/ghi phức tạp hơn trong vòng lặp hoặc khi bạn muốn đảm bảo mọi lỗi I/O đều được xử lý tập trung, hãy cân nhắc sử dụng stream.exceptions() kết hợp với try-catch. Thường bật failbitbadbit. Cẩn thận khi bật eofbit bằng ngoại lệ.
  • Trong khối catch, hãy in ra thông tin từ e.what() vì nó thường cung cấp thông tin lỗi chi tiết từ hệ thống. Bạn cũng có thể kiểm tra lại các cờ trạng thái (stream.fail(), stream.bad(), stream.eof()) bên trong khối catch để hiểu rõ hơn nguyên nhân gốc rễ của ngoại lệ.
  • Quan trọng nhất: Đảm bảo rằng file của bạn được đóng lại dù có lỗi hay không. Sử dụng các đối tượng stream được khai báo trước khối try sẽ giúp đối tượng tự động giải phóng tài nguyên (đóng file) khi thoát khỏi phạm vi (RAII - Resource Acquisition Is Initialization), hoặc bạn phải thêm logic close() trong finally (không có trong C++ chuẩn) hoặc sau khối catch (đảm bảo is_open() trước khi gọi).

Comments

There are no comments at the moment.