跳轉到內容

Windows 程式設計/輸入輸出

來自華夏公益教科書,自由的教科書

前面的許多章節都試圖闡明 Windows 圖形介面,但本章將開始轉向 Windows 作業系統基礎的內部工作原理。在本章中,我們將討論輸入和輸出例程。這包括(但不限於)檔案 I/O、控制檯 I/O,甚至裝置 I/O。

檔案 API

[編輯 | 編輯原始碼]

檔案,就像 Windows 平臺上的其他所有內容一樣,由控制代碼管理。當您想要讀取或寫入檔案時,您必須首先開啟該檔案的控制代碼。控制代碼開啟後,您可以在讀/寫操作中使用該控制代碼。事實上,所有 I/O(包括控制檯 I/O 和裝置 I/O)都是如此:您必須開啟用於讀/寫的控制代碼,並且必須使用該控制代碼執行您的操作。

CreateFile

[編輯 | 編輯原始碼]

我們將從本章中經常會看到的一個函式開始:CreateFile。CreateFile 是用於在您的系統中開啟 I/O 控制代碼的通用函式。即使名稱沒有表明,CreateFile 也用於開啟控制檯控制代碼和裝置控制代碼。正如 MSDN 文件所說

The CreateFile function creates or opens a file, file stream, directory, 
physical disk, volume, console buffer, tape drive, communications resource, 
mailslot, or named pipe. The function returns a handle that can be used to 
access an object.

現在,這是一個功能強大的函式,並且隨著強大而來在使用該函式方面的一定困難。不用說,CreateFile 比標準 C STDLIB fopen 稍微複雜一些。

HANDLE CreateFile(
 LPCTSTR lpFileName,
 DWORD dwDesiredAccess,
 DWORD dwShareMode,
 LPSECURITY_ATTRIBUTES lpSecurityAttributes,
 DWORD dwCreationDisposition,
 DWORD dwFlagsAndAttributes,
 HANDLE hTemplateFile);

正如可以猜測的那樣,“lpFileName”引數是要開啟的檔案的名稱。“dwDesiredAccess”指定檔案控制代碼的所需訪問許可權。在最基本的情況下,對於檔案,此引數可以指定讀操作、寫操作或執行操作。但是,不要被愚弄,這裡可以使用許多不同的選項,用於不同的應用程式。最常見的操作是 GENERIC_READ、GENERIC_WRITE 和 GENERIC_EXECUTE。如果需要,這些可以按位或運算來獲得讀+寫訪問許可權。

檔案控制代碼可以選擇共享或鎖定。其他程序可以同時開啟和訪問共享檔案。如果檔案未共享,則嘗試訪問該檔案的其他程式將失敗。“dwShareMode”指定其他應用程式是否可以訪問該檔案。將 dwShareMode 設定為零表示檔案訪問不可共享,並且其他應用程式在檔案控制代碼開啟時嘗試訪問該檔案將失敗。其他常見值是 FILE_SHARE_READ 和 FILE_SHARE_WRITE,它們分別允許其他程式開啟讀控制代碼和寫控制代碼。

lpSecurityAttributes 是指向 SECURITY_ATTRIBUTES 結構的指標。此結構可以幫助保護檔案免受不必要的訪問。我們將在後面的章節中討論安全屬性。現在,您始終可以將此欄位設定為 NULL。

dwCreationDisposition 成員最好命名為“dwCreateMode”或類似名稱。此位標誌允許您根據不同的標誌值確定檔案將如何開啟

CREATE_ALWAYS
始終建立一個新檔案。如果檔案已經存在,它將被刪除並覆蓋。如果檔案不存在,則建立它。
CREATE_NEW
如果檔案存在,則函式失敗。否則,建立一個新檔案。
OPEN_ALWAYS
開啟檔案,如果檔案存在,則不擦除內容。如果檔案不存在,則建立一個新檔案。
OPEN_EXISTING
開啟檔案,如果檔案已經存在,則不擦除內容。如果檔案不存在,則函式失敗。
TRUNCATE_EXISTING
開啟檔案,只有在檔案存在時才打開。當檔案開啟時,所有內容都被刪除,並且檔案長度設定為 0 位元組。如果檔案不存在,則函式失敗。使用 TRUNCATE_EXISTING 開啟時,您必須指定 GENERIC_WRITE 標誌作為訪問模式,否則函式將失敗。

dwFlagsAndAttributes 成員指定了一系列用於控制檔案 I/O 的標誌。如果 CreateFile 函式用於建立非檔案控制代碼的內容,則不使用此引數,可以將其設定為 0。對於訪問普通檔案,應使用標誌 FILE_ATTRIBUTE_NORMAL。但是,還有 FILE_ATTRIBUTE_HIDDEN、FILE_ATTRIBUTE_READONLY、FILE_ATTRIBUTE_ARCHIVE 等選項。

最後,如果希望新檔案控制代碼模仿現有檔案控制代碼的屬性,則可以指定 hTemplateFile 成員。如果不使用,可以將其設定為 NULL。

ReadFile WriteFile

[編輯 | 編輯原始碼]

一旦檔案控制代碼開啟,理想情況下我們希望與指定的檔案互動。我們可以最直接地使用 ReadFile 和 WriteFile 函式來做到這一點。它們都採用類似的引數

BOOL ReadFile(
 HANDLE hFile,
 LPVOID lpBuffer,
 DWORD nNumberOfBytesToRead,
 LPDWORD lpNumberOfBytesRead,
 LPOVERLAPPED lpOverlapped);
BOOL WriteFile(
 HANDLE hFile,
 LPCVOID lpBuffer,
 DWORD nNumberOfBytesToWrite,
 LPDWORD lpNumberOfBytesWritten,
 LPOVERLAPPED lpOverlapped);

在這兩者中,hFile 引數是我們使用 CreateFile 獲取的檔案控制代碼。lpOverlapped 引數僅用於一種稱為“重疊 I/O 模式”的特殊 I/O 模式,我們將在後面討論。對於簡單 I/O,lpOverlapped 引數可以設定為 NULL。

在 ReadFile 中,lpBuffer 是指向用於接收資料的通用緩衝區的指標。此資料可能不是字元資料,因此我們不將其稱為 LPSTR 型別。“nNumberofBytesToRead”是要讀取的位元組數,“lpNumberOfBytesRead”是實際讀取的位元組數。如果 lpNumberOfBytesRead 為零,則檔案中不再有資料。

在 WriteFile 中,lpBuffer 引數指向要寫入檔案的資料。同樣,它並不一定是字元資料。nNumberOfBytesToWrite 是要寫入的最大位元組數,lpNumberOfBytesWritten 返回實際寫入檔案的位元組數。

CloseHandle

[編輯 | 編輯原始碼]

完成使用檔案控制代碼後,您應該使用 CloseHandle 函式關閉它。CloseHandle 只接受一個引數,即您要關閉的檔案控制代碼。如果您沒有關閉控制代碼,Windows 將在程式關閉時自動關閉控制代碼。但是,對於 Windows 來說,為您完成它是一個更昂貴的操作,並且會浪費您系統上的時間。在退出程式之前始終顯式關閉所有控制代碼是一個好主意。

無法關閉控制代碼稱為“控制代碼洩漏”,這是常見的記憶體洩漏形式,會導致您的程式以及整個系統丟失資源並執行速度變慢。控制代碼本身只佔用 32 位資訊,但核心在內部為每個控制代碼維護大量資料和儲存空間。無法關閉控制代碼意味著核心必須維護有關控制代碼的所有關聯資訊。在查詢有關當前控制代碼的資訊時,它還會花費核心更多時間和資源來檢查所有舊的未使用的控制代碼。

記憶體對映檔案

[編輯 | 編輯原始碼]

記憶體對映檔案提供了一種使用常規指標和陣列結構讀取和寫入檔案的機制。 您可以使用記憶體指標讀取檔案,而不是使用 ReadFile 讀取檔案。 系統透過將檔案讀入記憶體頁面,然後將更改寫入該頁面到物理磁碟來實現這一點。 在開始時將檔案讀入記憶體以及在對映完成後將檔案寫回,存在一定量的額外開銷。 但是,如果對檔案有很多訪問,從長遠來看它會方便得多。

重疊 I/O

[edit | edit source]

“重疊”I/O 是 Microsoft 用於描述非同步 I/O 的術語。 當您要執行 I/O 操作(無論是到檔案還是到外部裝置)時,您有兩個選擇

同步(非重疊)
您向系統請求 I/O,並等待 I/O 完成。 程式將停止執行,直到 I/O 完成。
非同步(重疊)
您向系統傳送請求,系統將與您的程式並行完成該請求。 您的程式可以繼續執行處理工作,系統將在您的請求完成時自動傳送通知。

同步 I/O 使用起來要容易得多,而且更直接。 在同步 I/O 中,事情是按順序發生的,當 I/O 函式返回時,您就知道事務已完成。 但是,I/O 通常比您程式中的任何其他操作都要慢,等待緩慢的檔案讀取或緩慢的通訊埠會浪費大量寶貴的時間。 此外,如果您的程式正在等待緩慢的 I/O 請求,圖形介面將看起來掛起且沒有響應,這會讓使用者感到厭煩。

程式設計師可以使用專用執行緒或執行緒池來執行同步 I/O 操作來避免這些延遲。 但是執行緒開銷很大,建立太多執行緒會導致系統資源耗盡。 非同步 I/O 避免了這種開銷,因此是高效能高負載伺服器應用程式的首選 API。

非同步 I/O 使用起來更復雜:它需要使用 OVERLAPPED 結構,以及建立一個當 I/O 完成時將由系統自動呼叫的處理程式函式。 但是,這種方法的效率優勢是顯而易見的。 您的程式可以請求多個事務,而不必等待任何事務完成,它還可以在系統執行所需任務時執行其他任務。 這意味著程式將對使用者看起來更具響應性,並且您可以將更多時間花在資料處理上,而將更少的時間花在等待資料上。

控制檯 API

[edit | edit source]

分配控制檯

[edit | edit source]

可以透過呼叫 AllocConsole 函式分配控制檯。 通常,如果我們正在建立一個“控制檯程序”(它包含 main 函式),我們不需要這樣做,因為它們已經附加到控制檯。 但是,我們可以為“GUI 程序”(其入口點為 WinMain)建立一個控制檯,並在新建立的控制檯上執行 I/O 操作。 應該注意的是,每個程序只能與一個控制檯關聯。 如果程序已經附加到控制檯,呼叫 AllocConsole 將返回 FALSE。

呼叫 AllocConsole 後,將出現 Windows 命令提示符視窗。

可以透過呼叫 FreeConsole 釋放控制檯。

獲取控制檯控制代碼

[edit | edit source]

建立控制檯後,將初始化標準輸出、標準輸入和標準錯誤控制代碼(我們稱之為“標準裝置”。 這些控制代碼對於任何控制檯 I/O 操作都是必不可少的。 可以透過呼叫 GetStdHandle 獲取它們,它接受一個指定要獲取的標準裝置控制代碼的引數。 引數可以是以下任何一個

STD_OUTPUT_HANDLE
指定標準輸出裝置,用於將資料輸出到控制檯。
STD_INPUT_HANDLE
指定標準輸入裝置,用於從控制檯讀取輸入。
STD_ERROR_HANDLE
指定標準錯誤裝置,主要用於輸出錯誤。

如果函式成功,返回值是指定標準裝置的控制代碼。 如果失敗,它將返回 INVALID_HANDLE_VALUE。

高階 I/O

[edit | edit source]

<stdio.h><iostream>(僅限 C++)標頭檔案包含通常用於高階控制檯 I/O 的函式。 高階 I/O 通常是“緩衝”的。 這些函式包括 printfscanffgets 等。 如果我們希望進行非緩衝 I/O,我們可以使用 freadfwrite 函式,並將 stdinstdoutstderr 分別傳遞給指定標準輸入、標準輸出和標準錯誤裝置的引數。 但是,通常不建議將高階 I/O 和低階 I/O 混合使用。

這些函式旨在可移植,並充當低階系統 I/O 函式的抽象。

低階 I/O

[edit | edit source]

可以透過使用一些 API 函式(如 WriteConsoleReadConsoleReadConsoleInput 等)來完成低階控制檯 I/O。

BOOL WriteConsole(
 HANDLE hConsoleOutput,
 const VOID *lpBuffer,
 DWORD dwNumberOfCharsToWrite,
 LPDWORD lpNumberOfCharsWritten,
 LPVOID lpReserved
);
BOOL ReadConsole(
 HANDLE hConsoleInput,
 LPVOID lpBuffer,
 DWORD dwNumberOfCharsToRead,
 LPDWORD lpNumberOfCharsRead,
 LPVOID pInputControl
);

請注意,所指的“字元”實際上是 TCHAR 的數量,當定義了 UNICODE 時,它可能是 2 個位元組寬。 它不是位元組數。

ReadConsoleInput 可用於讀取擊鍵,這無法透過 C 或 C++ 標準庫完成。 還有更多函式提供強大的 I/O 函式。

顏色和功能

[edit | edit source]

有很多令人興奮的 API 函式提供了對控制檯的額外控制。 其中一個最常用的函式是 SetConsoleTitle,它用於設定控制檯標題文字。 我們還可以使用 SetConsoleCursorPosition 函式來更改游標的位置。

我們可以透過 SetConsoleTextAttribute 以不同的前景色和背景色輸出文字。 我們還可以使用 SetConsoleScreenBufferSize 更改螢幕緩衝區的大小。

有關所有控制檯 API 的詳細文件,您可以參考 MSDN。

裝置 IO API

[edit | edit source]

程式和裝置驅動程式之間的互動可能很複雜。 但是,有一些標準裝置驅動程式可用於訪問標準埠和硬體。 在大多數情況下,與埠或硬體互動與開啟該裝置的控制代碼然後像檔案一樣讀取或寫入一樣簡單。 在大多數情況下,可以使用 CreateFile 函式開啟這些埠和裝置,方法是呼叫裝置的名稱而不是檔案的名稱。

獲取裝置控制代碼

[edit | edit source]

裝置 IO 函式

[edit | edit source]

裝置 IO 函式。

關於裝置 IO 的警告

[編輯 | 編輯原始碼]

完成埠

[編輯 | 編輯原始碼]

本頁是Windows 程式設計書籍中的一個頁面存根。您可以透過擴充套件它來提供幫助。


下一章

[編輯 | 編輯原始碼]
華夏公益教科書