Xử lý lỗi trong mã C


152

Bạn nghĩ gì về "cách thực hành tốt nhất" khi nói đến lỗi xử lý lỗi một cách nhất quán trong thư viện C.

Có hai cách tôi đã nghĩ đến:

Luôn trả lại mã lỗi. Một chức năng điển hình sẽ trông như thế này:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

Luôn luôn cung cấp một cách tiếp cận con trỏ lỗi:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

Khi sử dụng cách tiếp cận đầu tiên, có thể viết mã như thế này trong đó kiểm tra xử lý lỗi được đặt trực tiếp vào lệnh gọi hàm:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

Cái nào nhìn tốt hơn mã xử lý lỗi ở đây.

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}

Tuy nhiên, tôi nghĩ rằng việc sử dụng giá trị trả về để trả về dữ liệu làm cho mã dễ đọc hơn, Rõ ràng là có gì đó được ghi vào biến kích thước trong ví dụ thứ hai.

Bạn có ý tưởng nào về lý do tại sao tôi nên thích bất kỳ phương pháp tiếp cận nào hoặc có thể trộn chúng hoặc sử dụng cái gì khác không? Tôi không phải là người hâm mộ các trạng thái lỗi toàn cầu vì nó có xu hướng khiến việc sử dụng thư viện trở nên khó khăn hơn.

EDIT: Những ý tưởng cụ thể của C ++ về điều này cũng sẽ rất thú vị khi nghe về miễn là chúng không liên quan đến ngoại lệ vì hiện tại nó không phải là một lựa chọn cho tôi ...


Tôi mới chỉ học C được khoảng hai tuần, nhưng cảm giác tôi nhận được là các tham số OUT là giá trị trả về của defacto cho phần lớn các hàm, vì nó tránh được chi phí trả về các giá trị phụ và giảm thiểu sự cần thiết phải giải phóng bộ nhớ vì hầu hết các biến nằm trên ngăn xếp. Vì vậy, vì tôi không sử dụng "return" cho giá trị thực của hàm, nên tôi tự do sử dụng nó để xử lý lỗi trong hầu hết thời gian.
Joel Roberts

Câu trả lời:


74

Tôi thích lỗi như cách trả về giá trị. Nếu bạn đang thiết kế api và bạn muốn sử dụng thư viện của mình không đau đớn nhất có thể, hãy nghĩ về những bổ sung này:

  • lưu trữ tất cả các trạng thái lỗi có thể trong một enum typedef'ed và sử dụng nó trong lib của bạn. Đừng chỉ trả lại số nguyên hoặc thậm chí tệ hơn, trộn số nguyên hoặc liệt kê khác với mã trả về.

  • cung cấp một chức năng chuyển đổi lỗi thành một cái gì đó con người có thể đọc được. Có thể đơn giản. Chỉ cần lỗi-enum trong, const char * out.

  • Tôi biết ý tưởng này làm cho việc sử dụng đa luồng hơi khó khăn, nhưng sẽ rất tuyệt nếu lập trình viên ứng dụng có thể thiết lập một cuộc gọi lại lỗi toàn cầu. Bằng cách đó, họ sẽ có thể đặt một điểm dừng vào cuộc gọi lại trong các phiên săn lỗi.

Hy vọng nó giúp.


5
Tại sao bạn nói, "ý tưởng này làm cho việc sử dụng đa luồng hơi khó khăn." Phần nào được làm khó bằng đa luồng? Bạn có thể đưa ra một ví dụ nhanh chóng?
SayeedHussain

1
@crypticcoder Nói một cách đơn giản: một cuộc gọi lại lỗi toàn cầu có thể được gọi trong bất kỳ bối cảnh luồng nào. Nếu bạn chỉ in ra lỗi, bạn sẽ không gặp phải bất kỳ vấn đề nào. Nếu bạn cố gắng khắc phục sự cố, bạn sẽ phải tìm ra luồng gọi nào gây ra lỗi và điều đó gây khó khăn.
Nils Pipenbrinck

9
Nếu bạn muốn thông báo thêm chi tiết về lỗi thì sao? Ví dụ: bạn có lỗi trình phân tích cú pháp và muốn cung cấp số dòng và cột của lỗi cú pháp và cách in tất cả độc đáo.
panzi

1
@panzi thì rõ ràng bạn cần trả về một struct (hoặc sử dụng một con trỏ out nếu struct thực sự lớn) và có một hàm để định dạng struct dưới dạng một chuỗi.
Cầu thủ chạy cánh Sendon

Tôi chứng minh 2 viên đạn đầu tiên của bạn bằng mã ở đây: stackoverflow.com/questions/385975/error-handling-in-c-code/ Kẻ
Gabriel Staples

92

Tôi đã sử dụng cả hai cách tiếp cận và cả hai đều hoạt động tốt với tôi. Bất cứ ai tôi sử dụng, tôi luôn cố gắng áp dụng nguyên tắc này:

Nếu lỗi duy nhất có thể là lỗi lập trình viên, đừng trả về mã lỗi, hãy sử dụng các xác nhận bên trong hàm.

Một xác nhận xác nhận các đầu vào truyền đạt rõ ràng những gì hàm mong đợi, trong khi kiểm tra lỗi quá nhiều có thể che khuất logic chương trình. Quyết định làm gì cho tất cả các trường hợp lỗi khác nhau có thể thực sự làm phức tạp thiết kế. Tại sao phải tìm ra cách functionX xử lý một con trỏ null nếu thay vào đó bạn có thể nhấn mạnh rằng lập trình viên không bao giờ vượt qua một?


1
Có một ví dụ về khẳng định trong C? (Tôi rất xanh đến C)
thomthom

Nó thường đơn giản như assert(X)trong đó X là bất kỳ câu lệnh C hợp lệ nào mà bạn muốn là đúng. xem stackoverflow.com/q/1571340/10394 .
AShelly

14
Ugh, tuyệt đối không bao giờ sử dụng khẳng định trong mã thư viện ! Ngoài ra, không trộn lẫn nhiều kiểu xử lý lỗi khác nhau trong một đoạn mã như những người khác đã làm
mirabilos

10
Tôi chắc chắn đồng ý về việc không pha trộn phong cách. Tôi tò mò về lý luận của bạn về khẳng định. Nếu tài liệu chức năng của tôi nói "đối số X không phải là NULL" hoặc "Y phải là thành viên của enum này", thì điều gì sai với assert(X!=NULL);hoặc assert(Y<enumtype_MAX);? Xem câu trả lời này trên các lập trình viên và câu hỏi mà nó liên kết để biết thêm chi tiết về lý do tại sao tôi nghĩ rằng đây là cách đúng đắn để đi.
HỎI

8
@AShelly Vấn đề với khẳng định rằng chúng thường không có trong các bản dựng phát hành.
Calmarius

29

Có một bộ slide tuyệt vời từ CERT của CMU với các khuyến nghị khi nào nên sử dụng từng kỹ thuật xử lý lỗi C (và C ++) phổ biến. Một trong những slide tốt nhất là cây quyết định này:

Xử lý lỗi cây quyết định

Cá nhân tôi sẽ thay đổi hai điều về lưu lượng này.

Đầu tiên, tôi sẽ làm rõ rằng đôi khi các đối tượng nên sử dụng các giá trị trả về để chỉ ra lỗi. Nếu một hàm chỉ trích xuất dữ liệu từ một đối tượng nhưng không làm biến đổi đối tượng, thì tính toàn vẹn của chính đối tượng đó không có nguy cơ và chỉ ra lỗi sử dụng giá trị trả về là phù hợp hơn.

Thứ hai, không phải lúc nào cũng thích hợp để sử dụng các ngoại lệ trong C ++. Các ngoại lệ là tốt vì chúng có thể giảm số lượng mã nguồn dành cho xử lý lỗi, chúng hầu như không ảnh hưởng đến chữ ký hàm và chúng rất linh hoạt trong những dữ liệu nào chúng có thể vượt qua trong cuộc gọi. Mặt khác, ngoại lệ có thể không phải là lựa chọn đúng đắn vì một vài lý do:

  1. Các ngoại lệ C ++ có ngữ nghĩa rất đặc biệt. Nếu bạn không muốn những ngữ nghĩa đó, thì ngoại lệ C ++ là một lựa chọn tồi. Một ngoại lệ phải được xử lý ngay lập tức sau khi bị ném và thiết kế ủng hộ trường hợp một lỗi sẽ cần phải giải phóng nút gọi một vài cấp.

  2. Các hàm C ++ ném ngoại lệ sau này không thể được bọc để không ném ngoại lệ, ít nhất là không phải trả toàn bộ chi phí ngoại lệ. Các hàm trả về mã lỗi có thể được gói để ném ngoại lệ C ++, làm cho chúng linh hoạt hơn. C ++ newcó quyền này bằng cách cung cấp một biến thể không ném.

  3. Các ngoại lệ C ++ tương đối đắt tiền nhưng nhược điểm này chủ yếu là quá mức đối với các chương trình sử dụng ngoại lệ hợp lý. Một chương trình chỉ đơn giản là không nên ném ngoại lệ vào một codepath trong đó hiệu năng là một mối quan tâm. Việc chương trình của bạn có thể báo lỗi và thoát nhanh như thế nào không quan trọng.

  4. Đôi khi ngoại lệ C ++ không có sẵn. Chúng thực sự không có sẵn trong triển khai C ++ của một người, hoặc các nguyên tắc mã của một người cấm chúng.


Vì câu hỏi ban đầu là về bối cảnh đa luồng, tôi nghĩ rằng kỹ thuật chỉ báo lỗi cục bộ (những gì được mô tả trong câu trả lời của SirDarius ) đã bị đánh giá thấp trong các câu trả lời ban đầu. Đó là chủ đề an toàn, không buộc lỗi phải được xử lý ngay lập tức bởi người gọi và có thể bó dữ liệu tùy ý mô tả lỗi. Nhược điểm là nó phải được giữ bởi một đối tượng (hoặc tôi cho rằng bằng cách nào đó được liên kết bên ngoài) và được cho là dễ dàng bỏ qua hơn một mã trả về.


5
Bạn có thể lưu ý rằng các tiêu chuẩn mã hóa C ++ của Google vẫn nói Chúng tôi không sử dụng ngoại lệ C ++.
Jonathan Leffler

19

Tôi sử dụng cách tiếp cận đầu tiên bất cứ khi nào tôi tạo một thư viện. Có một số lợi thế của việc sử dụng enum typedef'ed làm mã trả về.

  • Nếu hàm trả về một đầu ra phức tạp hơn, chẳng hạn như một mảng và độ dài của nó, bạn không cần phải tạo các cấu trúc tùy ý để trả về.

    rc = func(..., int **return_array, size_t *array_length);
  • Nó cho phép xử lý lỗi đơn giản, chuẩn hóa.

    if ((rc = func(...)) != API_SUCCESS) {
       /* Error Handling */
    }
  • Nó cho phép xử lý lỗi đơn giản trong chức năng thư viện.

    /* Check for valid arguments */
    if (NULL == return_array || NULL == array_length)
        return API_INVALID_ARGS;
  • Sử dụng enum typedef'ed cũng cho phép hiển thị tên enum trong trình gỡ lỗi. Điều này cho phép gỡ lỗi dễ dàng hơn mà không cần phải liên tục tham khảo tệp tiêu đề. Có một chức năng để dịch enum này thành một chuỗi cũng hữu ích.

Vấn đề quan trọng nhất bất kể cách tiếp cận được sử dụng là phải nhất quán. Điều này áp dụng cho việc đặt tên hàm và đối số, sắp xếp đối số và xử lý lỗi.


9

Sử dụng setjmp .

http://en.wikipedia.org/wiki/setjmp.h

http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}

2
Khối mã thứ hai dựa trên phiên bản mã trước đó tại trang của Francesco Nidito được tham chiếu ở đầu câu trả lời. Các ETRYmã đã được sửa đổi kể từ khi câu trả lời này đã được viết.
Jonathan Leffler

2
Setjmp là một chiến lược xử lý lỗi khủng khiếp. Nó đắt tiền, dễ bị lỗi (w / không biến đổi cục bộ không giữ lại các giá trị đã thay đổi và tất cả) và rò rỉ tài nguyên nếu bạn phân bổ bất kỳ giữa các lệnh gọi setjmp và longjmp. Bạn sẽ có thể thực hiện như 30 lần trả lại và kiểm tra int-val trước khi bạn thu lại chi phí của sigjmp / longjmp. Hầu hết các cuộc gọi không đi sâu đến thế đặc biệt là nếu bạn không quá nặng nề về đệ quy (và nếu bạn làm thế, bạn có vấn đề hoàn hảo ngoài chi phí trả lại + kiểm tra).
PSkocik

1
Nếu bạn vồ bộ nhớ rồi ném, bộ nhớ sẽ bị rò rỉ mãi mãi. Cũng setjmprất tốn kém, ngay cả khi không có lỗi nào bị ném, nó sẽ tiêu tốn khá nhiều thời gian của CPU và không gian ngăn xếp. Khi sử dụng gcc cho Windows, bạn có thể chọn giữa các phương thức xử lý ngoại lệ khác nhau cho C ++, một trong số đó dựa trên cơ sở setjmpvà nó làm cho mã của bạn chậm hơn tới 30% trong thực tế.
Mecki

7

Cá nhân tôi thích cách tiếp cận trước đây (trả về một chỉ báo lỗi).

Khi cần thiết, kết quả trả về sẽ chỉ ra rằng đã xảy ra lỗi, với một hàm khác đang được sử dụng để tìm ra lỗi chính xác.

Trong ví dụ getSize () của bạn, tôi cho rằng kích thước phải luôn bằng 0 hoặc dương, do đó, việc trả về kết quả âm có thể chỉ ra lỗi, giống như các lệnh gọi hệ thống UNIX.

Tôi không thể nghĩ ra bất kỳ thư viện nào tôi đã sử dụng theo cách tiếp cận sau với một đối tượng lỗi được truyền vào dưới dạng con trỏ. stdio, v.v ... tất cả đều đi với giá trị trả về.


1
Đối với bản ghi, một thư viện tôi đã thấy sử dụng phương pháp sau là API lập trình Maya. Đó là một thư viện c ++ chứ không phải C mặc dù. Nó khá không nhất quán trong cách xử lý lỗi của nó và đôi khi lỗi được truyền dưới dạng giá trị trả về và lần khác nó chuyển kết quả dưới dạng tham chiếu.
Laserallan

1
đừng quên strtod, ok, đối số cuối cùng không chỉ để chỉ ra lỗi mà còn xảy ra.
quinmars

7

Khi tôi viết chương trình, trong quá trình khởi tạo, tôi thường quay ra một luồng để xử lý lỗi và khởi tạo một cấu trúc đặc biệt cho các lỗi, bao gồm cả khóa. Sau đó, khi tôi phát hiện ra lỗi, thông qua các giá trị trả về, tôi nhập thông tin từ ngoại lệ vào cấu trúc và gửi SIGIO đến luồng xử lý ngoại lệ, sau đó xem liệu tôi có thể tiếp tục thực thi không. Nếu tôi không thể, tôi gửi SIGURG đến chuỗi ngoại lệ, điều này dừng chương trình một cách duyên dáng.


7

Trả về mã lỗi là cách tiếp cận thông thường để xử lý lỗi trong C.

Nhưng gần đây chúng tôi đã thử nghiệm với cách tiếp cận con trỏ lỗi đi.

Nó có một số lợi thế so với cách tiếp cận giá trị trả về:

  • Bạn có thể sử dụng giá trị trả về cho các mục đích có ý nghĩa hơn.

  • Phải viết ra tham số lỗi đó nhắc nhở bạn xử lý lỗi hoặc truyền nó. (Bạn không bao giờ quên kiểm tra giá trị trả lại của fclose, phải không?)

  • Nếu bạn sử dụng một con trỏ lỗi, bạn có thể chuyển nó xuống khi bạn gọi các hàm. Nếu bất kỳ chức năng nào thiết lập nó, giá trị sẽ không bị mất.

  • Bằng cách đặt điểm dừng dữ liệu trên biến lỗi, bạn có thể biết lỗi xảy ra ở đâu trước. Bằng cách đặt điểm dừng có điều kiện, bạn cũng có thể bắt lỗi cụ thể.

  • Nó giúp dễ dàng tự động hóa việc kiểm tra xem bạn có xử lý tất cả các lỗi hay không. Quy ước mã có thể buộc bạn gọi con trỏ lỗi của mình errvà nó phải là đối số cuối cùng. Vì vậy, tập lệnh có thể khớp với chuỗi err);sau đó kiểm tra xem nó có theo sau không if (*err. Trên thực tế, trong thực tế, chúng tôi đã tạo ra một macro được gọi là CER(check err return) và CEG(check err goto). Vì vậy, bạn không cần phải gõ nó luôn khi chúng tôi chỉ muốn quay lại lỗi và có thể làm giảm sự lộn xộn thị giác.

Không phải tất cả các chức năng trong mã của chúng tôi đều có tham số gửi đi này. Điều tham số gửi đi này được sử dụng cho các trường hợp bạn thường ném ngoại lệ.


6

Tôi đã làm rất nhiều chương trình C trong quá khứ. Và tôi thực sự đã đánh giá giá trị trả về mã lỗi. Nhưng có một số cạm bẫy có thể xảy ra:

  • Số lỗi trùng lặp, điều này có thể được giải quyết với một tập tin error.h toàn cầu.
  • Quên kiểm tra mã lỗi, điều này sẽ được giải quyết với một cluebat và thời gian sửa lỗi dài. Nhưng cuối cùng bạn sẽ học được (hoặc bạn sẽ biết rằng người khác sẽ thực hiện việc gỡ lỗi).

2
vấn đề thứ hai có thể được giải quyết bằng mức cảnh báo trình biên dịch thích hợp, cơ chế xem lại mã thích hợp và bằng các công cụ phân tích mã tĩnh.
Ilya

1
Bạn cũng có thể làm việc theo nguyên tắc: nếu hàm API được gọi và giá trị trả về không được kiểm tra, có lỗi.
Jonathan Leffler

6

Cách tiếp cận UNIX tương tự như đề xuất thứ hai của bạn. Trả về kết quả hoặc một giá trị "nó đã sai". Chẳng hạn, open sẽ trả về bộ mô tả tệp khi thành công hoặc -1 khi thất bại. Về thất bại nó cũng đặt ra errno, một số nguyên toàn cầu từ bên ngoài để chỉ ra đó thất bại xảy ra.

Đối với những gì nó có giá trị, Ca cao cũng đã áp dụng một cách tiếp cận tương tự. Một số phương thức trả về BOOL và lấy NSError **tham số, để khi thất bại, chúng đặt lỗi và trả về NO. Sau đó, việc xử lý lỗi trông như sau:

NSError *error = nil;
if ([myThing doThingError: &error] == NO)
{
  // error handling
}

đó là một nơi nào đó giữa hai lựa chọn của bạn :-).



5

Đây là một cách tiếp cận mà tôi nghĩ là thú vị, trong khi yêu cầu một số kỷ luật.

Giả định này là một biến loại xử lý là trường hợp vận hành tất cả các hàm API.

Ý tưởng là cấu trúc phía sau tay cầm lưu trữ lỗi trước đó dưới dạng cấu trúc với dữ liệu cần thiết (mã, thông báo ...) và người dùng được cung cấp một hàm trả về một con trỏ tp đối tượng lỗi này. Mỗi thao tác sẽ cập nhật đối tượng nhọn để người dùng có thể kiểm tra trạng thái của nó mà không cần gọi các chức năng. Trái ngược với mẫu errno, mã lỗi không phải là toàn cục, điều này làm cho cách tiếp cận luồng an toàn, miễn là mỗi tay cầm được sử dụng đúng cách.

Thí dụ:

MyHandle * h = MyApiCreateHandle();

/* first call checks for pointer nullity, since we cannot retrieve error code
   on a NULL pointer */
if (h == NULL)
     return 0; 

/* from here h is a valid handle */

/* get a pointer to the error struct that will be updated with each call */
MyApiError * err = MyApiGetError(h);


MyApiFileDescriptor * fd = MyApiOpenFile("/path/to/file.ext");

/* we want to know what can go wrong */
if (err->code != MyApi_ERROR_OK) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

MyApiRecord record;

/* here the API could refuse to execute the operation if the previous one
   yielded an error, and eventually close the file descriptor itself if
   the error is not recoverable */
MyApiReadFileRecord(h, &record, sizeof(record));

/* we want to know what can go wrong, here using a macro checking for failure */
if (MyApi_FAILED(err)) {
    fprintf(stderr, "(%d) %s\n", err->code, err->message);
    MyApiDestroy(h);
    return 0;
}

4

Cách tiếp cận đầu tiên là tốt hơn IMHO:

  • Đó là cách dễ dàng hơn để viết chức năng theo cách đó. Khi bạn nhận thấy một lỗi ở giữa hàm, bạn chỉ cần trả về một giá trị lỗi. Trong cách tiếp cận thứ hai, bạn cần gán giá trị lỗi cho một trong các tham số và sau đó trả về một cái gì đó .... nhưng bạn sẽ trả về cái gì - bạn không có giá trị chính xác và bạn không trả về giá trị lỗi.
  • nó phổ biến hơn nên sẽ dễ hiểu hơn, duy trì

4

Tôi chắc chắn thích giải pháp đầu tiên:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

tôi sẽ sửa đổi nó một chút, thành:

int size;
MYAPIError rc;

rc = getObjectSize(h, &size)
if ( rc != MYAPI_SUCCESS) {
  // Error handling
}

Ngoài ra, tôi sẽ không bao giờ trộn lẫn giá trị trả về hợp pháp với lỗi ngay cả khi hiện tại phạm vi của hàm cho phép bạn làm như vậy, bạn không bao giờ biết cách thực hiện chức năng sẽ diễn ra trong tương lai.

Và nếu chúng ta đã nói về việc xử lý lỗi, tôi sẽ đề xuất goto Error;là mã xử lý lỗi, trừ khi một số undochức năng có thể được gọi để xử lý lỗi chính xác.


3

Những gì bạn có thể làm thay vì trả lại lỗi của mình và do đó cấm bạn trả lại dữ liệu với chức năng của mình, đang sử dụng trình bao bọc cho loại trả về của bạn:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

Sau đó, trong hàm được gọi:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

Xin lưu ý rằng với phương pháp sau, trình bao bọc sẽ có kích thước MyType cộng với một byte (trên hầu hết các trình biên dịch), khá có lợi; và bạn sẽ không phải đẩy một đối số khác trên ngăn xếp khi bạn gọi hàm của mình ( returnedSizehoặc returnedErrortrong cả hai phương thức bạn đã trình bày).


3

Đây là một chương trình đơn giản để chứng minh 2 viên đạn đầu tiên của câu trả lời của Nils Pipenbrinck tại đây .

2 viên đạn đầu tiên của anh là:

  • lưu trữ tất cả các trạng thái lỗi có thể trong một enum typedef'ed và sử dụng nó trong lib của bạn. Đừng chỉ trả lại số nguyên hoặc thậm chí tệ hơn, trộn số nguyên hoặc liệt kê khác với mã trả về.

  • cung cấp một chức năng chuyển đổi lỗi thành một cái gì đó con người có thể đọc được. Có thể đơn giản. Chỉ cần lỗi-enum trong, const char * out.

Giả sử bạn đã viết một mô-đun có tên mymodule. Đầu tiên, trong mymodule.h, bạn xác định mã lỗi dựa trên enum của mình và bạn viết một số chuỗi lỗi tương ứng với các mã này. Ở đây tôi đang sử dụng một chuỗi các chuỗi C ( char *), chỉ hoạt động tốt nếu mã lỗi dựa trên enum đầu tiên của bạn có giá trị 0 và bạn không thao tác các số sau đó. Nếu bạn sử dụng số mã lỗi có khoảng trống hoặc các giá trị bắt đầu khác, bạn sẽ chỉ cần thay đổi từ sử dụng mảng chuỗi C được ánh xạ (như tôi làm dưới đây) sang sử dụng hàm sử dụng câu lệnh switch hoặc if / other if để ánh xạ từ mã lỗi enum sang chuỗi C có thể in (mà tôi không thể hiện). Sự lựa chọn là của bạn.

mymodule.h

/// @brief Error codes for library "mymodule"
typedef enum mymodule_error_e
{
    /// No error
    MYMODULE_ERROR_OK = 0,
    
    /// Invalid arguments (ex: NULL pointer where a valid pointer is required)
    MYMODULE_ERROR_INVARG,

    /// Out of memory (RAM)
    MYMODULE_ERROR_NOMEM,

    /// Make up your error codes as you see fit
    MYMODULE_ERROR_MYERROR, 

    // etc etc
    
    /// Total # of errors in this list (NOT AN ACTUAL ERROR CODE);
    /// NOTE: that for this to work, it assumes your first error code is value 0 and you let it naturally 
    /// increment from there, as is done above, without explicitly altering any error values above
    MYMODULE_ERROR_COUNT,
} mymodule_error_t;

// Array of strings to map enum error types to printable strings
// - see important NOTE above!
const char* const MYMODULE_ERROR_STRS[] = 
{
    "MYMODULE_ERROR_OK",
    "MYMODULE_ERROR_INVARG",
    "MYMODULE_ERROR_NOMEM",
    "MYMODULE_ERROR_MYERROR",
};

// To get a printable error string
const char* mymodule_error_str(mymodule_error_t err);

// Other functions in mymodule
mymodule_error_t mymodule_func1(void);
mymodule_error_t mymodule_func2(void);
mymodule_error_t mymodule_func3(void);

mymodule.c chứa chức năng ánh xạ của tôi để ánh xạ từ mã lỗi enum sang chuỗi C có thể in:

mymodule.c

#include <stdio.h>

/// @brief      Function to get a printable string from an enum error type
/// @param[in]  err     a valid error code for this module
/// @return     A printable C string corresponding to the error code input above, or NULL if an invalid error code
///             was passed in
const char* mymodule_error_str(mymodule_error_t err)
{
    const char* err_str = NULL;

    // Ensure error codes are within the valid array index range
    if (err >= MYMODULE_ERROR_COUNT)
    {
        goto done;
    }

    err_str = MYMODULE_ERROR_STRS[err];

done:
    return err_str;
}

// Let's just make some empty dummy functions to return some errors; fill these in as appropriate for your 
// library module

mymodule_error_t mymodule_func1(void)
{
    return MYMODULE_ERROR_OK;
}

mymodule_error_t mymodule_func2(void)
{
    return MYMODULE_ERROR_INVARG;
}

mymodule_error_t mymodule_func3(void)
{
    return MYMODULE_ERROR_MYERROR;
}

main.c chứa một chương trình thử nghiệm để thể hiện việc gọi một số chức năng và in một số mã lỗi từ chúng:

C chính

#include <stdio.h>

int main()
{
    printf("Demonstration of enum-based error codes in C (or C++)\n");

    printf("err code from mymodule_func1() = %s\n", mymodule_error_str(mymodule_func1()));
    printf("err code from mymodule_func2() = %s\n", mymodule_error_str(mymodule_func2()));
    printf("err code from mymodule_func3() = %s\n", mymodule_error_str(mymodule_func3()));

    return 0;
}

Đầu ra:

Trình diễn mã lỗi dựa trên enum trong mã lỗi C (hoặc C ++)
từ mymodule_func1 () = MYMODULE_ERROR_OK
mã lỗi từ mymodule_func2 () = MYMODULE_ERROR_INVARG
mã lỗi từ mymod

Người giới thiệu:

Bạn có thể tự chạy mã này tại đây: https://onlinegdb.com/ByEbKLupS .


2

Ngoài những gì đã nói, trước khi trả lại mã lỗi của bạn, hãy tắt một xác nhận hoặc chẩn đoán tương tự khi trả về lỗi, vì nó sẽ giúp việc truy tìm dễ dàng hơn rất nhiều. Cách tôi làm là để có một xác nhận tùy chỉnh vẫn được biên dịch khi phát hành nhưng chỉ bị hủy khi phần mềm ở chế độ chẩn đoán, với tùy chọn báo cáo âm thầm với tệp nhật ký hoặc tạm dừng trên màn hình.

Cá nhân tôi trả về mã lỗi dưới dạng số nguyên âm với no_error là 0, nhưng nó khiến bạn gặp phải lỗi sau

if (MyFunc())
 DoSomething();

Một giải pháp thay thế là lỗi luôn được trả về 0 và sử dụng hàm LastError () để cung cấp chi tiết về lỗi thực tế.


2

Tôi đã tham gia hỏi đáp này nhiều lần và muốn đóng góp một câu trả lời toàn diện hơn. Tôi nghĩ cách tốt nhất để nghĩ về điều này là làm thế nào để trả lại lỗi cho người gọi và những gì bạn trả lại.

Làm sao

Có 3 cách để trả về thông tin từ một hàm:

  1. Giá trị trả lại
  2. Ra tranh luận
  3. Out of Band, bao gồm goto không cục bộ (setjmp / longjmp), tệp hoặc biến phạm vi toàn cầu, hệ thống tệp, v.v.

Giá trị trả lại

Bạn chỉ có thể trả về giá trị là một đối tượng duy nhất, tuy nhiên, nó có thể là một phức hợp tùy ý. Dưới đây là một ví dụ về chức năng trả về lỗi:

  enum error hold_my_beer();

Một lợi ích của các giá trị trả về là nó cho phép xâu chuỗi các cuộc gọi để xử lý lỗi ít xâm phạm hơn:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

Điều này không chỉ về khả năng đọc, mà còn có thể cho phép xử lý một loạt các con trỏ hàm như vậy một cách thống nhất.

Ra tranh luận

Bạn có thể trả lại nhiều hơn thông qua nhiều đối tượng thông qua các đối số, nhưng cách tốt nhất là đề xuất để giữ cho tổng số đối số ở mức thấp (giả sử <= 4):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

Ra khỏi ban nhạc

Với setjmp () bạn xác định một vị trí và cách bạn muốn xử lý một giá trị int và bạn chuyển điều khiển đến vị trí đó thông qua longjmp (). Xem cách sử dụng thực tế của setjmp và longjmp trong C .

  1. Chỉ tiêu
  2. Vật
  3. Gọi lại

Chỉ tiêu

Một chỉ báo lỗi chỉ cho bạn biết rằng có một vấn đề nhưng không có gì về bản chất của vấn đề đã nói:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

Đây là cách ít mạnh mẽ nhất để một chức năng truyền đạt trạng thái lỗi, tuy nhiên, hoàn hảo nếu người gọi không thể phản hồi lỗi theo cách tốt nghiệp.

Mã lỗi cho người gọi biết về bản chất của vấn đề và có thể cho phép phản hồi phù hợp (từ phần trên). Nó có thể là giá trị trả về hoặc giống như ví dụ look_ma () phía trên một đối số lỗi.

Vật

Với một đối tượng lỗi, người gọi có thể được thông báo về các vấn đề phức tạp tùy ý. Ví dụ, một mã lỗi và một thông điệp phù hợp với con người có thể đọc được. Nó cũng có thể thông báo cho người gọi rằng có nhiều lỗi xảy ra hoặc lỗi trên mỗi mục khi xử lý bộ sưu tập:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

Thay vì phân bổ trước mảng lỗi, bạn cũng có thể (tái) phân bổ nó một cách linh hoạt khi cần thiết.

Gọi lại

Gọi lại là cách mạnh mẽ nhất để xử lý lỗi, vì bạn có thể nói cho hàm biết hành vi nào bạn muốn thấy xảy ra khi có sự cố. Một đối số gọi lại có thể được thêm vào từng chức năng hoặc nếu bạn chỉ yêu cầu tùy chỉnh cho mỗi phiên bản của một cấu trúc như thế này:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

Một lợi ích thú vị của một cuộc gọi lại là nó có thể được gọi nhiều lần hoặc không có gì trong trường hợp không có lỗi trong đó không có chi phí trên đường dẫn hạnh phúc.

Tuy nhiên, có một sự đảo ngược của kiểm soát. Mã cuộc gọi không biết nếu gọi lại được gọi. Như vậy, nó cũng có thể có ý nghĩa để sử dụng một chỉ báo.


1

EDIT: Nếu bạn chỉ cần truy cập vào lỗi cuối cùng và bạn không làm việc trong môi trường đa luồng.

Bạn chỉ có thể trả về true / false (hoặc một số loại #define nếu bạn làm việc trong C và không hỗ trợ các biến bool) và có bộ đệm Lỗi toàn cầu sẽ giữ lỗi cuối cùng:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}

Thật vậy, nhưng nó không phải C có thể được cung cấp bởi HĐH hay không. Nếu bạn đang làm việc trên các hệ điều hành thời gian thực, ví dụ như bạn không có nó.
Ilya

1

Cách tiếp cận thứ hai cho phép trình biên dịch tạo ra mã được tối ưu hóa hơn, bởi vì khi địa chỉ của một biến được truyền đến một hàm, trình biên dịch không thể giữ giá trị của nó trong (các) thanh ghi trong các cuộc gọi tiếp theo đến các hàm khác. Mã hoàn thành thường chỉ được sử dụng một lần, ngay sau cuộc gọi, trong khi dữ liệu "thực" được trả về từ cuộc gọi có thể được sử dụng thường xuyên hơn


1

Tôi thích xử lý lỗi trong C bằng cách sử dụng kỹ thuật sau:

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

   // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

   // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

   // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

   // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

   // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

   // good? return list or else return NULL
    return (good ? list : NULL);
}

Nguồn: http://blog.staila.com/?p=114


1
Kỹ thuật tốt. Tôi thấy thậm chí gọn gàng hơn với gotothay vì lặp đi lặp lại if. Tài liệu tham khảo: một , hai .
Ant_222

0

Ngoài ra, các câu trả lời tuyệt vời khác, tôi khuyên bạn nên thử tách cờ lỗi và mã lỗi để lưu một dòng trên mỗi cuộc gọi, nghĩa là:

if( !doit(a, b, c, &errcode) )
{   (* handle *)
    (* thine  *)
    (* error  *)
}

Khi bạn có nhiều kiểm tra lỗi, sự đơn giản hóa nhỏ này thực sự có ích.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.