“exit”与“return”的区别是什么?

120

在C程序中从任何地方调用时,return语句和exit语句有什么区别?


1
我移除了“标记为重复”的选项,因为所选的重复问题同时打上了C和C++标签,没有必要用C++问题(虽然与C问题有些相似但是不同)来困扰人们。这个重复问题是关于 return statement vs exit() in main()? 的。 - Jonathan Leffler
5个回答

172
  • return 从当前函数返回;它是像forbreak这样的语言关键字。
  • exit() 终止整个程序,无论你从哪里调用它。(在刷新stdio缓冲区等之后)。

唯一的情况是在main()函数中,当从主函数返回时执行exit()(几乎)相同的操作。

在大多数C实现中,main是由某些启动代码调用的真正的函数,该代码执行类似于int ret = main(argc, argv); exit(ret);的操作。 C标准保证如果main返回,则发生与此等效的操作,但是实现方式不同。

使用return的示例:

#include <stdio.h>

void f(){
    printf("Executing f\n");
    return;
}

int main(){
    f();
    printf("Back from f\n");
}

如果你运行这个程序,它会打印出:

Executing f
Back from f

exit() 的另一个例子:

#include <stdio.h>
#include <stdlib.h>

void f(){
    printf("Executing f\n");
    exit(0);
}

int main(){
    f();
    printf("Back from f\n");
}

如果您执行此程序,它会打印出:
Executing f

你永远不会从"f"返回。另请注意,调用库函数exit()需要包含#include <stdlib.h>

还要注意exit()的参数是整数(它是启动器进程可以获取的进程返回状态;惯用法是成功为0或任何其他值为错误)。

返回语句的参数是函数返回类型。如果函数返回void,则可以在函数末尾省略return。

最后一个要点,exit()有两种形式_exit()exit()。两种形式之间的区别在于exit()(和从主函数返回)在真正终止进程之前调用使用atexit()on_exit()注册的函数,而_exit()(来自#include <unistd.h>或其同义词_Exit来自#include <stdlib.h>)立即终止进程。

现在还有一些特定于C ++的问题。

当 C++ 退出函数(通过 return)时,它比 C 做的工作要多得多。具体而言,它会调用超出范围的局部对象的析构函数。在大多数情况下,程序员不太关心进程停止后程序的状态,因此这并不会有太大的区别:分配的内存将被释放,文件资源将关闭等等。但是如果您的析构函数执行输入/输出操作,则可能会很重要。例如,自动创建的 C++ OStream 在调用 exit 时不会被刷新,您可能会丢失一些未刷新的数据(另一方面,静态的 OStream 将被刷新)。

如果您使用老旧的 C FILE* 流,则不会发生这种情况。这些将在 exit() 上刷新。实际上,规则对于注册的退出函数也是相同的,在所有正常终止中 FILE* 都会被刷新,其中包括 exit(),但不包括对 _exit() 或 abort() 的调用。

您还应该记住,C++ 提供了第三种退出函数的方式:抛出异常。这种退出函数的方式将会调用析构函数。如果它在调用者链中没有任何地方被捕获,异常可以上升到 main() 函数并终止进程。

如果您从程序中的任何位置调用exit()或从main()中使用return,则将调用全局(静态)C++对象的析构函数。如果使用_exit()abort()终止程序,则不会调用它们。 abort()通常在调试模式下非常有用,目的是立即停止程序并获取堆栈跟踪(进行事后分析)。它通常隐藏在assert()宏后面,仅在调试模式下活动。

exit()何时有用?

exit()意味着您想立即停止当前进程。当我们遇到某种无法恢复的问题时,它可以用于错误管理,这将不再允许您的代码执行任何有用的操作。当控制流复杂且必须向上传播错误代码时,它通常很方便。但请注意,这是糟糕的编码实践。在大多数情况下,默默地结束进程是最糟糕的行为,应该优先考虑实际的错误管理(或在C ++中使用异常)。

如果在库中直接调用exit(),特别是不好的,因为它会使库用户无法使用,并且库用户应该选择是否实现某种错误恢复。如果您想要一个调用exit()的库的示例是不好的,它会导致人们问this question

对于支持fork()的操作系统来说,使用exit()是结束由它启动的子进程的不容置疑的合法方法。回到fork()之前的代码通常不是一个好主意。这就是解释为什么exec()系列函数永远不会返回给调用者的原因。


9
exit() 不是一个系统调用。 - anon
5
我通常在main()函数中使用return语句。当然,在main()函数的末尾我会使用return 0;语句——有时我也会在函数体内使用exit();语句。我不喜欢C99规则关于从main()函数结尾掉出被视为等同于return 0;的特殊情况;这是一个很荒谬的规定(虽然C++更早地引领了这种做法)。 - Jonathan Leffler
1
注意:C11标准具有其他功能at_quick_exit()_Exit()(也在C99中),以及quick_exit()_exit()函数来自POSIX,但本质上与_Exit()相同。 - Jonathan Leffler
3
如果你想要一个例子来说明为什么图书馆中的退出功能是不好的:它会让人们提出这个问题https://dev59.com/hZHea4cB1Zd3GeqPsKcB。然后可能要使用hack或者无端地使用IPC。 - Flexo
2
@Milan:一些IO是有缓冲区的,这也适用于OStream。在这种情况下,flushed表示数据实际上已经发送到控制台/写入磁盘,而不仅仅是保留在缓冲区中。未刷新表示数据仍然存储在应用程序的某些内存缓冲区中,但尚未发送到系统。在C++文档中,他们将数据发送到的底层系统对象称为“受控序列”。OStream上有一个显式的flush方法来执行此操作。 - kriss
显示剩余27条评论

26

我写了两个程序:

int main(){return 0;}

#include <stdlib.h>
int main(){exit(0)}
执行gcc -S -O1后,观察汇编代码(只列出重要部分)如下:
main:
    movl    $0, %eax    /* setting return value */
    ret                 /* return from main */

main:
    subq    $8, %rsp    /* reserving some space */
    movl    $0, %edi    /* setting return value */
    call    exit        /* calling exit function */
                        /* magic and machine specific wizardry after this call */
因此,我的结论是:尽可能使用return,需要时使用exit()

这是一个非常好的答案,除了结论之外;在我看来,它激发了相反的观点:我们可以假设ret指令返回到可能有一些额外工作的地方,但最终还是会调用exit()函数 - 否则如何避免执行exit()的操作呢?这意味着前者只是一些额外的“重复”工作,没有任何优势比第二种解决方案更好,因为无论如何,exit()最终肯定会被调用。 - Max
2
@Max:在你自己的程序中递归调用main函数并不是被禁止的。因此,你不应该假设从main函数返回将立即退出,否则会破坏代码语义。在某些(罕见)情况下,这甚至是有用的。例如,在将代码入口点更改为与main不同的内容之后,在调用main函数之前帮助准备/清除一些上下文。这甚至可以通过某些运行时代码注入方法来完成。当然,如果是你自己的程序,可以按照自己的喜好或认为更易于阅读的方式进行操作。 - kriss
@kriss:这是一个非常好的观点,之前没有提到过。虽然我认为main()被递归调用的情况极为罕见,但这种可能性比其他任何事情都更清楚地阐明了return和exit(0)之间的区别,我的看法。 - Max

11
在 C 语言中,在程序启动函数(可以是 main()、wmain()、_tmain() 或编译器使用的默认名称)中使用 return 没有太大差异。
如果在 main() 中使用 return,控制权将返回到 C 库中最初启动程序的 _start() 函数,然后该函数会调用 exit()。因此,使用哪个函数其实并不重要。

3
很重要。exit()函数立即终止程序,无论在何处调用都会如此。return语句只会退出当前的函数。它们唯一相同的地方是在main()函数中。 - user47589
谢谢,我已经修正了措辞。它并非必须只在 main() 函数中,因为并非所有编译器都使用相同的启动函数名称。 - Dumb Guy
8
我猜你所有的程序都是写在一个大的主函数里面吗?;-) - C.J.
1
答案不完整,但仍然有信息价值。 - Jagdish

4

在大多数情况下,在C程序中使用return和调用exit()来终止main()没有任何区别。

当有所不同的情况是,如果您创建的代码将在从main()返回后执行,并且依赖于main()中的局部变量。这种情况会通过setvbuf()等方式显现:

int main(void)
{
    char buffer[BUFSIZ];
    setvbuf(stdout, buffer, _IOFBF, BUFSIZ);
    …code using stdoutreturn 0;
}

在这个例子中,通过setvbuf()提供的缓冲区会在main()返回时超出作用域,但是刷新和关闭stdout的代码将尝试使用该缓冲区。这会导致未定义的行为。
另一种机制是使用一个访问来自main()的数据的函数的指针来调用atexit()。这更难设置,因为通过atexit()机制调用的函数不会被给予任何参数。因此,您必须执行以下操作:
static void *at_exit_data = 0;

static void at_exit_handler(void)
{
    char *str = at_exit_data;
    printf("Exiting: %s\n", str);
}

int main(void);
{
    char buffer[] = "Message to be printed via functions registered with at_exit()";
    at_exit_data = buffer;
    at_exit(at_exit_handler);
    …processing…
    return 0;
}

at_exit_data指向的缓冲区在程序从main()返回后已经停止存在,因此处理程序函数会引发未定义的行为。

有一个相关的函数at_quick_exit(),但是只有在调用quick_exit()函数时才会调用注册的函数,这排除了在main()返回后调用这些函数的可能性。


1
返回语句退出当前函数,而exit()退出程序。
they are the same when used in main() function

同时,return是一个语句,而exit()是一个需要包含stdlib.h头文件的函数。

如果您的程序中的任何一个函数调用了main函数,那么它们就是相同的。这种情况很少见;大多数程序不使用递归或可重入的main函数,但如果我们讨论语言细节,那么这是可能的。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接