1 概述

在写一个简单的校验工具时,意外的发现了在 C++中的 Main 函数中使用returnexit退出效果存在不同,这也导致了我的程序core dump了。在检查core文件的同时,我发现如果使用exit则可能导致析构顺序与预想的方式不一致,进而导致段错误。总结一句话:exit不会优先将主函数内的局部变量析构,因此在单例模式下可能会产生异常。

2 起因

事情起因是我复用了现有的代码结构,分出来了一个入口实现数据库参数校验的功能,用于程序启动前参数校验提醒。整体来看是这样的:

  • OraOper 类,单例,有initfinal两个接口分别实现初始化和退出,有问题的是final接口内使用了logger输出日志。
  • Logger 类,单例,有init接口,实现初始化,没有主动析构入口,也就是默认程序退出时析构 以上是被复用的代码结构,因为一个main函数内存在多个出口(包含参数检查、连接检查、参数校验),因此一次一次调用OraOper::final是不现实的(不好看),因此我打算对其进行RAII封装,变成这样:
class OraOperRAII
{
public:
~OraOperRAII(){
  if(m_init_flag){
    OraOper::final();
  }
}
 
public:
bool init(){
  return OraOper::init();
}
std::unique_ptr<OraOper> get(){.....}
 
private:
bool m_init_flag{false};
};

接着在 main 函数里实例化并使用

int main()
{
  if(situation1){
    exit(-1);
  }
  if(initLogger()){
    exit(-1);
  }
  OraOperRAII raii;
  if(!raii.init()){
    exit(-1);
  }
  if(!check(raii.get(),...))
  {
    exit(-1);
  }
  return 0;
}

我当时认为,当程序退出后,析构的循序是这样的:

  1. OraOperRAII
  2. ~Logger 因为 Logger 是单例,是一个全局的静态变量,理论上应该最后析构。但实际上查看 core 文件时发现是它先于OraOperRAII析构,这明显不符合常理。

3 原因

在我查阅资料后,我发现有一篇博文分析了主函数returnexit的汇编,链接如下:

https://www.cnblogs.com/aquester/p/10333238.html

int main()
{
    X x(1);
 
    exit(0); // 和下一行二选一执行
 
    return(0);
}

在他的示例中,我们能注意到对于在使用return 0的场景中,在main结束时调用了X的析构,而exit则没有。

此时我就猜测,有没有一种可能,因为exit的原因导致了析构的优先级出现了异常,也就打破了原来的生命周期,使OraOperRAIILogger的析构优先级平级?进而导致了异常?

4 总结原因

问题就在于单例之间的互相依赖(我知道不好,之后再想办法解决吧)导致其在析构时需要注意析构顺序。

exit相比起return对原有的生命周期有一定的影响。

优先使用return