Chúng ta có nên sử dụng exit () trong C không?


79

Có một câu hỏi về việc sử dụng exittrong C ++. Câu trả lời thảo luận rằng đó không phải là ý tưởng tốt chủ yếu là do RAII, ví dụ: nếu exitđược gọi ở đâu đó trong mã, các trình hủy của các đối tượng sẽ không được gọi, do đó, nếu ví dụ, nếu trình hủy được dùng để ghi dữ liệu vào tệp, điều này sẽ không xảy ra , vì trình hủy không được gọi.

Tôi đã quan tâm đến tình huống này ở C. Các vấn đề tương tự có áp dụng được ở C không? Tôi nghĩ vì trong C chúng ta không sử dụng hàm tạo / hủy, tình hình có thể khác trong C. Vì vậy, liệu sử dụng exittrong C có được không?

Tôi đã thấy các hàm như bên dưới, mà tôi thấy tốt khi sử dụng trong một số trường hợp, nhưng liệu chúng ta có quan tâm đến vấn đề tương tự trong C khi sử dụng exit, như được mô tả ở trên với C ++ không? (điều này sẽ làm cho việc sử dụng các hàm như bên dưới không phải là một ý kiến ​​hay.).

void die(const char *message)
{
    if(errno) {
        perror(message);
    } else {
        printf("ERROR: %s\n", message);
    }

    exit(1);
}

2
"hủy đối tượng sẽ không được gọi" - Điều đó không hoàn toàn chính xác (xem: cplusplus.com/reference/cstdlib/exit ). Bạn đang nghĩ đến việc quick_exit (xem: cplusplus.com/reference/cstdlib/quick_exit/?kw=quick_exit ).
Coder

Bạn cũng có thể gặp một số vấn đề cụ thể của hệ điều hành, ví dụ như vai trò truyền thống của SIGTERMtín hiệu trên POSIX & Linux .... Các máy chủ hoạt động tốt được mong đợi sẽ xử lý tốt. Và bạn nên tránh sử dụng SIGKILL(ví dụ: thử kill -TERMsau đó kill -QUITvà chỉ sau đó kill -KILLnhư là một sysadmin)
Basile Starynkevitch

20
Làm thế nào đây không phải là bản sao gần 7 năm sau khi Stack Overflow ra mắt?
Peter Mortensen,

7
@PeterMortensen: nó ngạc nhiên, nhưng tôi không nhận thức được một lựa chọn tốt mà điều này bản sao. Việc sử dụng exit()chức năng được bình chọn cao không phải là Đức (và số phiếu bầu cao thật đáng ngạc nhiên - đó không phải là một câu hỏi hay). Tại sao tôi không nên sử dụng hàm exit () trong C? sẽ là một ứng cử viên sáng giá nếu nó có câu trả lời tầm cỡ cho câu hỏi này. Nó không; đóng ngược lại như một bản sao của điều này là phù hợp - và tôi đã làm được.
Jonathan Leffler,

Tôi không chắc bạn muốn sử dụng 'printf' trong trường hợp lỗi. Hàm sử dụng bộ đệm, nhưng nếu bạn bị hỏng bộ nhớ, những bộ đệm đó có thể không tốt. Bạn có thể chỉ muốn sử dụng 'write (2, "ERROR:", 7); write (2, message, strlen (message));
Alex,

Câu trả lời:


82

Thay vào đó abort(), exit()hàm trong C được coi là một lối ra "duyên dáng".

Từ C11 (N1570) 7.22.4.4/p2 Chức năng thoát (mỏ nhấn mạnh):

Các exitchức năng gây ra bình thường chấm dứt chương trình để xảy ra.

Tiêu chuẩn cũng nói trong 7.22.4.4/p4 rằng:

Tiếp theo, tất cả các luồng mở có dữ liệu đệm không được viết sẵn sẽ bị xóa , tất cả các luồng mở bị đóng và tất cả các tệp được tạo bởi tmpfilehàm sẽ bị xóa.

Nó cũng được giá trị xem xét 7.21.3 / p5 tập tin :

Nếu mainhàm quay trở lại trình gọi ban đầu của nó hoặc nếu exit hàm được gọi, tất cả các tệp đang mở sẽ bị đóng (do đó tất cả các luồng đầu ra được xóa) trước khi kết thúc chương trình. Các đường dẫn khác để kết thúc chương trình, chẳng hạn như gọi aborthàm, không cần phải đóng tất cả các tệp đúng cách.

Tuy nhiên, như đã đề cập trong các nhận xét bên dưới, bạn không thể cho rằng nó sẽ bao gồm mọi tài nguyên khác , vì vậy bạn có thể cần sử dụng atexit()và xác định các lệnh gọi lại cho bản phát hành của chúng riêng lẻ. Trên thực tế, nó chính xác là những gì atexit()dự định làm, như nó đã nói trong 7.22.4.2/p2 Hàm atexit :

Các atexitthanh ghi chức năng chức năng được trỏ đến bởi func, gọi mà không cần tranh luận tại chấm dứt chương trình bình thường.

Đáng chú ý, tiêu chuẩn C không cho biết chính xác điều gì sẽ xảy ra với các đối tượng có thời lượng lưu trữ được phân bổ (ví dụ malloc()), do đó, yêu cầu bạn phải biết cách thực hiện đối với việc triển khai cụ thể. Đối với hệ điều hành hướng máy chủ hiện đại, có khả năng hệ thống sẽ xử lý nó, nhưng bạn vẫn có thể muốn tự xử lý việc này để tắt các trình gỡ lỗi bộ nhớ như Valgrind .


3
Đây dường như là câu trả lời chính xác đối với tôi. Một kết nối socket bị đóng khi thoát bởi vì quá trình kết thúc đóng tất cả các bộ mô tả tệp cho dù được mở bằng hay không stdio, tức là tương đương với a close()trên FD. Tôi không biết @BlueMoon nghĩ điều đó không chính xác theo nghĩa nào, nhưng nó không chính xác hơn một cuộc gọi đến close(), và nếu cần phải làm rõ thêm, đó chính xác là những gì atexit()dành cho.
abligh

1
@abligh Hệ điều hành hiện đại sẽ đóng tất cả các fds liên quan đến quá trình đó; nhưng đó là một cái gì đó tàn bạo và không chuẩn (những gì Blue Moon đang nói).
edmz

3
@black nếu cần xóa thêm, atexit()có thể được sử dụng. Sử dụng exit()chính nó không tàn bạo hơn làm returntừ main().
abligh

5
Viết phần mềm 'thích hợp' bằng cách sử dụng 'các phương pháp hay' là lý do tại sao tôi có rất nhiều ứng dụng không tắt ngay lập tức khi được yêu cầu. :( Nếu hệ điều hành có thể làm điều gì đó, bạn nên để nó làm điều đó thay vì loay hoay với mã người dùng đang thử làm một thứ gì đó đã tồn tại, đã được kiểm tra và đã được gỡ lỗi. Nếu hệ thống phần mềm của bạn không thể chịu được việc tắt đột ngột, bất ngờ, (ví dụ: từ một chuỗi nào đó yêu cầu chấm dứt ngay trong quá trình, Task Manager 'End process', 'kill -9' hoặc mất điện), nó là chất lượng kém anyway.
Martin James

sẽ rất tuyệt nếu bạn có thể thêm -nếu có thể- vào câu trả lời của mình loại tài nguyên nào có thể cần để phát hành atexit, nếu không thì cũng được. @BlueMoon: Tôi không nghĩ rằng việc sử dụng chức năng như vậy diecó nghĩa là người ta không thể lập trình, trong một số trường hợp, người ta có thể muốn sử dụng nó. Nếu không thực sự, bạn cũng có thể xử lý này sử dụng chỉ trả về giá trị, vv
giorgim

23

Có, nó là ok để sử dụng exittrong C.

Để đảm bảo tất cả các bộ đệm và tắt một cách có trật tự, bạn nên sử dụng chức năng này atexit, thêm thông tin về điều này tại đây

Một mã ví dụ sẽ như thế này:

void cleanup(void){
   /* example of closing file pointer and free up memory */
   if (fp) fclose(fp);
   if (ptr) free(ptr);
}

int main(int argc, char **argv){
   /* ... */
   atexit(cleanup);
   /* ... */
   return 0;
}

Bây giờ, bất cứ khi nào exitđược gọi, hàm cleanupsẽ được thực thi, có thể giúp bạn tắt máy, dọn sạch bộ đệm, bộ nhớ, v.v.


@IlmariKaronen Bộ nhớ được phân bổ không được xử lý tự động theo bất kỳ cách nào. Đó là hệ điều hành có thể đảm bảo bộ nhớ được khôi phục trở lại hệ điều hành. Lập luận của Grzegorz Szpetkowski phản đối tuyên bố của bạn: Đáng chú ý, tiêu chuẩn C không cho biết chính xác điều gì sẽ xảy ra với các đối tượng có thời lượng lưu trữ được phân bổ (iemalloc ()), do đó đòi hỏi bạn phải biết cách thực hiện đối với việc triển khai cụ thể. Đối với hệ điều hành hướng máy chủ hiện đại, có khả năng hệ thống sẽ xử lý nó, nhưng bạn vẫn có thể muốn tự xử lý việc này để tắt các trình gỡ lỗi bộ nhớ như Valgrind.
này

15

Bạn không có hàm tạo và hàm hủy nhưng bạn có thể có tài nguyên (ví dụ: tệp, luồng, ổ cắm) và điều quan trọng là phải đóng chúng một cách chính xác. Một bộ đệm không thể được ghi đồng bộ, vì vậy việc thoát khỏi chương trình mà không đóng chính xác tài nguyên trước, có thể dẫn đến hỏng.


8
Câu trả lời này là một ví dụ tuyệt vời về việc "đừng học một ngôn ngữ lập trình; hãy học cách lập trình". Nhìn chung, bạn không thể tránh hiểu vấn đề bằng cách chọn một ngôn ngữ khác.
Kerrek SB,

10
Tôi tin rằng điều này là không chính xác. Nếu bạn gọi exit()(chứ không phải _exit()), các atexitquy trình sẽ được gọi và stdiobộ đệm được chuyển vào đĩa. exit()chính xác ở đó để cho phép thoát khỏi chương trình một cách có trật tự.
abligh

2
@abligh Hệ thống thời gian thực không giải phóng bộ nhớ bị lỗi khi thoát (), dẫn đến rò rỉ. Bộ nhớ chia sẻ POSIX là một ví dụ khác. Vì vậy, nó phụ thuộc vào môi trường hơn là tuân thủ nghiêm ngặt tiêu chuẩn C.
PP

2
@abligh: Không phải tất cả tài nguyên đều được cung cấp thư viện tiêu chuẩn. Mặc dù bạn chắc chắn đúng rằng thư viện tiêu chuẩn đưa ra những đảm bảo nhất định, bạn vẫn cần suy nghĩ về quy trình kiểm soát toàn cầu của chương trình của mình và trách nhiệm của từng bộ phận trong đó và đảm bảo rằng từng trách nhiệm đang chờ xử lý được xử lý một cách thích hợp. Thuật ngữ "tài nguyên" là một thuật ngữ ngắn gọn cho khái niệm chung này và vượt qua bất kỳ thư viện cụ thể nào, và việc xử lý tài nguyên cuối cùng là trách nhiệm của người lập trình. Các tiện nghi như atexitcó thể giúp ích, mặc dù chúng không phải lúc nào cũng thích hợp.
Kerrek SB,

2
@abligh: Tất nhiên bạn có thể sử dụng exitnếu bạn muốn, nhưng nó là một bước nhảy toàn cầu về quyền kiểm soát đi kèm với tất cả những mặt trái của kiểm soát không có cấu trúc mà chúng ta đã thảo luận. Như một cách để kết thúc khỏi điều kiện lỗi, nó có thể phù hợp (và nó có vẻ là những gì OP muốn), mặc dù đối với luồng điều khiển bình thường, tôi có thể thích thiết kế vòng lặp chính với điều kiện thoát thích hợp để bạn thực sự kết thúc trở về từ chính.
Kerrek SB

10

Sử dụng exit()là OK

Hai khía cạnh chính của thiết kế mã vẫn chưa được đề cập là 'phân luồng' và 'thư viện'.

Trong một chương trình đơn luồng, trong mã bạn đang viết để triển khai chương trình đó, việc sử dụng exit()là tốt. Các chương trình của tôi sử dụng nó thường xuyên khi có sự cố và mã sẽ không khôi phục.

Nhưng…

Tuy nhiên, kêu gọi exit()là một hành động đơn phương không thể hoàn tác. Đó là lý do tại sao cả 'luồng' và 'thư viện' đều cần phải suy nghĩ cẩn thận.

Chương trình phân luồng

Nếu một chương trình là đa luồng, thì việc sử dụng exit()là một hành động kịch tính sẽ kết thúc tất cả các luồng. Nó có thể sẽ không thích hợp để thoát toàn bộ chương trình. Có thể thích hợp để thoát khỏi chuỗi, báo cáo lỗi. Nếu bạn nhận thức được thiết kế của chương trình, thì có thể cho phép thoát đơn phương đó, nhưng nói chung, nó sẽ không được chấp nhận.

Mã thư viện

Và mệnh đề 'nhận thức về thiết kế của chương trình' cũng áp dụng cho mã trong thư viện. Việc gọi một hàm thư viện mục đích chung là rất hiếm khi đúng exit(). Bạn sẽ rất khó chịu nếu một trong các hàm thư viện C chuẩn không trả lại được chỉ vì lỗi. (Rõ ràng, các chức năng như exit(), _Exit(), quick_exit(), abort()nhằm mục đích không để trở lại, đó là khác nhau.) Các chức năng trong thư viện C do đó một trong hai "không thể thất bại" hoặc trả lại một dấu hiệu lỗi bằng cách nào đó. Nếu bạn đang viết mã để đi vào thư viện mục đích chung, bạn cần xem xét chiến lược xử lý lỗi cho mã của mình một cách cẩn thận. Nó phải phù hợp với các chiến lược xử lý lỗi của chương trình mà nó được dự định sử dụng, hoặc việc xử lý lỗi có thể được cấu hình.

Tôi có một loạt các hàm thư viện (trong một gói có tiêu đề "stderr.h", một tên nằm trên lớp băng mỏng) nhằm thoát ra khi chúng được sử dụng để báo lỗi. Các chức năng đó thoát ra theo thiết kế. Có một loạt hàm liên quan trong cùng một gói báo lỗi và không thoát. Tất nhiên, các chức năng thoát được thực hiện theo các chức năng không thoát, nhưng đó là chi tiết triển khai nội bộ.

Tôi có nhiều chức năng thư viện khác và rất nhiều chức năng trong số họ dựa vào "stderr.h"mã để báo lỗi. Đó là một quyết định thiết kế mà tôi đã đưa ra và là một quyết định mà tôi đồng ý. Nhưng khi các lỗi được báo cáo với các chức năng thoát ra, nó hạn chế tính hữu ích chung của mã thư viện. Nếu mã gọi các hàm báo lỗi không thoát, thì các đường dẫn mã chính trong hàm phải xử lý lỗi trả về một cách lành mạnh - phát hiện chúng và chuyển tiếp chỉ báo lỗi đến mã gọi.


Mã cho gói báo cáo lỗi của tôi có sẵn trong kho lưu trữ SOQ (Câu hỏi dồn về ngăn xếp) trên GitHub dưới dạng tệp stderr.cstderr.htrong thư mục con src / libsoq .


Nó cũng nhọn. Bạn có thể thấy ở đây một ví dụ về thư viện gọi abort()khi phân bổ bộ nhớ bằng một trong hai malloc()hoặc realloc(), Hãy tưởng tượng, bạn có một ứng dụng, liên kết với 100 thư viện và bạn tự hỏi cái nào và ứng dụng của bạn bị lỗi như thế nào. Hơn nữa, tôi không tìm thấy bất kỳ đề cập nào abort()trong tài liệu của họ (nhưng đừng hiểu lầm. Đó là thư viện tuyệt vời cho mục đích của nó).
Grzegorz Szpetkowski,

@GrzegorzSzpetkowski Và nó thậm chí không gọi flush theo cách thủ công sau khi fprinting sang stderr. Ôi!
này

1
@this: Không cần thiết phải làm như vậy. Đầu ra từ stderrthường được đệm dòng. Nếu đầu ra kết thúc bằng một dòng mới, nó vẫn sẽ được hệ thống xóa.
Jonathan Leffler,

@JonathanLeffler Dựa vào người dùng, không phải là một bước đi thông minh.
này

@this: Không, dựa vào việc triển khai tuân thủ các yêu cầu của tiêu chuẩn C: §7.21.3 ¶7 Khi khởi động chương trình, ba luồng văn bản được xác định trước và không cần phải mở rõ ràng - đầu vào tiêu chuẩn (để đọc đầu vào thông thường), tiêu chuẩn đầu ra (để ghi đầu ra thông thường) và lỗi tiêu chuẩn (để ghi đầu ra chẩn đoán). Khi được mở ban đầu, luồng lỗi tiêu chuẩn không được lưu vào bộ đệm đầy đủ; … Nó yêu cầu lập trình viên phải mã hóa tích cực để làm cho lỗi chuẩn được lưu vào bộ đệm đầy đủ (và không có ai giúp đỡ nếu lập trình viên mắc lỗi đó - ngoại trừ "không sử dụng mã").
Jonathan Leffler,

6

Một lý do cần tránh exittrong các chức năng khác main()là khả năng mã của bạn có thể được đưa ra khỏi ngữ cảnh. Hãy nhớ rằng, lối ra là một loại luồng không kiểm soát cục bộ . Giống như các trường hợp ngoại lệ không thể điều chỉnh được.

Ví dụ: bạn có thể viết một số chức năng quản lý lưu trữ thoát ra trên một lỗi đĩa nghiêm trọng. Sau đó, ai đó quyết định chuyển chúng vào một thư viện. Thoát khỏi thư viện là một cái gì đó sẽ khiến chương trình gọi thoát ra ở trạng thái không phù hợp mà nó có thể không được chuẩn bị cho.

Hoặc bạn có thể chạy nó trên một hệ thống nhúng. Đâu có để thoát đến , điều chạy toàn bộ trong một while(1)vòng lặp trong main(). Nó thậm chí có thể không được xác định trong thư viện chuẩn.


2
Và mọi người không được phép sở hữu dao làm bếp, bởi vì ai đó có thể lấy một vài con dao và cố gắng tung chúng hoặc chơi trò "bắt" với con của anh ta. Rõ ràng là tôi không đồng ý. Nếu ai đó quyết định sao chép mã của tôi vào chương trình của anh ta, và mã của tôi hóa ra không phù hợp với mục đích của anh ta, đó là vấn đề của anh ta, vì không đọc được mã mà anh ta đang đồng hóa.
G-Man nói 'Phục hồi Monica' vào

3
Một sự tương tự khá phức tạp. C có đầy đủ những điều thể làm nhưng có lẽ nên tránh. Do đó sự tồn tại của IOCCC. Lưu ý rằng bài đăng của tôi không nói "không nên".
pjc50

2

Tùy thuộc vào những gì bạn đang làm, thoát có thể là cách hợp lý nhất để thoát khỏi một chương trình trong C. Tôi biết nó rất hữu ích để kiểm tra để đảm bảo chuỗi lệnh gọi lại hoạt động chính xác. Lấy ví dụ về cuộc gọi lại mà tôi đã sử dụng gần đây:

unsigned char cbShowDataThenExit( unsigned char *data, unsigned short dataSz,unsigned char status)
{

    printf("cbShowDataThenExit with status %X (dataSz %d)\n", status, dataSz);
    printf("status:%d\n",status);
    printArray(data,dataSz);
    cleanUp();
    exit(0);
}

Trong vòng lặp chính, tôi thiết lập mọi thứ cho hệ thống này và sau đó đợi trong vòng lặp (1) một lúc. Có thể tạo cờ toàn cục để thoát khỏi vòng lặp while, nhưng điều này rất đơn giản và thực hiện những gì cần làm. Nếu bạn đang xử lý bất kỳ bộ đệm mở nào như tệp và thiết bị, bạn nên dọn dẹp chúng trước khi đóng để có tính nhất quán.


-1

Thật là khủng khiếp trong một dự án lớn khi bất kỳ mã nào có thể thoát ra ngoại trừ coredump. Trace là rất quan trọng để duy trì một máy chủ trực tuyến.

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.