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

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ếugoodbit
được bật, tất cả các bit trạng thái lỗi khác đều tắt. Phương thứcgood()
trả vềtrue
nếugoodbit
đượ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ứceof()
trả vềtrue
nếueofbit
đượ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ứcfail()
trả vềtrue
nếufailbit
đượ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ứcbad()
trả vềtrue
nếubadbit
đượ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ếufailbit
hoặcbadbit
đượ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ếubadbit
được bật.stream.eof()
: Trả vềtrue
nếueofbit
được bật.!stream
: Toán tử!
(NOT) được nạp chồng cho các luồng, nó trả vềtrue
nếufailbit
hoặcbadbit
đượ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() {
ifstream t_doc("file_khong_ton_tai.txt");
if (!t_doc) {
cerr << "Loi: Khong the mo file." << endl;
return 1;
}
string d;
while (getline(t_doc, d)) {
cout << d << endl;
}
t_doc.close();
cout << "Chuong trinh ket thuc." << 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.
Output:
Loi: Khong the mo file.
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() {
ifstream t_doc("file_co_du_lieu.txt");
if (!t_doc.is_open()) {
cerr << "Loi: Khong the mo file." << endl;
return 1;
}
string t;
vector<string> ts;
cout << "Dang doc tu file..." << endl;
while (t_doc >> t) {
ts.push_back(t);
}
cout << "Doc file xong. Kiem tra trang thai:" << endl;
if (t_doc.eof()) {
cout << "- Da gap cuoi file (eofbit)." << endl;
}
if (t_doc.fail()) {
cerr << "- Loi khi doc du lieu (failbit)." << endl;
}
if (t_doc.bad()) {
cerr << "- Loi nghiem trong voi stream (badbit)." << endl;
}
if (t_doc.good()) {
cout << "- Stream van dang o trang thai tot (goodbit) - Khong xay ra sau vong lap doc het file." << endl;
}
cout << "\nCac tu da doc:";
if (ts.empty()) {
cout << " (khong doc duoc tu nao)";
} else {
for (const auto& e : ts) {
cout << " " << e;
}
}
cout << endl;
t_doc.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()
là true
, đó là điểm dừng bình thường. Nếu fail()
hoặc bad()
là true
, đã có lỗi xảy ra trong quá trình đọc.
Output (giả định file_co_du_lieu.txt có nội dung sau):
Hello World
C++ File Handling
Error Handling
Dang doc tu file...
Doc file xong. Kiem tra trang thai:
- Da gap cuoi file (eofbit).
Cac tu da doc: Hello World C++ File Handling Error Handling
File da duoc dong.
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ệ khibadbit
được bật.ios_base::failbit
: Ném ngoại lệ khifailbit
được bật.ios_base::eofbit
: Ném ngoại lệ khieofbit
đượ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>
#include <ios>
int main() {
ifstream t_doc;
try {
t_doc.exceptions(t_doc.failbit | t_doc.badbit);
cout << "Dang mo file 'file_khong_ton_tai_exception.txt'..." << endl;
t_doc.open("file_khong_ton_tai_exception.txt");
cout << "File mo thanh cong (Neu ban thay dong nay, co le file da ton tai)." << endl;
if (t_doc.is_open()) {
t_doc.close();
cout << "File da duoc dong sau khi xu ly." << endl;
}
} catch (const ios_base::failure& e) {
cerr << "**LOI NGOAI LE:** Loi I/O khi mo file: " << e.what() << endl;
if (t_doc.fail() && !t_doc.bad()) {
cerr << " -> Day la loi failbit (vi du: file khong ton tai)." << endl;
} else if (t_doc.bad()) {
cerr << " -> Day la loi badbit (vi du: loi he thong)." << endl;
}
} catch (const exception& e) {
cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
}
if (t_doc.is_open()) {
t_doc.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.
Output:
Dang mo file 'file_khong_ton_tai_exception.txt'...
**LOI NGOAI LE:** Loi I/O khi mo file: (Error message from system, e.g., No such file or directory)
-> Day la loi failbit (vi du: file khong ton tai).
Ket thuc chuong trinh.
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() {
ifstream t_doc("file_co_du_lieu.txt");
if (!t_doc.is_open()) {
cerr << "Loi: Khong the mo file." << endl;
return 1;
}
vector<int> so;
try {
t_doc.exceptions(t_doc.failbit | t_doc.eofbit);
int s;
cout << "Dang doc so tu file..." << endl;
while (t_doc >> s) {
so.push_back(s);
cout << "Doc duoc so: " << s << endl;
}
cout << "Vong lap doc xong (Bat thuong)." << endl;
} catch (const ios_base::failure& e) {
cerr << "**LOI NGOAI LE:** Loi I/O khi doc file: " << e.what() << endl;
if (t_doc.eof()) {
cerr << " -> Vong lap ket thuc do gap cuoi file (eofbit)." << endl;
}
if (t_doc.fail()) {
cerr << " -> Vong lap ket thuc do loi dinh dang hoac doc (failbit)." << endl;
}
if (t_doc.bad()) {
cerr << " -> Vong lap ket thuc do loi nghiem trong (badbit)." << endl;
}
} catch (const exception& e) {
cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
}
cout << "\nCac so da doc truoc khi xay ra loi/het file:";
if (so.empty()) {
cout << " (khong doc duoc so nao)";
} else {
for (int e : so) {
cout << " " << e;
}
}
cout << endl;
if (t_doc.is_open()) {
t_doc.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ả failbit
và eofbit
. 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ốicatch
được thực thi vớiinputFile.eof()
là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ốicatch
được thực thi vớiinputFile.fail()
là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?).
Output (giả định file_co_du_lieu.txt có nội dung sau):
123 abc 456
line 2
789
Dang doc so tu file...
Doc duoc so: 123
**LOI NGOAI LE:** Loi I/O khi doc file: (Error message from system, e.g., Bad file descriptor)
-> Vong lap ket thuc do loi dinh dang hoac doc (failbit).
Cac so da doc truoc khi xay ra loi/het file: 123
File da duoc dong o cuoi chuong trinh.
Ket thuc chuong trinh.
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 t_ghi;
vector<string> dl_ghi = {"Dong thu nhat.", "Dong thu hai.", "Dong thu ba."};
try {
t_ghi.exceptions(t_ghi.failbit | t_ghi.badbit);
cout << "Dang mo file 'file_ghi_exception.txt' de ghi..." << endl;
t_ghi.open("file_ghi_exception.txt");
cout << "File mo thanh cong." << endl;
cout << "Dang ghi du lieu vao file..." << endl;
for (const auto& d : dl_ghi) {
t_ghi << d << endl;
}
cout << "Ghi du lieu hoan tat (Neu khong co exception)." << endl;
if (t_ghi.is_open()) {
t_ghi.close();
cout << "File da duoc dong sau khi ghi xong." << endl;
}
} catch (const ios_base::failure& e) {
cerr << "**LOI NGOAI LE:** Loi I/O khi ghi file: " << e.what() << endl;
if (t_ghi.fail() && !t_ghi.bad()) {
cerr << " -> Co the la do het dung luong dia hoac loi dinh dang (failbit)." << endl;
} else if (t_ghi.bad()) {
cerr << " -> Day la loi nghiem trong voi stream (badbit)." << endl;
}
} catch (const exception& e) {
cerr << "**LOI NGOAI LE KHAC:** " << e.what() << endl;
}
if (t_ghi.is_open()) {
t_ghi.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 failbit
và badbit
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
.
Output:
Dang mo file 'file_ghi_exception.txt' de ghi...
File mo thanh cong.
Dang ghi du lieu vao file...
Ghi du lieu hoan tat (Neu khong co exception).
File da duoc dong sau khi ghi xong.
Ket thuc chuong trinh.
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
.
- Ưu điểm: Đơn giản cho các kiểm tra cơ bản (ví dụ: chỉ kiểm tra
- Ngoại lệ:
- Ưu điểm: Gom logic xử lý lỗi vào một nơi (
catch block
), làm code trongtry 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
.
- Ưu điểm: Gom logic xử lý lỗi vào một nơi (
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ớitry-catch
. Thường bậtfailbit
vàbadbit
. Cẩn thận khi bậteofbit
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ốicatch
để 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 logicclose()
trongfinally
(không có trong C++ chuẩn) hoặc sau khốicatch
(đảm bảois_open()
trước khi gọi).
Comments