65.9K
CodeProject 正在变化。 阅读更多。
Home

CFtpFileFind 空析构函数导致 MFC 6, NT 中崩溃

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (4投票s)

2002年4月15日

3分钟阅读

viewsIcon

78443

虚拟析构函数未调用正确 Close 函数的情况

引言

过去一年,我在客户现场度过了三个不愉快的日子。 幸运的是,我在 MFC 6.0 代码中找到了问题。 这就是给我带来麻烦的代码(基础)。

void CMyClass::OnGetFiles()
{
  ...
  CInternetSession is;
      
  // start an ftp session
  CFtpConnection *pf;

  try
  {
    pf = is.GetFtpConnection(..., ..., ...);  // Get an FTP connection
  }
  catch(CInternetException* e )
  {
    ...
  }

  // Go to the right directory
  if (!pf->SetCurrentDirectory("Whatever"))
  {
    ...
  }

  CFtpFileFind fff(pf);
     fff.FindFile();
  bContinue=TRUE;
  while (bContinue)
  {
    bContinue = fff.FindNextFile();
    ...
  }

  pf->Close();// close the ftp connection
  deletepf;

  is.Close();// close the internet session
}

当我们在(调试模式下)在最后一个括号处断点,并更进一步时,CFtpFileFind 对象 "fff" 的析构函数会显示一个(已处理的)C++ 异常。

原因

CFtpFileFind 派生自 CFileFind。 因此,当超出作用域时

  • 调用 CFtpFileFind 析构函数

    Inet.cpp MFC 代码是
  • CFtpFileFind::~CFtpFileFind()
    {
    }

    所以什么都没发生!

  • 然后,调用 CFileFind 的析构函数
    CFileFind::~CFileFind()
    {
          Close();
    }
  • 所以这会调用以下 Close
    void CFileFind::Close()
    {
          if (m_pFoundInfo != NULL)
          {
                delete m_pFoundInfo;
                m_pFoundInfo = NULL;
          }
    
          if (m_pNextInfo != NULL)
          {
                delete m_pNextInfo;
                m_pNextInfo = NULL;
          }
    
          if (m_hContext != NULL &&
              m_hContext != INVALID_HANDLE_VALUE)
          {
                CloseContext();
                m_hContext = NULL;
          }
    }
  • 依次调用以下 CFileFind::CloseContext
    void CFileFind::CloseContext()
    {
          ::FindClose(m_hContext);   // <<<<< C++ exception
          return;
    }
  • 当执行 ::FindClose(m_hContext); 时,就会发生 C++ 异常。

    在客户现场度过了几个不愉快的日子后,我们发现,可以通过在我们的函数中添加一行额外的代码来避免该异常(导致 Windows NT 上的崩溃)

      CFtpFileFind fff(pf);
         fff.FindFile();
      bContinue=TRUE;
      while (bContinue)
      {
        bContinue = fff.FindNextFile();
        ...
      }
      fff.Close(); // The solution!!!

    通过添加这一行,在 CFtpFileFind 对象超出范围之前,会调用 Close()

CFtpFileFind 没有 Close() 函数,因此调用 CFileFind::Close() 。 但现在 CloseContext() 调用会导致 CFtpFileFind::CloseContext()  (该对象是 CFtpFileFind)

void CFtpFileFind::CloseContext()
{
      if (m_hContext != NULL && m_hContext != INVALID_HANDLE_VALUE)
      {
            InternetCloseHandle(m_hContext);
            m_hContext = NULL;
      }

      return;
}

在这里,我们看到 - 不是一个简单的 ::FindClose() - 而是在 m_hContext 句柄上执行 InternetCloseHandle()

所以这就是区别!

  • 在糟糕的情况下(依赖于析构函数),调用 CFileFind::CloseContext()
  • 在解决方法的情况下(我们自己调用 fff.Close()),则调用 CFtpFileFind::CloseContext()

尽管 C++ 异常(在 NT 和 W2K 中都会发生)被处理,但我们的经验是,只有在 NT 中,它才会进一步导致严重的问题(=崩溃),即访问文件(也使用句柄)。 因此,显然 Win2K 以比 WinNT 更好的方式处理异常。

我们的解决方案是可以的,但您必须记住自己调用 Close()

事实上,这是一个 MFC 错误(与糟糕的 WinNT 异常处理相结合):如果 MFC 代码的 CFtpFileFind 析构函数从空更改为包含 Close();,则问题不会出现

因此,另一种更安全的解决方案是创建一个派生自 CFtpFileFind CMyFtpFileFind 类,其具有

CMyFtpFileFind::~CMyFtpFileFind
{
   Close();  // At last leading to the CloseContext() of CFtpFileFind
}

一些额外的信息

您可能想知道为什么 CFtpFileFind 析构函数调用 CFileFind 析构函数调用 CloseContext() 不会自动导致 CFtpFileFind::CloseContext()(您正在删除一个 CFtpFileFind 对象,不是吗?),而是导致 CFileFind::CloseContext()

这就是原因

  • 在三种情况下,编译器会在编译时静态解析对虚函数的调用

    3. 当虚函数在...基类的析构函数中被调用时。由于派生类对象...已经被析构,因此调用虚函数的基类实例。” - “C++ Primer 2nd Edition”,Stanley B. Lippman,第 463 页。

  • 类似的事情发生在析构期间的虚函数表中。 在你的析构代码执行之前,编译器生成的代码将虚函数表设置为析构函数所属类的虚函数表... 你的代码执行,然后编译器生成的代码调用基类析构函数。 再次,析构函数内的虚函数调用行为就像它们是静态的一样。 同样,这很有意义,因为一旦析构函数完成,该对象就不再存在,并且你不会希望在该对象被销毁后调用派生虚函数。 因此,每个析构函数做的第一件事是通过将虚函数表设置为其自己的虚函数表来消除一个派生程度。” - MSDN 杂志,2000 年 3 月,C++ 问答。

  • 用我自己的话来说:一旦你离开了 CFtpFileFind 析构函数,你就不能再调用任何 CFtpFileFind 函数了(虚函数表只指向 CFileFind 函数)。 因此,当您遇到调用 CloseContext 函数的 Close 函数时,它是 CFileFind::CloseContext 而不是 CFtpFileFind::CloseContext

(注意:我还没有检查此错误是否已在 MFC 7 中得到解决)

© . All rights reserved.