IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    C main() 的 exit() 和 return

    Xin LI发表于 2023-05-15 06:22:47
    love 0

    这里讨论一个犀利而无用的细节问题。事情的缘起是有人在 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 老师已于当地时间周六晚因突发心梗辞世, 在此让我们一起缅怀他。



沪ICP备19023445号-2号
友情链接