很多人聽過標準C裡的fopen、fread、fclose這些函式,還有檔案有文字檔和二進位檔兩種吧。
不過回想一下作業系統的理論,檔案跟前篇的視窗一樣,也需要作業系統提供功能才能使用,C標準函式並不是系統直接支援的,是把系統原生的函式包裝一層。
直接用系統原生API第一個好處當然是減少一層包裝,效能比較好。此外隨著作業系統演變成多人多工,作業系統都增加了權限管理的功能,管理權限的方式跟平臺相關所以C語言標準沒有定義,想控制權限還是要用系統原生API。
(本篇以前貼在另一個地方,移到這裡並做些修改)
參考:
MSDN裡的檔案操作函式一覽下面會看到DWORD或BOOL之類的型態名稱,是把整數型態取另一個名稱,這裡有說明。
Windows Data Types之前的視窗篇也可看一下,有些之前提過的觀念就不再提了。
如何建一個視窗—Windows API篇
Windows裡原生的開檔函式是CreateFile
(其實這是個macro,實際存在的函式是CreateFileA和CreateFileW,見下面說明)
#define UNICODE #include<windows.h> //本篇提到的函式都是include此檔
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile ); |
MSDN的說明有七個參數看起來很煩,但它跟CreateWindow一樣使用率很高,建議背下來。
1:檔名,型態「TSTR」如果有#define UNICODE就變成WCHAR型態,否則是char型態。
Windows API裡型態名稱前面加LP代表指標,C是const,所以LPCTSTR昰const WCHAR*或const char*,查MSDN要看得懂這些縮寫。
2:把檔案開成讀、寫還是讀+寫模式,看情況填GENERIC_READ、GENERIC_WRITE或GENERIC_READ|GENERIC_WRITE。
MSDN有寫可以填其他值做細部控制,但通常這三個就夠用。
3:允不允許其他程式讀、寫、刪除檔案,可填FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_DELETE或是三者的位元or。
有時候刪檔會跳出「無法刪除……有其他人或其他程式正在使用它……」的訊息就是這個參數在作用。為了避免使用者的困擾,請只用必要的限制,用完檔案也請記得關檔。
一般都用FILE_SHARE_READ,因為兩個程式同時讀一個檔案不會發生錯誤,但兩個程式同時寫一個檔案就有互相蓋掉的問題。
4:權限資訊,沒用到的話可填NULL。
5:檔案已存在或不存在時要怎麼做,列個表比較清楚
值 |
檔案已存在 |
檔案不存在 |
CREATE_ALWAYS |
覆蓋舊檔 |
建新檔 |
CREATE_NEW |
失敗 |
建新檔 |
OPEN_ALWAYS |
開檔 |
建新檔 |
OPEN_EXISTING |
開檔 |
失敗 |
TRUNCATE_EXISTING |
覆蓋舊檔 |
失敗 |
6:檔案屬性。
在檔案總管裡,在檔案上按右鍵選「內容」,可以設定如下的屬性,就是這個參數,通常不用設特別的屬性,填FILE_ATTRIBUTE_NORMAL即可。
查MSDN會看到能填的值很多,有興趣的自己試吧,很多我也沒用過。
另外檔案總管的資料夾選項有這些設定
如果此參數填FILE_ATTRIBUTE_HIDDEN,要開啟「顯示隱藏的檔案、資料夾及磁碟機」才看得到檔案。
如果填FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_HIDDEN,要把「隱藏保謢的作業系統檔案」取消才看得到,有的惡意軟體會用這個方法把自己隱藏。
7:字面意思是樣板,看MSDN的說明是建立新檔時會複製此檔的屬性,此參數很少用,通常就填NULL。
傳回值是一個handle,如視窗篇所說,handle代表一個Windows內部的物件,之後對此檔案讀、寫、取得屬性就傳入這個handle給函式。
如果開檔失敗會傳回INVALID_HANDLE_VALUE(=-1),要注意不是傳回NULL(=0),可用if(handle==INVALID_HANDLE_VALUE)判斷,檔案操作完要呼叫CloseHandle(handle)關閉。
例如遊戲要讀取資料檔,這是讀取已存在的檔案,如果找不到檔案代表有什麼意外讓檔案不見,應該視為失敗並跳訊息通知使用者,不該建立新檔,就像這樣呼叫函式
HANDLE file=CreateFile(L"image.shp", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); |
另外跟視窗篇的RegisterClassA和RegisterClassW一樣,查.h檔可看到實際存在的函式是CreateFileA和CreateFileW,
CreateFileA:檔名是ANSI字串,第一參數是const char*。
CreateFileW:檔名是Unicode字串,第一參數是const WCHAR*。
CreateFile是個macro看情況代換,如果有#define UNICODE則換成CreateFileW,反之是CreateFileA。在Unicode環境下如果想用ANSI檔名,可以直接呼叫CreateFileA。
Windows API有字串參數的函式幾乎都有兩個版本,如下面的GetFileAttributesEx和FindFirstFile也是。
接下來介紹一些檔案操作函式
一、取得檔案資訊
從handle取得檔案大小
DWORD GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh); BOOL GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER lpFileSize);
//檔案大於4GB時的做法 DWORD high; DWORD low=GetFileSize(file, &high); uint64_t fileSize1=((uint64_t)high<<32)|low;
LARGE_INTEGER fileSize2; GetFileSizeEx(file, &fileSize2); //此時fileSize2.QuadPart是檔案大小 |
可以支援大於4GB(2^32)的檔案,兩個函式處理大檔案的方法不一樣。
GetFileSize()本身傳回低32 bit,如果lpFileSizeHigh不是NULL則傳回高32 bit。lpFileSizeHigh可以填NULL,但這樣就不能支援大檔案。
LARGE_INTEGER本身就是64位元,GetFileSizeEx()把64位元都用這個指標傳回。
Windows API裡WORD型態是無號16位元整數(=uint16_t),DWORD是double word所以是32位元,而QWORD是64位元。
BOOL是32位元整數,C語言的規定是非0代表true,0代表false,此處函式成功時傳回非0,失敗時傳回0。
從handle取得檔案資訊(大小、建立時間、修改時間等等)
BOOL GetFileInformationByHandle(HANDLE hFile, LPBY_HANDLE_FILE_INFORMATION lpFileInformation); |
把檔案資訊存入BY_HANDLE_FILE_INFORMATION結構,至於此結構包含什麼就請自己查。
從檔名取得檔案資訊
BOOL GetFileAttributesEx(LPCTSTR lpFileName, GET_FILEEX_INFO_LEVELS fInfoLevelId, LPVOID lpFileInformation);
//用法 WIN32_FILE_ATTRIBUTE_DATA fileAttr; GetFileAttributesEx(fileName, GetFileExInfoStandard, &fileAttr); |
沒有直接從檔名取得檔案大小的函式,必須用這個函式。
第二參數是個enum,第三參數宣告成void*似乎是想保留擴充空間,根據不同類型傳回不同的資訊,但目前只有如上一種用法。
字尾有Ex那當然有個函式叫GetFileAttributes(),這個取得的資訊很少,只能取得CreateFile第六參數設定的屬性。
另外在不開檔的情況下,從檔名檢查檔案是否存在也是用GetFileAttributesEx(),如果函式本身傳回false,然後呼叫GetLastError()傳回ERROR_FILE_NOT_FOUND就代表檔案不存在。
Windows API發生error時會把出錯原因記錄在一個全域變數,GetLastError()可取得這個變數得知原因。
二、讀寫檔
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); |
C語言標準有文字和二進位兩種檔案,從底層的角度來看實際上只有二進位檔(畢竟電腦裡的資料就是一堆bytes),要讀取就是給一塊記憶體和長度,讓系統把資料填進去。C語言讀取文字檔是先用ReadFile()讀binary資料,把資料做一些轉換(轉換換行符號、檢查換行符號的位置等等),再傳回來。
參數如下
1:用CreateFile()開啟的handle。
2、3:一塊記憶體buffer的指標和長度。
4:傳回實際讀取的byte數,宣告一個DWORD型態的變數然後把它的指標傳入。
程式不一定需要這個值,但是Windows規定如果lpOverlapped是NULL,這個參數就不能是NULL。
5:檔案開成OVERLAPPED模式時會用到,沒用到時就填NULL。
至於何謂OVERLAPPED模式?overlap字面意思是重疊,另一個名稱比較容易懂:非阻擋模式(non-blocking mode)。
阻擋模式是呼叫ReadFile()時把資料全部讀完才繼續執行下一行,非阻擋則是不等待,程式繼續執行而讀資料在背景同時做。非阻擋可以避免讀取時間很長讓程式卡住,但是使用資料前必須檢查是否已讀完,檢查就會用到struct OVERLAPPED。
雖然如此,筆者沒用過OVERLAPPED模式,因為通常阻擋模式也不會造成問題,如果真的需要非阻擋模式就開一個新thread讀寫檔案,不用這個功能。
學過C語言應該知道檔案指標這個東西,移動檔案指標要用SetFilePointer()
DWORD SetFilePointer( HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod ); |
第二、三參數是要移的byte數,可正可負,第三個是用在2GB以上的情況(LONG的範圍是正負各2^31),不需要時可填NULL。
第四參數是以哪裡為基準移動,可填以下值。
FILE_BEGIN:檔案開頭。
FILE_CURRENT:目前位置。
FILE_END:檔案結尾。此時長度填正數就會超過檔案長度,所以通常是填負數。
傳回值是移動後的位置。
另外還有個SetFilePointerEx(),處理長度大於4GB的方法不同。
例:讀某個檔案的前16 bytes,跳過之後128 bytes,再讀1024bytes(file是已經開好的handle)
char buffer1[16]; char buffer2[1024]; DWORD bytes; ReadFile(file, buffer1, 16, &bytes, NULL); SetFilePointer(file, 128, NULL, FILE_CURRENT); ReadFile(file, buffer2, 1024, &bytes, NULL); |
寫檔用WriteFile()
BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped ); |
參數跟ReadFile差不多,第二參數是要寫入的資料,第三個是byte數。
三、列出資料夾裡的所有檔案,用以下三個函式。
a.用FindFirstFile()輸入檔名,傳回一個搜尋handle和找到的第一個檔案,找不到時傳回INVALID_HANDLE_VALUE。
b.用FindNextFile()一個一個取得符合條件的檔案,已搜尋完成或有error會傳回false。
c.用完handle後用FindClose()關閉。
檔名可包含萬用字元?和*,?代表任何值的「一個」字元,*則是不限內容不限字數的字元。
傳入的WIN32_FIND_DATA結構會填入檔案的資訊,可寫成一個for迴圈。
如下會找出目前資料夾下所有附檔名為txt的檔案。
WIN32_FIND_DATA findData; BOOL result=1; HANDLE findHandle=FindFirstFile(L"*.txt", &findData); for(; result!=0; result=FindNextFile(findHandle,&findData)){ //此時findData.cFileName是檔名,在這裡處理檔案 //findData也包含檔屬性 } FindClose(findHandle); |
此函式會同時搜尋資料夾,可檢查findData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY,如果結果是非0則是資料夾,反之是普通檔案。
這樣可讓程式支援外掛功能,例如規定一個資料夾專門放自製資料,程式去尋找資料夾裡所有檔案就可讀取使用者自製的資料。
附帶一提,有些不是檔案的物件也是用本篇提到的函式操作,例如named pipe,目前先不介紹。