創作內容

4 GP

【程式】檔案操作—Windows篇

作者:Shark│2017-12-25 16:28:14│巴幣:56│人氣:3915
很多人聽過標準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,目前先不介紹。
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=3831924
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:Windows程式設計|Windows API|C語言|程式

留言共 1 篇留言

這根本不正常
臨時要用 終於找到比較好的解釋

03-15 22:25

我要留言提醒:您尚未登入,請先登入再留言

4喜歡★shark0r 可決定是否刪除您的留言,請勿發表違反站規文字。

前一篇:21週年站聚遊記... 後一篇:【程式】檔案操作—Lin...

追蹤私訊切換新版閱覽

作品資料夾

face基於日前微軟官方表示 Internet Explorer 不再支援新的網路標準,可能無法使用新的應用程式來呈現網站內容,在瀏覽器支援度及網站安全性的雙重考量下,為了讓巴友們有更好的使用體驗,巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業系統版本才可使用)

face我們了解您不想看到廣告的心情⋯ 若您願意支持巴哈姆特永續經營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學】