【问题标题】:Does this Microsoft CFileDialog example lead to a potential memory violation此 Microsoft CFileDialog 示例是否会导致潜在的内存冲突
【发布时间】:2017-06-19 14:02:37
【问题描述】:

我在使用 MFC CFileDialog 类时遇到了一些随机崩溃,因此我查看了来自 this page 的示例代码,内容如下;

#define MAX_CFileDialog_FILE_COUNT 99
#define FILE_LIST_BUFFER_SIZE ((MAX_CFileDialog_FILE_COUNT * (MAX_PATH + 1)) + 1)

CString fileName;
wchar_t* p = fileName.GetBuffer( FILE_LIST_BUFFER_SIZE );
CFileDialog dlgFile(TRUE);
OPENFILENAME& ofn = dlgFile.GetOFN( );
ofn.Flags |= OFN_ALLOWMULTISELECT;
ofn.lpstrFile = p;
ofn.nMaxFile = FILE_LIST_BUFFER_SIZE;

dlgFile.DoModal();
fileName.ReleaseBuffer();  

wchar_t* pBufEnd = p + FILE_LIST_BUFFER_SIZE - 2;  
wchar_t* start = p;
while( ( p < pBufEnd ) && ( *p ) )
  p++;
if( p > start )
{
  _tprintf(_T("Path to folder where files were selected:  %s\r\n\r\n"), start );
  p++;

  int fileCount = 1;
  while( ( p < pBufEnd ) && ( *p ) )
  {
    start = p;
    while( ( p < pBufEnd ) && ( *p ) )
      p++;
    if( p > start )
      _tprintf(_T("%2d. %s\r\n"), fileCount, start );
    p++;
    fileCount++;
  }
}

其中的my reading,语句fileName.ReleaseBuffer();使得缓冲区变量pinvalid指向的内存,使得剩余的代码容易发生内存冲突。同时,我还假设微软会在发布这些示例之前对其进行检查。我在这里遗漏了一些明显的东西吗?在不再需要缓冲区后,是否有任何理由在此处使用 CString 而不是简单的 new 后跟 delete

【问题讨论】:

  • 永远不要假设示例代码没有错误。无论是来自微软还是来自其他人。通过使用示例代码,所有错误都会自动归您所有。
  • 我没有,因此这篇文章,但我希望 Microsoft 在其 API 文档附带的代码示例中提供更好的标准,因为此代码通常被视为惯用的方式应该使用 API。正如这里的其他答案所示,这段代码有很多问题。
  • 这不是 Windows API 文档。它是 MFC,一个库,它的文档始终满足低于 Windows API 文档的标准。无论如何,人们希望 MFC 文档和示例代码也是正确的。如果我有任何希望,我会提交一份缺陷报告,即输入被合并。我还不够轻信,无法维持这种希望。
  • 公平点,我可能有点天真地期望更好,多年来在 MFC 上玩了很多“乐趣和游戏”。

标签: c++ mfc cfiledialog


【解决方案1】:

示例代码不是正式文档。这个样本是错误的。 documentation 是对的:

GetBuffer 返回的地址在调用ReleaseBuffer 后可能无效,因为额外的CSimpleStringT 操作可能会导致CSimpleStringT 缓冲区被重新分配。

该示例使用CString(通过原始指针和手动内存管理)进行自动内存管理和异常安全。后者更难通过手动内存管理来正确处理(尽管此示例也没有获得正确的异常安全性)。

如果您想修复示例代码以遵守合同,则需要进行以下更改:*

  1. wchar_t* pBufEnd = p + FILE_LIST_BUFFER_SIZE - 2; 替换为const wchar_t* pBufEnd = fileName.GetString() + FILE_LIST_BUFFER_SIZE - 2;
  2. wchar_t* start = p; 替换为const wchar_t* start = fileName.GetString();
  3. 将对话调用后代码中所有剩余的p 替换为新变量,初始化为const wchar_t* current = fileName.GetString();)。

这是一个常见错误。每当开发人员认为他们需要某种char* 时,他们忽略了他们需要const char*,几乎每种字符串类型都通过成员函数提供。


请注意,示例代码中还有其他错误,未在此答案中明确解决(如另一个 answer 中解释的字符类型不匹配)。


*可以在 answer 中找到检索所选文件列表的 C++ 实现。

【讨论】:

  • 整个 pBufEnd 也是一派胡言,因为 CFileDialog 提供了显式方法 'GetStartPosition' 和 'GetNextPathName' 来提取选定文件的名称。修复示例代码需要做的最重要的事情是将 fileName.ReleaseBuffer 移动到代码末尾,以停止无效的内存访问。显式调用 GetBuffer 和 ReleaseBuffer 也不完全是自动内存管理。
  • @ShaneMacLaughlin:需要大量修复才能使此代码合法,但移动 ReleaseBuffer 调用(尽可能)并不是一个好的解决方案。我的回答概述了如何将 ReleaseBuffer 呼叫留在原处,并且仍然使用指向受控序列的指针。 GetBuffer/ReleaseBuffer 是与 C 库交互所必需的工件。在这些调用之间,内存管理确实是手动的,但是一旦ReleaseBuffer 返回,您将获得所有自动内存管理。使用CString 是有好处的,但该示例大多是错误的。
  • 如果您将 ReleaseBuffer 保留在原处,然后继续使用 p,您正在访问的内存在理论上可以根据文档更改,因为它不再有效。虽然在实践中它可能只会被 CString 'dtor 释放或通过更改 CString 内容移动,但它似乎仍然是一种糟糕的处理方式。
  • @ShaneMacLaughlin:正确。在调用ReleaseBuffer 之后访问p 是未定义的。更新了我的答案以修复 p 的最终剩余用途。
【解决方案2】:

您可能会注意到规范和实现之间的差异。上面的代码有效,因为 CString 实现允许它,即使 CString 规范禁止它。

为了突出示例的质量:它混合了TCHARwchar_t。在tprintf("%s", start) 中,字符串start 必须是TCHAR*,但示例使用wchar_t* start

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-05
    • 1970-01-01
    • 2011-01-15
    • 2020-01-29
    • 1970-01-01
    • 2020-08-13
    • 2013-04-04
    • 2012-05-22
    相关资源
    最近更新 更多