2009-08-02 3 views
13

Tôi đến từ một nền C#, nhưng tôi đang học C vào lúc này. Trong C#, khi người ta muốn báo hiệu rằng một lỗi đã xảy ra, bạn ném một ngoại lệ. Nhưng bạn làm gì trong C?C: Làm thế nào để bạn mô phỏng một 'ngoại lệ'?

Ví dụ: bạn có ngăn xếp với các hàm pushpop. Cách tốt nhất để báo hiệu là ngăn xếp trống trong một pop là gì? Bạn quay trở lại từ chức năng đó là gì?

double pop(void) 
{ 
    if(sp > 0) 
     return val[--sp]; 
    else { 
     printf("error: stack empty\n"); 
     return 0.0; 
    } 
} 

K&R 's ví dụ từ trang 77 (mã trên) trả về một 0.0. Nhưng điều gì sẽ xảy ra nếu người dùng đã đẩy 0.0 trước đó lên ngăn xếp, làm cách nào để bạn biết liệu ngăn xếp có trống hay liệu giá trị chính xác có được trả về không?

Trả lời

15

hành vi ngoại lệ giống như trong C được thực hiện thông qua setjmp/longjmp. Tuy nhiên, những gì bạn thực sự muốn ở đây là một mã lỗi. Nếu tất cả các giá trị này là có khả năng trả lại, sau đó bạn có thể muốn tận trong một out-tham số như một con trỏ, và sử dụng để trả về giá trị, như vậy:

int pop(double* outval) 
{ 
     if(outval == 0) return -1; 
     if(sp > 0) 
       *outval = val[--sp]; 
     else { 
       printf("error: stack empty\n"); 
       return -1; 
     } 
     return 0; 
} 

Không lý tưởng, rõ ràng, nhưng đó là những hạn chế Ngoài ra, nếu bạn đi trên con đường này, bạn có thể muốn xác định hằng số tượng trưng cho mã lỗi của mình (hoặc sử dụng một số the standard ones), để người dùng có thể phân biệt giữa "ngăn xếp trống" và "bạn đã cho tôi một con trỏ null, dumbass ".

+2

Tôi không đồng ý, một chút, bởi vì ngay cả khi tôi hiểu ý bạn, tôi sẽ không cho ai đó đến từ java/C# land giả định rằng setjmp/longjmp là bất kỳ cách nào 'giải pháp' cho 'ngoại lệ của tôi là ở đâu?' – Jonke

+0

Lưu ý rằng rất ít mã C sử dụng setjmp/longjmp vì lý do lịch sử. Mã lỗi là cách C thông thường. Mã lỗi không tốt lắm; thiếu ngoại lệ trong C là lý do chính khiến nhiều ngôn ngữ hiện đại trở nên tốt hơn. – Nelson

+0

ANSI đã mã hóa 'setjmp 'để đảm bảo hoạt động (ít nhất nếu trình biên dịch phù hợp với tiêu chuẩn), nhưng chuyển đổi ngăn xếp và đồng thời có chưa được chuẩn hóa, do đó, vẫn còn là vấn đề phải viết chủ đề an toàn gói cho C (thậm chí sử dụng asm để chuyển đổi ngữ cảnh) vì trình biên dịch * có thể * (mặc dù nó có thể khó xảy ra) thực hiện tối ưu hóa và biến đổi phá vỡ các giả định trong gói luồng. – Jonke

4

Một cách tiếp cận là chỉ định rằng pop() có hành vi không xác định nếu ngăn xếp trống. Sau đó, bạn phải cung cấp hàm is_empty() có thể được gọi để kiểm tra ngăn xếp.

phương pháp khác là sử dụng C++, mà không có ngoại lệ :-)

+0

một cách hữu ích hơn cho trường hợp cụ thể này, C++ có một chồng ngay tại đó trong thư viện :-) –

+0

Nhưng C++ std :: chồng pop chức năng không thực sự làm những gì OP muốn. –

+1

Đúng, và hiểu lý do tại sao sẽ thêm vào giáo dục C++ của OP, đó là mục đích chính của câu hỏi :-) Dù sao, hãy gói một cuộc gọi vào mỗi 'top()' và 'pop()', trả về một bản sao, cho kết quả cuối cùng giống như lấy những gì OP có và áp dụng những gì bạn nói về việc cần một hàm 'empty()'. IYSWIM. –

1

bạn có thể trả về một con trỏ đến kép:

  • phi NULL -> hợp lệ
  • NULL -> không hợp lệ
+0

downvotes mà không có ý kiến ​​là vô nghĩa, xin vui lòng giải thích downvotes của bạn – dfa

+3

Tôi không phải là downvote, nhưng tôi tự hỏi, nơi lưu trữ sao lưu cho con trỏ đến từ. Nếu phần tử xuất hiện của nó, thì người gọi phải dereference nó trước khi một giá trị mới có thể được đẩy. Nếu nó là một 'static double' riêng biệt, thì người gọi phải dereference trước khi cuộc gọi tiếp theo xuất hiện. Cả hai nguyên nhân gây ra rất nhiều rắc rối cho mã gọi. – RBerteig

+0

Tôi đã không downvote hoặc nhưng tôi đã có những mối quan tâm tương tự. Tôi nghĩ rằng đó là một cách tiếp cận hiệu quả nhưng bạn cần phải thay đổi cách hoạt động của chức năng và lưu trữ dữ liệu để thực hiện nó. – Jon

7

Bạn có một vài lựa chọn:

1) Magic giá trị lỗi. Không phải lúc nào cũng đủ tốt, vì lý do bạn mô tả. Tôi đoán trong lý thuyết cho trường hợp này bạn có thể trả lại một NaN, nhưng tôi không khuyên bạn nên nó.

2) Xác định rằng nó không hợp lệ để bật khi ngăn xếp trống. Sau đó, mã của bạn hoặc chỉ giả định nó không trống (và đi không xác định nếu nó là), hoặc khẳng định.

3) Thay đổi chữ ký của các chức năng để bạn có thể cho biết thành công hay thất bại.

int pop(double *dptr) 
{ 
    if(sp > 0) { 
      *dptr = val[--sp]; 
      return 0; 
    } else { 
      return 1; 
    } 
} 

Document nó như là "Nếu thành công, trả về 0 và viết các giá trị cho các vị trí được trỏ đến bởi dptr On thất bại, trả về một giá trị khác không. "

Tùy chọn, bạn có thể sử dụng giá trị trả lại hoặc errno để cho biết lý do thất bại, mặc dù ví dụ cụ thể này chỉ có một lý do.

4) Chuyển đối tượng "ngoại lệ" vào mọi hàm bằng con trỏ và viết giá trị cho nó khi không thành công. Người gọi sau đó kiểm tra hoặc không theo cách họ sử dụng giá trị trả lại. Điều này rất giống với việc sử dụng "errno", nhưng không có giá trị toàn bộ chuỗi.

5) Như những người khác đã nói, hãy triển khai ngoại lệ với setjmp/longjmp. Nó có thể thực hiện được, nhưng yêu cầu hoặc truyền một tham số phụ ở khắp mọi nơi (đích của longjmp để thực hiện trên thất bại), hoặc ẩn nó trong globals. Nó cũng làm cho tài nguyên kiểu C điển hình xử lý một cơn ác mộng, bởi vì bạn không thể gọi bất cứ điều gì có thể nhảy ra khỏi mức stack của bạn nếu bạn đang nắm giữ một nguồn tài nguyên mà bạn chịu trách nhiệm giải phóng.

+0

+1: Đối với điểm # 3. –

10

Bạn có thể xây dựng hệ thống ngoại lệ trên đầu trang của longjmp/setjmp: Exceptions in C with Longjmp and Setjmp. Nó thực sự hoạt động khá tốt và bài viết cũng là một bài đọc hay. Sau đây là cách mã của bạn có thể trông giống như nếu bạn sử dụng các hệ thống ngoại lệ từ các bài viết liên quan:

TRY { 
    ... 
    THROW(MY_EXCEPTION); 
    /* Unreachable */ 
    } CATCH(MY_EXCEPTION) { 
    ... 
    } CATCH(OTHER_EXCEPTION) { 
    ... 
    } FINALLY { 
    ... 
    } 

Thật ngạc nhiên là những gì bạn có thể làm với một macro ít, phải không? Thật ngạc nhiên khi thấy khó khăn như thế nào để tìm hiểu xem cái quái gì đang diễn ra nếu bạn chưa biết những gì macro làm.

longjmp/setjmp là di động: C89, C99 và POSIX.1-2001 chỉ định setjmp().

Lưu ý, tuy nhiên, ngoại lệ được triển khai theo cách này sẽ vẫn có một số hạn chế so với ngoại lệ "thực" trong C# hoặc C++. Một vấn đề lớn là chỉ có mã của bạn sẽ tương thích với hệ thống ngoại lệ này. Vì không có tiêu chuẩn được thiết lập cho các trường hợp ngoại lệ trong C, hệ thống và thư viện của bên thứ ba sẽ không tương thích tối ưu với hệ thống ngoại lệ trong nhà của bạn. Tuy nhiên, điều này đôi khi có thể trở thành một hack hữu ích.

Tôi không khuyên bạn nên sử dụng mã này ở mã số nghiêm trọng mà các lập trình viên khác ngoài chính bạn có nghĩa vụ phải làm việc cùng. Nó chỉ là quá dễ dàng để bắn mình vào chân với điều này nếu bạn không biết chính xác những gì đang xảy ra. Threading, quản lý tài nguyên và xử lý tín hiệu là các vấn đề mà các chương trình không phải đồ chơi sẽ gặp phải nếu bạn cố gắng sử dụng "ngoại lệ" longjmp.

+4

Tôi thực sự xây dựng một cái gì đó như thế này trong C + + trước khi ngoại lệ trở nên phổ biến rộng rãi. Tôi thậm chí còn thực hiện bằng hình thức riêng của chồng thư giãn. May mắn thay, tôi đã đi đến giác quan của tôi trước khi chúng tôi sử dụng nó trong mã sản xuất. –

+0

@Neil: Tôi thích phần thứ hai của câu đó :-) – jeroenh

+1

"May mắn thay, tôi đã đến với giác quan của tôi". Xin chúc mừng. Symbian đã làm điều tương tự như bạn đã làm, ngay đến điểm mà bạn đến với các giác quan của bạn, và chúng được vận chuyển. Hơn 10 năm sau, họ vẫn có NewLC ở mọi nơi ... –

2

Không có ngoại lệ tương đương với ngoại lệ C. Bạn phải thiết kế chữ ký chức năng của mình để trả về thông tin lỗi, nếu đó là những gì bạn muốn.

Các cơ chế có sẵn trong C là:

gotos
  • không thuộc địa phương với setjmp/longjmp
  • Tín hiệu

Tuy nhiên, không ai trong số những có ngữ nghĩa từ xa giống như C# (hoặc C++) trường hợp ngoại lệ .

3

Trong những trường hợp như thế này, bạn thường làm một trong

  • Để lại nó cho người gọi. ví dụ. tùy thuộc vào người gọi để biết nếu nó an toàn để bật() (ví dụ: gọi hàm stack-> is_empty() trước khi bật ngăn xếp), và nếu người gọi messes lên, đó là lỗi của mình và may mắn.
  • Báo hiệu lỗi thông qua tham số ngoài hoặc giá trị trả lại.

ví dụ:bạn hoặc là

double pop(int *error) 
{ 
    if(sp > 0) { 
     return val[--sp]; 
     *error = 0; 
    } else { 
    *error = 1; 
     printf("error: stack empty\n"); 
     return 0.0; 
    } 

}

hoặc

int pop(double *d) 
{ 
    if(sp > 0) { 
     *d = val[--sp]; 
     return 0; 
    } else { 
    return 1; 

    } 
} 
0

Hiện đã có một số câu trả lời tốt ở đây, chỉ muốn đề cập đến một cái gì đó gần với "ngoại lệ", có thể được thực hiện với việc sử dụng các một macro, như đã được thực hiện trong tuyệt vời MinUnit (điều này chỉ trả về "ngoại lệ" cho chức năng người gọi).

1

1) Bạn trả lại giá trị cờ để hiển thị không thành công hoặc bạn sử dụng cú pháp TryGet trong đó trả về là giá trị thành công trong khi giá trị được chuyển qua thông số đầu ra.

2) Nếu điều này nằm trong Windows, có một dạng ngoại lệ C thuần túy ở mức OS, được gọi là Xử lý ngoại lệ có cấu trúc, sử dụng cú pháp như "_try". Tôi đề cập đến nó, nhưng tôi không đề nghị nó cho trường hợp này.

4

Đây thực sự là một ví dụ hoàn hảo về các tệ nạn của việc cố gắng quá tải kiểu trả về với các giá trị ma thuật và thiết kế giao diện đơn giản.

Một giải pháp tôi có thể sử dụng để loại bỏ sự mơ hồ (và do đó nhu cầu "ngoại lệ như hành vi") trong ví dụ này là để xác định một kiểu trả về đúng:

struct stack{ 
    double* pData; 
    uint32 size; 
}; 

struct popRC{ 
    double value; 
    uint32 size_before_pop; 
}; 

popRC pop(struct stack* pS){ 
    popRC rc; 
    rc.size=pS->size; 
    if(rc.size){ 
     --pS->size; 
     rc.value=pS->pData[pS->size]; 
    } 
    return rc; 
} 

Cách sử dụng tất nhiên là:

popRC rc = pop(&stack); 
if(rc.size_before_pop!=0){ 
    ....use rc.value 

Điều này xảy ra mọi thời đại, nhưng trong C++ để tránh sự mơ hồ như người ta thường chỉ trả về một

std::pair<something,bool> 
.210

nơi bool là một chỉ số thành công - nhìn vào một số:

std::set<...>::insert 
std::map<...>::insert 

Ngoài ra thêm một double* để giao diện và trả về một mã trở lại (! N UNOVERLOADED), nói một enum cho thấy thành công.

Tất nhiên, người dùng không phải trả lại kích thước trong cấu trúc popRC. Có thể là

enum{FAIL,SUCCESS}; 

Nhưng vì kích thước có thể là gợi ý hữu ích cho cửa sổ bật lên, bạn cũng có thể sử dụng nó.

BTW, tôi chân thành đồng ý rằng ngăn xếp giao diện struct nên có

int empty(struct stack* pS){ 
    return (pS->size == 0) ? 1 : 0; 
} 
0

setjmp, longjmp, và macro. Nó đã được thực hiện bất kỳ số lần — thực hiện lâu đời nhất mà tôi biết là bởi Eric Roberts và Mark vanderVoorde — nhưng cái tôi sử dụng hiện nay là một phần của Dave Hanson's C Interfaces and Implementations và miễn phí từ Princeton.

1

Cái gì mà chưa ai đề cập nào, nó khá xấu xí mặc dù:

int ok=0; 

do 
{ 
    /* Do stuff here */ 

    /* If there is an error */ 
    break; 

    /* If we got to the end without an error */ 
    ok=1; 

} while(0); 

if (ok == 0) 
{ 
    printf("Fail.\n"); 
} 
else 
{ 
    printf("Ok.\n"); 
}