注:
1. 本文討論的是怎么用Windows API遍歷目錄下的所有文件。除Windows API,還有一種Windows/Linux通用的方式,使用<io.h>。
2. 本文部分翻譯自MSDN,翻譯可能不准確。
WIN32_FIND_DATA結構
遍歷目錄下的文件需要用到WIN32_FIND_DATA結構。實際上有兩種結構:WIN32_FIND_DATAA和WIN32_FIND_DATAW。A和W分別代表ASCII和寬字符(Unicode)。定義UNICODE宏時,WIN32_FIND_DATA指WIN32_FIND_DATAW;否則指WIN32_FIND_DATAA。
下面是兩個結構的定義(minwinbase.h,VS2015):
typedef struct _WIN32_FIND_DATAA { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; _Field_z_ CHAR cFileName[ MAX_PATH ]; _Field_z_ CHAR cAlternateFileName[ 14 ]; #ifdef _MAC DWORD dwFileType; DWORD dwCreatorType; WORD wFinderFlags; #endif } WIN32_FIND_DATAA; typedef struct _WIN32_FIND_DATAW { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD dwReserved0; DWORD dwReserved1; _Field_z_ WCHAR cFileName[ MAX_PATH ]; _Field_z_ WCHAR cAlternateFileName[ 14 ]; #ifdef _MAC DWORD dwFileType; DWORD dwCreatorType; WORD wFinderFlags; #endif } WIN32_FIND_DATAW;
關於_MAC宏的部分可以忽略, 這是有歷史原因的——曾今Microsoft是Mac的最大開發者,為了方便Windows上的應用移植到Mac上,就使用_MAC宏,如果是Mac操作系統_MAC就是有定義的。(根據Stack Overflow)因為這里說的是Windows,就先把這個放一邊。
下面是每個結構成員的含義:
dwFileAttributes
一個文件(或路徑)的文件屬性。
文件屬性常量:
FILE_ATTRIBUTE_ARCHIVE(0x20):文件或目錄是檔案文件或目錄。應用程序使用這種屬性標記文件,表示備份或移除。
FILE_ATTRIBUTE_COMPRESSED(0x800):文件或目錄是壓縮的。對於一個文件,其中的所有數據都是壓縮的。對於一個目錄,對於新創建的文件和子目錄默認壓縮。
FILE_ATTRIBUTE_DEVICE(0x40):這個值保留給系統使用。
FILE_ATTRIBUTE_DIRECTORY(0x10):表示這是一個目錄。
FILE_ATTRIBUTE_ENCRYPTED(0x10):文件或目錄是加密的。對於一個文件,所有的數據流都被加密了。對於一個目錄,對於新創建的文件和子目錄默認加密。
FILE_ATTRIBUTE_HIDDEN(0x2):文件或目錄是隱藏的。遍歷文件夾時一般不包括它們。
FILE_ATTRIBUTE_INTEGRITY_STREAM(0x8000):路徑或用戶數據流被設置為integrity(只有ReFS volume支持)。遍歷文件夾時一般不包括它們。Integrity設置在文件重命名之后依然保留。如果一個文件被復制,目標文件將會是integrity,不管源文件或目標路徑是否是integrity。
FILE_ATTRIBUTE_NORMAL(0x80):文件沒有任何其它屬性。只能單獨使用。
FILE_ATTRIBUTE_NOT_CONTEXT_INDEXED(0x2000):文件或目錄不會被context indexing service標索引。
FILE_ATTRIBUTE_READONLY(0x1):文件為只讀。程序可以讀取該文件,但不能寫入或刪除。此屬性不適用於目錄。
……(太多了,有時間再全部列舉)
順帶提一下位標記(bit flags)。
如你所見,所有的文件屬性常量寫成二進制都只有一位是1,剩下的都是0。對於dwFileAttributes,將其寫成二進制的形式,它的一些位的值是有含義的——例如個位表示是否是只讀的(FILE_ATTRIBUTE_READONLY是1),16位表示是否是目錄(FILE_ATTRIBUTE_DIRECTORY是0x10,即16),32位表示是否是檔案文件/目錄(FILE_ATTRIBUTE_ARCHIVE是0x20,即32)……
那么怎么指定多個屬性呢?因為每種屬性寫成二進制都只有一位是1,剩下的都是0,所以可以使用按位or運算符(|)指定多個屬性,例如FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_DEVICE,這樣每種屬性對應的位都為1,其余的為0。
至於判斷是否具有某個屬性,可以用按位and運算符(&)。例如對於屬性attrib,判斷是否是目錄:attrib & FILE_ATTRIBUTE_DIRECTORY。如果不為0則是目錄,為0則不是。
ftCreationTime
FILETIME結構。指定一個文件或目錄的創建時間。如果文件系統不支持創建時間,此成員為0。
ftLastAccessTime
FILETIME結構。對於文件,指定文件最后被讀取、寫入,或(對於可執行文件)被運行的時間。對於目錄,指定目錄的創建時間。如果文件系統不支持最后一次寫入時間,此成員為0。
ftLastWriteTime
FILETIME結構。對於文件,指定文件最后被寫入、截短或重寫的時間(例如調用WriteFile()或SetEndOfFile()時)。日期和時間在文件屬性或描述符被改變時不會被更新。
nFileSizeHigh
DWORD。文件大小(以字節為單位)的高DWORD。除非文件大小大於MAXDWORD,否則值為0。文件大小等於(nFileSizeHigh * (MAXDWORD + 1)) + nFileSizeLow。
nFileSizeLow
DWORD。文件大小(以字節為單位)的低DWORD。
dwReserved0
DWORD。如果dwFileAttributes成員含有FILE_ATTRIBUTE_REPARSE_POINT屬性,這個成員指定重新分析點標簽(reparse point tag)。否則這個值是未定義的。
可能的值:
IO_REPARSE_TAG_CSV,IO_REPARSE_TAG_DEDUP,IO_REPARSE_TAG_DFS,IO_REPARSE_TAG_DFSR,IO_REPARSE_TAG_HSM,IO_REPARSE_TAG_HSM2,IO_REPARSE_TAG_MOUNT_POINT,IO_REPARSE_TAG_NFS,IO_REPARSE_TAG_SIS,IO_REPARSE_TAG_SIMLINK,IO_REPARSE_TAG_WIM。
dwReserved1
DWORD。保留給將來使用。
cFileName
CHAR/WCHAR數組,大小為MAX_PATH。文件名。
cAlternateFileName
CHAR/WCHAR數組,大小為14。文件的別名。名稱的格式為8.3文件名格式。
FILETIME結構
可以看到WIN32_FIND_DATA的ftCreationTime、ftLastAccessTime、ftLastWriteTime類型是FILETIME結構。那么FILETIME結構是怎樣的呢?下面是MSDN上的定義:
typedef struct _FILETIME { DWORD dwLowDateTime; DWORD dwHighDateTime; } FILETIME;
dwLowDateTime
文件時間的低DWORD。
dwHighDateTime
文件時間的高DWORD。
FILETIME結構表示的時間(距離Epoch的秒數)為dwHighDateTime * (MAXDWORD + 1) + dwLowDateTime。
FindFirstFile()/FindNextFile()/FindClose()函數
要查找文件,需要使用FindFirstFile()、FindNextFile()和FindClose()函數。
FindFirstFile()函數
HANDLE WINAPI FindFirstFile(
_In_ LPCTSTR lpFileName,
_Out_ LPWIN32_FIND_DATA lpFindFileData
);
搜索第一個文件,創建並返回搜索句柄。
lpFileName
CHAR/WCHAR指針(取決於是否定義UNICODE)。路徑或文件名。可以包含通配符,例如*或?。不能以\\字符結尾。如果以通配符、.字符或目錄名結尾,用戶必須有根目錄和所有子目錄的訪問權限。(遍歷目錄中的所有文件時,應以*.*結尾。)
lpFindFileData
WIN32_FIND_DATA指針。用於接收找到的文件/目錄的信息。
返回值
如果成功,函數將創建一個搜索句柄,可以使用該句柄調用FindNextFile()和FindClose()。如果失敗,返回INVALID_HANDLE_VALUE。
FindNextFile()函數
BOOL WINAPI FindNextFile(
_In_ HANDLE hFindFile,
_Out_ LPWIN32_FIND_DATA lpFindFileData
);
搜索下一個文件。
hFindFile
HANDLE。搜索句柄。
lpFindFileData
WIN32_FIND_DATA指針。用於接收找到的文件/目錄的信息。
返回值
如果成功,返回TRUE;如果失敗(例如找不到更多的文件了或其它原因),返回FALSE。
FindClose()函數
BOOL WINAPI FindClose(
_Inout_ HANDLE hFindFile
);
釋放搜索句柄。
hFindFile
HANDLE。搜索句柄。
返回值
如果成功,返回TRUE;如果失敗,返回FALSE。
通配符(wildcards)
*和?字符被用作通配符。
指定全部具有某個擴展名的文件
格式為*.ext(ext為擴展名)。例如指定所有.txt文件:"*.txt"。指定D:\Projects\目錄下所有.txt文件:"D:\\Projects\\*.txt"。
指定全部具有某個名稱的文件/目錄
格式為name.*(name為文件名)。例如指定所有名為readme(格式不限)的文件和目錄:"readme.*"。指定D:\Projects\目錄下的所有名為readme的文件:"D:\\Projects\\readme.*"。
指定具有一定長度的擴展名的文件
格式為name.???(name為文件名)。?的數量和擴展名長度一樣。例如指定所有擴展名為4個字符,名為index的文件:"index.????"。
指定具有一定長度的文件名的文件
格式為???.ext(ext為擴展名)。?的數量和文件名的長度一樣。例如指定所有擴展名為.txt,名字含有7個字符的文件:"???????.txt"。
當然還有更復雜的,例如*.???、????.*、*.*、????.???,分別是“所有擴展名長度為3的文件”、“所有文件名長度為4的文件/目錄”、“所有文件/目錄”、“所有文件名長度為4且擴展名長度為3的文件”。
當前目錄和上一級目錄
調用FindFirstFile()時,使用"."表示當前目錄,使用".."表示上一級目錄。FindFirstFile()和FindNextFile()所返回的文件/目錄名也可能是"."或"..",可以忽略。
最后的工作
我們還需要聲明一個HANDLE才能開始搜索:
HANDLE hFind;
程序示例
廢話了這么久,也是該上程序代碼了。
1. 遍歷某個目錄下的所有文件
遍歷某個目錄下的所有文件,並輸出文件名和文件大小。
#include <iostream> #include <cstring> #include <windows.h> void listFiles(const char * dir); int main() { using namespace std; char dir[100]; cout << "Enter a directory (ends with \'\\\'): "; cin.getline(dir, 100); strcat(dir, "*.*"); // 需要在目錄后面加上*.*表示所有文件/目錄 listFiles(dir); return 0; } void listFiles(const char * dir) { using namespace std; HANDLE hFind; WIN32_FIND_DATA findData; LARGE_INTEGER size; hFind = FindFirstFile(dir, &findData); if (hFind == INVALID_HANDLE_VALUE) { cout << "Failed to find first file!\n"; return; } do { // 忽略"."和".."兩個結果 if (strcmp(findData.cFileName, ".") == 0 || strcmp(findData.cFileName, "..") == 0) continue; if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) // 是否是目錄 { cout << findData.cFileName << "\t<dir>\n"; } else { size.LowPart = findData.nFileSizeLow; size.HighPart = findData.nFileSizeHigh; cout << findData.cFileName << "\t" << size.QuadPart << " bytes\n"; } } while (FindNextFile(hFind, &findData)); cout << "Done!\n"; }
以學校電腦為例,輸入"C:\",輸出如下:
$360Section <dir>
360SANDBOX <dir>
AUTOEXEC.BAT 0 bytes
boot.ini 210 bytes
bootfont.bin 322730 bytes
CONFIG.SYS 0 bytes
dell <dir>
Documents and Settings <dir>
Drivers <dir>
ExamClient <dir>
FPC <dir>
IO.SYS 0 bytes
Joinmax <dir>
ksd <dir>
MSDOS.SYS 0 bytes
MSOCache <dir>
...
(剩余輸出省略)
2. 遍歷某個目錄里的所有文件
注意是“某個目錄里”而不是“某個目錄下”,兩者是有區別的。“某個目錄里”除了目錄里的第一級的文件,還包括里面的子目錄里的所有文件。
和上面的例子一樣,使用listFiles()函數遍歷一個目錄里的所有文件。但不同的是,這里的listFiles()是遞歸調用的。
#include <iostream> #include <cstring> #include <windows.h> void listFiles(const char * dir); int main() { using namespace std; char dir[100]; cout << "Enter a directory (do not add \'\\\' in the end): "; cin.getline(dir, 100); listFiles(dir); return 0; } void listFiles(const char * dir) { using namespace std; HANDLE hFind; WIN32_FIND_DATA findData; LARGE_INTEGER size; char dirNew[100]; // 向目錄加通配符,用於搜索第一個文件 strcpy(dirNew, dir); strcat(dirNew, "\\*.*"); hFind = FindFirstFile(dirNew, &findData); do { // 是否是文件夾,並且名稱不為"."或".." if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY != 0 && strcmp(findData.cFileName, ".") != 0 && strcmp(findData.cFileName, "..") != 0 ) { // 將dirNew設置為搜索到的目錄,並進行下一輪搜索 strcpy(dirNew, dir); strcat(dirNew, "\\"); strcat(dirNew, findData.cFileName); listFiles(dirNew); } else { size.LowPart = findData.nFileSizeLow; size.HighPart = findData.nFileSizeHigh; cout << findData.cFileName << "\t" << size.QuadPart << " bytes\n"; } } while (FindNextFile(hFind, &findData)); FindClose(hFind); }
呼,總算講完了~如果有什么問題,請在評論中提出哦~