Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 56.430 phút đọc

Xử lý lỗi

Sau bài này bạn sẽ làm được

  • So sánh bốn chiến lược trả lỗi
  • Viết hàm dọn dẹp bằng goto một cách đúng
  • Truyền lỗi lên trên mà không mất thông tin
  • Biết khi nào dừng chương trình là đúng

C không có ngoại lệ, nên xử lý lỗi là một quyết định thiết kế bạn phải đưa ra một cách có ý thức. Bài này so sánh bốn chiến lược bằng cách chạy cả bốn trên cùng một dữ liệu, và hai trong số đó cho ra kết quả sai trong im lặng.

#Bốn chiến lược

Chiến lượcHình thứcVí dụ trong thư viện chuẩn
Mã lỗi trả về, kết quả qua tham số raint f(const char *s, int *ra)Không có, nhưng là kiểu của nhiều thư viện hiện đại
Giá trị canh gácint f(const char *s), trả về INT_MIN nếu lỗiatoi, strchr trả về NULL, malloc trả về NULL
Biến lỗi toàn cụcTrả về giá trị, đặt errnostrtol, fopen, mọi lời gọi hệ thống POSIX
Cấu trúc lỗi giàu thông tinint f(const char *s, int *ra, Loi *l)SQLite với sqlite3_errmsg, libcurl với CURLcode
Cách 1: mã lỗi và tham số ra
enum { OK = 0, LOI_NULL = -1, LOI_DINH_DANG = -2, LOI_TRAN = -3 };

static const char *ten_loi(int ma) {
    switch (ma) {
        case OK:            return "OK";
        case LOI_NULL:      return "doi so NULL";
        case LOI_DINH_DANG: return "khong phai so";
        case LOI_TRAN:      return "tran pham vi int";
        default:            return "khong biet";
    }
}

static int doc_so_1(const char *s, int *ra) {
    if (s == NULL || ra == NULL) return LOI_NULL;
    errno = 0;
    char *cuoi;
    long v = strtol(s, &cuoi, 10);
    if (cuoi == s || *cuoi != '\0')            return LOI_DINH_DANG;
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return LOI_TRAN;
    *ra = (int)v;
    return OK;
}
Cách 2 và cách 3: canh gác, và biến toàn cục
/* Cach 2: gia tri canh gac */
static int doc_so_2(const char *s) {
    char *cuoi;
    long v = strtol(s, &cuoi, 10);
    if (cuoi == s || *cuoi != '\0') return INT_MIN;   /* canh gac */
    return (int)v;
}

/* Cach 3: bien loi toan cuc kieu errno */
static int loi_cuoi = 0;
static int doc_so_3(const char *s) {
    loi_cuoi = 0;
    char *cuoi;
    long v = strtol(s, &cuoi, 10);
    if (cuoi == s || *cuoi != '\0') { loi_cuoi = LOI_DINH_DANG; return 0; }
    return (int)v;
}
Cách 4: cấu trúc lỗi
typedef struct {
    int         ma;
    char        thong_diep[128];
    const char *tep;
    int         dong;
} Loi;

#define DAT_LOI(l, m, ...)                                          \
    do { (l)->ma = (m); (l)->tep = __FILE__; (l)->dong = __LINE__;  \
         snprintf((l)->thong_diep, sizeof (l)->thong_diep,          \
                  __VA_ARGS__); } while (0)

static int doc_so_4(const char *s, int *ra, Loi *l) {
    char *cuoi;
    errno = 0;
    long v = strtol(s, &cuoi, 10);
    if (cuoi == s || *cuoi != '\0') {
        DAT_LOI(l, LOI_DINH_DANG, "\"%s\" khong phai so nguyen", s);
        return -1;
    }
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) {
        DAT_LOI(l, LOI_TRAN, "\"%s\" vuot pham vi int (%d..%d)",
                s, INT_MIN, INT_MAX);
        return -1;
    }
    *ra = (int)v;
    return 0;
}

#Chạy thử cả bốn

terminal
gcc -std=c11 -O2 -Wall -Wextra -D__USE_MINGW_ANSI_STDIO=1 bon.c -o bon.exe
./bon.exe
dau vao: "42"
  cach 1: ma=0  (OK) gia tri=42
  cach 2: 42
  cach 3: 42, loi_cuoi=0
  cach 4: 42

dau vao: "abc"
  cach 1: ma=-2 (khong phai so)
  cach 2: -2147483648  (canh gac hay gia tri that?)
  cach 3: 0, loi_cuoi=-2
  cach 4: "abc" khong phai so nguyen  [bon.c:67]

dau vao: "99999999999999"
  cach 1: ma=-3 (tran pham vi int)
  cach 2: 2147483647
  cach 3: 2147483647, loi_cuoi=0
  cach 4: "99999999999999" vuot pham vi int (-2147483648..2147483647)  [bon.c:71]

dau vao: ""
  cach 1: ma=-2 (khong phai so)
  cach 2: -2147483648  (canh gac hay gia tri that?)
  cach 3: 0, loi_cuoi=-2
  cach 4: "" khong phai so nguyen  [bon.c:67]

#Dọn dẹp bằng goto

dondep.c, bốn tài nguyên
static int lam_viec(void) {
    int ma = -1;
    char *a = NULL, *b = NULL, *c = NULL, *d = NULL;

    a = cap(16, "a"); if (!a) goto het;
    b = cap(16, "b"); if (!b) goto tha_a;
    c = cap(16, "c"); if (!c) goto tha_b;
    d = cap(16, "d"); if (!d) goto tha_c;

    printf("  lam viec that\n");
    ma = 0;

    tha(d, "d");            /* duong ra binh thuong roi vao chuoi nhan */
tha_c:
    tha(c, "c");
tha_b:
    tha(b, "b");
tha_a:
    tha(a, "a");
het:
    return ma;
}
terminal
./dd.exe
=== buoc hong = 0 ===
  a: cap
  b: cap
  c: cap
  d: cap
  lam viec that
  d: tha
  c: tha
  b: tha
  a: tha
  ket qua=0, con lai 0 vung chua tha

=== buoc hong = 2 ===
  a: cap
  b: CO Y that bai
  a: tha
  ket qua=-1, con lai 0 vung chua tha

=== buoc hong = 4 ===
  a: cap
  b: cap
  c: cap
  d: CO Y that bai
  c: tha
  b: tha
  a: tha
  ket qua=-1, con lai 0 vung chua tha
Không dùng goto
int lam_viec(void) { char *a = malloc(16); if (!a) return -1; char *b = malloc(16); if (!b) { free(a); return -1; } char *c = malloc(16); if (!c) { free(b); free(a); return -1; } char *d = malloc(16); if (!d) { free(c); free(b); free(a); return -1; } /* Voi 4 tai nguyen: 1+2+3 = 6 lenh free trong nhanh loi. Voi 8 tai nguyen: 28 lenh free. Them mot tai nguyen o giua = sua moi nhanh ben duoi. Day la noi ro ri bo nho sinh ra. */ ... }
Dùng goto
int lam_viec(void) { int ma = -1; char *a = NULL, *b = NULL, *c = NULL, *d = NULL; a = malloc(16); if (!a) goto het; b = malloc(16); if (!b) goto tha_a; c = malloc(16); if (!c) goto tha_b; d = malloc(16); if (!d) goto tha_c; ma = 0; free(d); tha_c: free(c); tha_b: free(b); tha_a: free(a); het: return ma; } /* Them mot tai nguyen = them mot dong va mot nhan. Day la mau chuan cua nhan Linux, cua OpenSSL, va cua gan nhu moi ma C nghiem tuc. */

#Truyền lỗi lên trên

Ba tầng, và thông tin mất dần
/* Tang thap */
int tep_doc(const char *dd, char **ra, size_t *co) {
    FILE *t = fopen(dd, "rb");
    if (!t) return -1;              /* mat: LY DO khong mo duoc */
    ...
}

/* Tang giua */
int cau_hinh_nap(const char *dd, CauHinh *c) {
    char *noi_dung; size_t co;
    if (tep_doc(dd, &noi_dung, &co) != 0) return -1;   /* mat: TEP NAO */
    ...
}

/* Tang cao */
int khoi_dong(void) {
    CauHinh c;
    if (cau_hinh_nap("/etc/ct.conf", &c) != 0) {
        fprintf(stderr, "khong khoi dong duoc\n");    /* vo dung */
        return 1;
    }
}

/* Nguoi dung nhan duoc: "khong khoi dong duoc".
   Nguyen nhan that: khong co quyen doc /etc/ct.conf.
   Ho khong the tu sua. */

#Khi nào dừng chương trình

Tình huốngXử lý hay dừngVì sao
Tệp người dùng chỉ định không tồn tạiXử lýNgười dùng gõ nhầm, báo và cho thử lại
Mạng ngắt giữa chừngXử lýBình thường, thử lại hoặc báo
Dữ liệu vào sai định dạngXử lýĐầu vào không tin được là chuyện thường
malloc trả về NULLTuỳXem bên dưới
Bất biến nội bộ bị vi phạmDừngChương trình đã ở trạng thái không hiểu được
Đối số NULL cho hàm nội bộDừng, bằng assertĐó là lỗi lập trình, không phải lỗi lúc chạy
Chỉ số vượt mảng trong mã của bạnDừngTiếp tục là hỏng bộ nhớ

Tự làm thử

  1. Chạy bon.c và giải thích vì sao cách 3 báo loi_cuoi=0 cho đầu vào tràn.
  2. Sửa doc_so_2 và doc_so_3 để chúng phát hiện được tràn.
  3. Viết lại một hàm của bạn có ba return lỗi thành mẫu goto.
  4. Thêm warn_unused_result vào các hàm trả mã lỗi và đếm số cảnh báo mới.
  5. Cài cấu trúc Loi có chuỗi nguyên nhân ba tầng và in ra đầy đủ.
  6. Tìm trong dự án của bạn một chỗ ghi nhật ký ở nhiều tầng cho cùng một lỗi, và dồn về một chỗ.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Bốn chiến lược; mặc định dùng mã lỗi trả về cộng tham số ra.
  • Giá trị canh gác chỉ dùng được khi miền giá trị có vùng chắc chắn không hợp lệ.
  • Biến lỗi toàn cục phải được xoá trước khi gọi và lưu lại ngay sau khi lỗi.
  • goto dọn dẹp: nhảy về phía trước, khởi tạo NULL, tên nhãn nói việc, thả ngược thứ tự cấp.
  • Xử lý hoặc truyền lên, không làm cả hai; ghi nhật ký một lần ở tầng biết phải làm gì tiếp.