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

迭代实现递归枚举文件和子文件夹

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (19投票s)

2010年7月10日

CPOL

3分钟阅读

viewsIcon

62352

downloadIcon

3665

又一个枚举文件的实现。

QOF_SnapShot.gif

演示应用程序,QuickOpenFiles。

引言

微软提供了一个关于列出目录中的文件的示例代码,但该示例代码不能用于列出子目录中的文件,那么我们该如何做呢?

枚举/搜索文件和子文件夹实际上是一项相当基础的技能,人们已经发布了许多关于此的文章,您可以在此处查看其他示例。 但是,许多这些实现以递归方式枚举文件。 作为一名编程初学者,我只是想知道如何非递归地做到这一点。

递归实现

在许多情况下,递归实现可以非常优雅,如果您已经知道如何递归地执行此操作,那么您可以简单地跳过这部分。

要在 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 个字符就足够了,并且堆栈可能不会那么容易溢出,但我认为尽可能避免递归不是一个坏主意;我说的对吗? 这种实现更像是对我这样的初学者的一种实践,所以让我们尽情地编写它吧。

迭代实现

一般来说,有两种方法可以进行迭代搜索

  1. 深度优先搜索 (DFS)
  2. 广度优先搜索 (BFS)

由于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 的文章的启发,请先阅读他的文章! 然后您需要实现从 CFileEnumeratorBaseCFilteredFileEnumerator 派生的您自己的类。

提供了 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 中启发我的其他项目

  1. CEnum - 文件枚举和文件通配符类
  2. 文件和目录枚举
  3. XEditPrompt - 基于 CEdit 的具有类似 web 提示的控件

历史

  • 2010-7-10:初始发布
  • 2010-11-25:提供了 DFS 实现
  • 2010-12-29:跳过 junction (reparse point) 以避免无限循环
© . All rights reserved.