C main() 的 exit() 和 return
这里讨论一个犀利而无用的细节问题。事情的缘起是有人在 GitHub 上提了一个 pull request 要求把许多程序的
main()
的终结部分从 exit(X)
改为 return X;
,我反对了这一变动。
值得注意的是,在实践上,从 main
中 return
和调用 exit(3)
几乎等效的(此处还是有细微差别,
后面将会讨论),原因是 C 运行环境库的启动部分(这部分会在连接过程中嵌入到可执行文件中,
FreeBSD 的实现中,这部分位于 lib/libc/csu/libc_start1.c
的 __libc_start1
:
void
__libc_start1(int argc, char *argv[], char *env[], void (*cleanup)(void),
int (*mainX)(int, char *[], char *[]))
{
/* ... */
exit(mainX(argc, argv, env));
}
但是目前大部分的用户态代码和 style(9) 均采用了调用 exit(3)
而不是 return
。
前面提到,两者之间存在些许的不同:当函数返回时,属于它的栈帧就销毁掉了。而当函数调用另外一个函数时,
属于它的栈帧依然存在。在追溯内存泄漏时,比较常见的办法是从有效栈帧开始沿着所有指针遍历并标记内存块,
这样,在完成时没有标记的已分配内存块就是泄漏的内存了。习惯上,C 程序在已经知道要调用 exit(3)
或其它终止进程的函数(部分编译器支持将这类函数标记为 __attribute__((__noreturn__))
属性,
在 FreeBSD 中这类函数通常会用 __dead2
来标记)时,在此前分配的内存无需再做显式地释放。
因此,如果在 main()
中在堆上分配(例如,通过 malloc(3))
了一些缓冲区而在 return
时没有释放,则这些缓冲区有可能被视作内存泄漏,
尽管实践上内核最终仍然会回收进程的全部资源,但相关的警告有可能给开发人员带来困扰。
不过,这些差异并不总是存在。例如,程序的 main()
函数可能完全不从堆上分配内存,
那么此时两者的区别就真的可以忽略不计了。在写新程序时,我认为两者基本上没什么区别,
个人比较倾向于使用 exit
,除非 main()
由于某些原因可能在其他地方由 C
运行环境以外的其它地方调用(一个比较典型的例子是嵌入 sh(1)
的那些命令:
如果这些命令的实现中使用了 exit()
,那么嵌入的版本就必须 fork()
并 wait()
,
而不是简单地像调用函数那样使用它们,这会让 shell 脚本的性能大幅下降),
但对没有这类要求,又没有在堆上分配内存的新程序来说,两种写法都可以接受。
今天从 Thomas Yao 那里 得知, 陈皓 @haoel 老师已于当地时间周六晚因突发心梗辞世, 在此让我们一起缅怀他。