
Chào các "coder hệ Gen Z"! Anh Creyt đây, hôm nay chúng ta sẽ cùng "flex" kiến thức về một khái niệm cực kỳ quan trọng trong C++: catch. Nghe có vẻ đơn giản, nhưng để dùng nó "chuẩn bài" thì không phải ai cũng biết đâu nhé!
1. catch là gì và để làm gì? (Kế hoạch B cho code)
Trong đời lập trình, đâu phải lúc nào code của chúng ta cũng chạy "mượt như nhung", "happy path" từ đầu đến cuối. Đôi khi, mọi thứ "toang" không báo trước: file không tồn tại, mạng rớt, người dùng nhập liệu "trời ơi đất hỡi", hay thậm chí là bộ nhớ đầy. Những lúc như vậy, nếu không có "kế hoạch B", ứng dụng của bạn sẽ "bay màu" ngay lập tức, người dùng thì "sốc ngang", còn bạn thì "đổ mồ hôi hột" tìm bug.
Đó chính là lúc try-catch "lên tiếng". Hãy hình dung thế này:
try: Là "sân khấu" nơi bạn cho code của mình "biểu diễn". Bạn tin là nó ổn, nhưng cũng hơi "rén" vì biết đâu có "phốt".throw: Nếu có "phốt" (lỗi, ngoại lệ) xảy ra trong khốitry, code sẽ "ném" ra một "tín hiệu SOS" - đó là một đối tượng ngoại lệ (exception).catch: Và đây,catchchính là "cái lưới" siêu to khổng lồ mà bạn giăng ra để "tóm gọn" cái "tín hiệu SOS" đó. Nó "bắt" lấy ngoại lệ đượcthrowra, cho phép bạn xử lý tình huống khẩn cấp một cách "thanh lịch" mà không làm sập cả "sân khấu" (chương trình).
Nói cách khác, catch giúp chương trình của bạn "sống sót" qua những tình huống bất ngờ, giúp bạn kiểm soát lỗi, ghi log lại để "điều tra" sau này, hoặc đơn giản là hiển thị một thông báo "dễ thương" cho người dùng thay vì một màn hình đen "đáng sợ".
2. Code Ví Dụ Minh Họa: 'Bắt' lỗi như pro
Để các bạn dễ hình dung, chúng ta hãy xem một ví dụ kinh điển: chia cho số 0.
#include <iostream>
#include <string>
#include <stdexcept> // Để dùng các exception chuẩn như std::runtime_error
// Định nghĩa một loại exception tùy chỉnh của riêng chúng ta
class DivideByZeroException : public std::runtime_error {
public:
DivideByZeroException(const std::string& msg)
: std::runtime_error("Lỗi chia cho 0: " + msg) {}
};
double divide(double numerator, double denominator) {
if (denominator == 0) {
// Nếu có lỗi, 'ném' ra một exception tùy chỉnh
throw DivideByZeroException("Mẫu số không được bằng 0!");
}
return numerator / denominator;
}
int main() {
double num1 = 10.0;
double num2 = 0.0;
double num3 = 2.0;
// Kịch bản 1: Chia cho 0 - sẽ bị 'bắt'
try {
std::cout << "Kết quả của " << num1 << " / " << num2 << " là: ";
double result = divide(num1, num2);
std::cout << result << std::endl; // Dòng này sẽ không được thực thi
} catch (const DivideByZeroException& e) { // Bắt exception tùy chỉnh của chúng ta
std::cerr << "*** Lỗi: " << e.what() << " ***" << std::endl;
} catch (const std::exception& e) { // Bắt các exception chuẩn khác
std::cerr << "*** Lỗi chung: " << e.what() << " ***" << std::endl;
} catch (...) { // Bắt tất cả các loại exception còn lại (catch-all)
std::cerr << "*** Lỗi không xác định đã xảy ra! ***" << std::endl;
}
std::cout << "\n--------------------------------\n\n";
// Kịch bản 2: Chia bình thường - sẽ không bị 'bắt'
try {
std::cout << "Kết quả của " << num1 << " / " << num3 << " là: ";
double result = divide(num1, num3);
std::cout << result << std::endl;
} catch (const DivideByZeroException& e) {
std::cerr << "*** Lỗi: " << e.what() << " ***" << std::endl;
} catch (const std::exception& e) {
std::cerr << "*** Lỗi chung: " << e.what() << " ***" << std::endl;
} catch (...) {
std::cerr << "*** Lỗi không xác định đã xảy ra! ***" << std::endl;
}
return 0;
}
Trong ví dụ trên:
- Chúng ta định nghĩa một
DivideByZeroExceptionkế thừa từstd::runtime_errorđể tạo ra một loại lỗi riêng biệt. - Hàm
dividesẽthrowexception này nếu mẫu số bằng 0. - Khối
trybao quanh lời gọi hàmdivide. - Các khối
catchđược sắp xếp từ cụ thể đến tổng quát:DivideByZeroException(của ta), rồi đếnstd::exception(của C++), và cuối cùng là...(bắt tất cả).

3. Mẹo (Best Practices) để 'bắt' lỗi chuẩn khỏi chỉnh
Để trở thành một "thợ săn lỗi" chuyên nghiệp, hãy bỏ túi vài mẹo sau:
- "Bắt" đúng loại: Luôn ưu tiên
catchcác exception cụ thể trước. Ví dụ,catch (const DivideByZeroException& e)sẽ được xử lý trướccatch (const std::exception& e). Việc này giống như bạn có bộ lọc thông minh, chỉ bắt những loại cá bạn muốn thôi. - "Bắt" bằng tham chiếu
const&: Thay vìcatch (std::exception e)(bắt bằng giá trị, tạo bản sao), hãy dùngcatch (const std::exception& e). Nó hiệu quả hơn nhiều, tránh được tình trạng "object slicing" (mất thông tin của exception con khi bắt bằng exception cha) và cho phép đa hình. - Đừng "nuốt chửng" lỗi: Đừng bao giờ
catchmột exception rồi để trống khốicatchhoặc chỉ in ra một câu "Lỗi rồi!" chung chung. Hãy luôn ghi log chi tiết, hoặc ít nhất là thông báo cho người dùng một cách rõ ràng. Lỗi mà bị "nuốt" đi, sau này debug bạn sẽ "đổ mồ hôi hột" tìm nó đấy. - RAII là "chân ái" cho tài nguyên:
try-catchtuyệt vời cho việc xử lý ngoại lệ. Nhưng để đảm bảo tài nguyên (file, bộ nhớ, khóa...) được giải phóng dù có lỗi hay không, hãy dùng RAII (Resource Acquisition Is Initialization). Ví dụ nhưstd::unique_ptrcho bộ nhớ,std::lock_guardcho mutex. Chúng tự động dọn dẹp khi ra khỏi scope, "đỉnh của chóp"! - Chỉ dùng cho trường hợp "bất thường": Exception handling có chi phí hiệu năng. Đừng dùng nó để kiểm tra các điều kiện "bình thường" như kiểm tra người dùng nhập số âm hay một
vectorrỗng. Những trường hợp đó,if-elselà "đúng bài" hơn nhiều.
4. Ứng dụng thực tế: catch ở khắp mọi nơi
catch không chỉ là lý thuyết, nó được ứng dụng "ngập tràn" trong các hệ thống "khủng" mà bạn dùng hàng ngày:
- Trình duyệt web (ví dụ Google Chrome): Khi một tab bị lỗi nặng (ví dụ, một script JavaScript chạy sai gây tràn bộ nhớ), trình duyệt sẽ
catchlỗi đó và chỉ làm sập tab đó thôi, không ảnh hưởng đến các tab khác hay toàn bộ trình duyệt. Bạn sẽ thấy thông báo "Trang này đã gặp sự cố". - Ứng dụng di động (ví dụ Spotify, Facebook): Khi bạn mất kết nối mạng, ứng dụng sẽ không "crash" mà sẽ
catchlỗi mạng, hiển thị thông báo "Không có kết nối Internet" và cho phép bạn thử lại. - Hệ thống quản lý cơ sở dữ liệu: Khi kết nối đến database thất bại, hoặc một truy vấn SQL bị lỗi cú pháp, hệ thống sẽ
throwexception và ứng dụng của bạn sẽcatchđể xử lý, ví dụ như hiển thị thông báo lỗi cho người dùng hoặc thử kết nối lại. - Game engines: Khi một tài nguyên game (texture, model) không thể tải được do file bị hỏng hoặc không tồn tại, engine sẽ
catchlỗi và có thể tải một tài nguyên mặc định hoặc hiển thị lỗi để nhà phát triển sửa.
5. Thử nghiệm của Creyt và lời khuyên
Hồi mới "nhập môn" C++, anh Creyt cũng từng "vật lộn" với try-catch. Ban đầu, anh hay có thói quen catch (...) (bắt tất cả) để chương trình không bị crash, nhưng rồi lại "đau đầu" vì không biết lỗi cụ thể là gì để mà sửa. Đó là một bài học đắt giá về việc phải "bắt" có chọn lọc.
Khi nào nên dùng try-catch?
- Khi tương tác với các thư viện bên thứ ba: Bạn không kiểm soát được code của họ, nên hãy chuẩn bị "đón lỗi" từ họ.
- Khi làm việc với tài nguyên bên ngoài: File I/O, network, database. Những thứ này luôn tiềm ẩn rủi ro.
- Khi một lỗi là thực sự ngoại lệ: Nghĩa là nó không nên xảy ra trong luồng hoạt động bình thường của chương trình. Ví dụ, một file cấu hình quan trọng bị thiếu, chứ không phải việc người dùng nhập sai tuổi.
Khi nào không nên dùng try-catch?
- Thay thế cho
if-else: Đừng dùng exception để kiểm tra các điều kiện thông thường.if (file.exists()) { ... } else { ... }tốt hơn nhiều so vớitry { open_file(); } catch (FileNotFoundException) { ... }. - Trong các vòng lặp hiệu năng cao: Chi phí của exception handling (stack unwinding, tìm kiếm catch block phù hợp) có thể rất lớn và làm chậm chương trình của bạn.
catch là một công cụ mạnh mẽ, nhưng như mọi công cụ khác, nó cần được sử dụng đúng lúc, đúng chỗ. Hãy "bắt" lỗi một cách thông minh, và code của bạn sẽ "chất" hơn rất nhiều!
Thuộc Series: C++
Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!