迭代实现递归枚举文件和子文件夹
又一个枚举文件的实现。
引言
微软提供了一个关于列出目录中的文件的示例代码,但该示例代码不能用于列出子目录中的文件,那么我们该如何做呢?
枚举/搜索文件和子文件夹实际上是一项相当基础的技能,人们已经发布了许多关于此的文章,您可以在此处查看其他示例。 但是,许多这些实现以递归方式枚举文件。 作为一名编程初学者,我只是想知道如何非递归地做到这一点。
递归实现
在许多情况下,递归实现可以非常优雅,如果您已经知道如何递归地执行此操作,那么您可以简单地跳过这部分。
要在 Windows 中搜索文件,我们可以使用 API FindFirstFile
/FindNextFile
(或SHGetDataFromIDList
,或者 boost 中的FileSystem
库),并且一个典型的实现是递归执行此操作。
#include "strsafe.h"
bool EnumerateFolder(LPCTSTR lpcszFolder, int nLevel = 0)
{
WIN32_FIND_DATA ffd;
TCHAR szDir[MAX_PATH];
HANDLE hFind = INVALID_HANDLE_VALUE;
StringCchCopy(szDir, MAX_PATH, lpcszFolder);
StringCchCat(szDir, MAX_PATH, TEXT("\\*"));
// Find the first file in the directory.
hFind = FindFirstFile(szDir, &ffd);
if (INVALID_HANDLE_VALUE == hFind)
{
return false;
}
// List all the files in the directory with some info about them.
TCHAR szOutLine[MAX_PATH] = {0};
for (int ii = 0; ii < nLevel; ++ii)
szOutLine[ii] = _T('\t');
do
{
if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if ( _T('.') != ffd.cFileName[0] )
{
_tprintf(TEXT("%s%s <DIR>\n"),
szOutLine, ffd.cFileName);
StringCchCopy(szDir+_tcslen(lpcszFolder)+1,
MAX_PATH, ffd.cFileName);
EnumerateFolder(szDir, nLevel+1); // Here's the recursive call!
}
}
else
{
_tprintf(TEXT("%s%s\n"), szOutLine, ffd.cFileName);
}
}
while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
return true;
}
递归的问题
递归实现的可读性通常很好,但我开始考虑潜在的堆栈溢出问题。
在 Windows 中,每个线程都被授予默认堆栈大小为 1 MB,这在大多数情况下可能足够大。
在上面的函数 EnumerateFolder()
中,它将占用大约 580 字节的局部变量(忽略 szOutLine
)。 这意味着,理论上,我们可以枚举多达 1808 个子目录级别。
但是,由于路径的缓冲区大小 (MAX_PATH
) 限制为 260 个字符,因此无法达到这么多级别。
实际上,微软确实提供了打破此限制的机制。
在该函数的 ANSI 版本中,名称限制为 MAX_PATH
个字符。 要将此限制扩展到 32,767 个宽字符,请调用该函数的 Unicode 版本,并在路径前面加上“\\?”。
尽管有些人说260 个字符就足够了,并且堆栈可能不会那么容易溢出,但我认为尽可能避免递归不是一个坏主意;我说的对吗? 这种实现更像是对我这样的初学者的一种实践,所以让我们尽情地编写它吧。
迭代实现
一般来说,有两种方法可以进行迭代搜索
由于Raymond Chen解释说广度优先搜索更适合文件系统树遍历,所以我决定在此处发布 BFS 实现片段。
对于那些仍然想知道如何使用 DFS 实现此功能的人,您可以在此页面中深入研究演示代码并打开源文件 FileEnumerator.h,然后您将看到我准备了一些 #ifdef
开关,您可以简单地尝试注释掉/取消注释这些宏
//#define FILEENUMERATOR_RECURSION
//#define FILEENUMERATOR_DOCOUNTING
#ifndef FILEENUMERATOR_RECURSION
#define FILEENUMERATOR_BFS
#endif
现在让我们编写一个 BFS 实现。 首先,我们需要一个封装基本操作的类
class CFileFinder
{
public:
CFileFinder(LPCTSTR lpcszInitDir, FindFileData& fileInfo)
: m_fileInfo(fileInfo)
{
Init(lpcszInitDir);
}
public:
inline bool FindFirst()
{
return EnumCurDirFirst();
}
inline bool FindCurDirNext()
{
bool bRet = ::FindNextFile(m_hFind, &m_fileInfo) != FALSE;
if ( bRet )
{
m_szPathBuffer.resize(m_nFolderLen);
m_szPathBuffer += m_fileInfo.cFileName;
}
else
{
::FindClose(m_hFind);
m_hFind = INVALID_HANDLE_VALUE;
}
return bRet;
}
virtual bool Finish() const
{ return INVALID_HANDLE_VALUE == m_hFind; }
inline LPCTSTR GetPath() const
{return STRBUFFER(m_szPathBuffer) + EXTEND_FILE_PATH_PREFIX_LEN;}
inline const FindFileData& GetFileInfo() const { return m_fileInfo; }
inline bool IsDirectory() const
{ return (m_fileInfo.dwFileAttributes &
FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; }
inline bool IsDot() const
{ return (m_fileInfo.cFileName[0] == '.') &&
((m_fileInfo.cFileName[1] == '.') ||
(m_fileInfo.cFileName[1] == '\0')); }
protected:
virtual bool EnumCurDirFirst()
{
m_szPathBuffer.resize(m_nFolderLen+2);
m_szPathBuffer[m_nFolderLen++] = _T('\\');
m_szPathBuffer[m_nFolderLen] = _T('*');
HANDLE hFind = ::FindFirstFile(STRBUFFER(m_szPathBuffer), &m_fileInfo);
bool bRet = hFind != INVALID_HANDLE_VALUE;
if ( bRet )
{
m_hFind = hFind;
m_szPathBuffer.resize(m_nFolderLen);
m_szPathBuffer += m_fileInfo.cFileName;
}
return bRet;
}
void Init(LPCTSTR lpcszInitDir)
{
m_nFolderLen = _tcslen(lpcszInitDir);
m_szPathBuffer = lpcszInitDir;
if ( m_szPathBuffer[m_nFolderLen-1] == _T('\\') )
{
m_szPathBuffer.erase(m_nFolderLen-1);
--m_nFolderLen;
}
}
protected:
FindFileData& m_fileInfo;
tstring m_szPathBuffer;
UINT m_nFolderLen;
HANDLE m_hFind;
};
然后我们需要将待处理的子目录放入队列中
#include <boost/shared_ptr.hpp>
typedef boost::shared_ptr<CFileFinder> FileFindPtr;
typedef std::queue<FileFindPtr> FileFindQueue;
bool CFileEnumeratorBase::EnumerateBFS
( LPCTSTR lpcszInitDir, FindFileData& findFileData, HANDLE hStopEvent /*= NULL*/ )
{
// Breadth-first searching, BFS:
FileFindPtr finder = NULL;
try
{
finder = new CFileFinder(lpcszInitDir, findFileData);
}
catch (bad_alloc&)
{
CFE_ASSERT(0);
return false;
}
bool bRet = true;
FileFindQueue finderQueue;
if ( !finder->FindFirst() )
{
m_dwLastError = ::GetLastError();
OnError(finder->GetPath());
return false;
}
else
{
while( !finder->Finish() && !IsStopEventSignaled() )
{
const FindFileData& fileInfo = finder->GetFileInfo();
if( finder->IsDirectory() )
{
if ( !finder->IsDot() )
{
if ( CheckUseDir
(finder->GetPath(), fileInfo) )
{
HandleDir(finder->GetPath(),
fileInfo);
FileFindPtr newFinder = NULL;
try
{
newFinder =
new CFileFinder
(finder->GetPath(),
findFileData);
finderQueue.push
(newFinder);
}
catch (bad_alloc&)
{
CFE_ASSERT(0);
}
}
}
}
else
{
if ( CheckUseFile(finder->GetPath(), fileInfo) )
{
HandleFile(finder->GetPath(), fileInfo);
}
}
if ( !finder->FindCurDirNext() )
{
FinishedDir( finder->GetPath() );
if ( finderQueue.empty() )
break;
else
{
while ( !IsStopEventSignaled() )
{
FileFindPtr nextFinder =
finderQueue.front();
finderQueue.pop();
finder = nextFinder;
if ( !finder->FindFirst() )
{
m_dwLastError =
::GetLastError();
if ( !OnError
(finder->GetPath()) )
{
return false;
}
}
else
break;
}
}
}
}
}
return bRet;
}
Using the Code
类的初始设计受到Andreas Saurwein Franci Gonçalves 的文章的启发,请先阅读他的文章! 然后您需要实现从 CFileEnumeratorBase
或 CFilteredFileEnumerator
派生的您自己的类。
提供了 CFileEnumeratorBase
用于基本的文件枚举,并提供了 CFilteredFileEnumerator
用于模式过滤枚举。
以下是从 CFilteredFileEnumerator
派生的示例类
class CFileInfoEnumerator : public CFilteredFileEnumerator
{
public:
CFileInfoEnumerator();
virtual ~CFileInfoEnumerator();
public:
enum FileInfoType
{
FIT_INVALID = -1,
FIT_FIRST,
FIT_FILENAME = FIT_FIRST,
FIT_FILEPATH,
FIT_FILEEXT,
FIT_MODIFIED,
FIT_SIZE,
};
struct FileInfo
{
std::string sFileInfo[FIT_SIZE];
};
public:
int GetFileCount() const { return m_arrFileInfo.size(); }
const FileInfo* GetFileInfo(int nIndex);
protected:
virtual void HandleFile(LPCTSTR lpcszPath, const FindFileData& ffd);
protected:
typedef std::vector<fileinfo> FileInfoArray;
FileInfoArray m_arrFileInfo;
};
#define DEFAULT_SORT_ASCENDING true
CFileInfoEnumerator::CFileInfoEnumerator()
: m_nSortInfoType(FIT_INVALID)
, m_bSortAscending(DEFAULT_SORT_ASCENDING)
{
}
CFileInfoEnumerator::~CFileInfoEnumerator()
{
}
void CFileInfoEnumerator::HandleFile( LPCTSTR lpcszPath, const FindFileData& ffd )
{
FileInfo fileInfo;
fileInfo.sFileInfo[FIT_FILENAME] = ffd.cFileName;
fileInfo.sFileInfo[FIT_FILEPATH] = lpcszPath;
std::string::size_type nDotPos = fileInfo.sFileInfo[FIT_FILENAME].rfind(_T('.'));
if ( nDotPos >= 0 )
fileInfo.sFileInfo[FIT_FILEEXT] =
fileInfo.sFileInfo[FIT_FILENAME].c_str()+nDotPos+1;
SYSTEMTIME st;
#define FTIME_BUFFER_SIZE 255
TCHAR szModified[FTIME_BUFFER_SIZE+FTIME_BUFFER_SIZE],
szLocalDate[FTIME_BUFFER_SIZE], szLocalTime[FTIME_BUFFER_SIZE];
FILETIME ft = ffd.ftLastWriteTime;
FileTimeToLocalFileTime( &ft, &ft );
FileTimeToSystemTime( &ft, &st );
GetDateFormat( LOCALE_USER_DEFAULT, DATE_SHORTDATE,
&st, NULL, szLocalDate, FTIME_BUFFER_SIZE );
GetTimeFormat( LOCALE_USER_DEFAULT, TIME_NOSECONDS,
&st, NULL, szLocalTime, FTIME_BUFFER_SIZE );
_sntprintf(szModified, FTIME_BUFFER_SIZE+FTIME_BUFFER_SIZE,
_T("%s %s"), szLocalDate, szLocalTime);
fileInfo.sFileInfo[FIT_MODIFIED] = szModified;
m_arrFileInfo.push_back(fileInfo);
}
const CFileInfoEnumerator::FileInfo* CFileInfoEnumerator::GetFileInfo( int nIndex )
{
if ( 0 <= nIndex && nIndex < (int)m_arrFileInfo.size() )
{
return &m_arrFileInfo.at(nIndex);
}
return NULL;
}
以及如何使用它
CFileInfoEnumerator enumerator;
TCHAR chDir[MAX_PATH];
GetCurrentDirectory(MAX_PATH, chDir);
enumerator.SetFilterPatterns(_T("*.exe;*.dll;*.h;*.c;*.cpp;"));
enumerator.Enumerate(chDir);
关于演示
我编写了一个控制台应用程序来演示源代码的用法。
CodeProject 中启发我的其他项目
历史
- 2010-7-10:初始发布
- 2010-11-25:提供了 DFS 实现
- 2010-12-29:跳过 junction (reparse point) 以避免无限循环