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






4.25/5 (4投票s)
2002年4月15日
3分钟阅读

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 中得到解决)