第二課 使用Visual C++ 5.0
在這一章裏,我們將介紹Visual C++的集成開發環境Visual Studio及其組件,以及使用Visual C++基礎類庫MFC編程的一些基礎知識。
2.3 WIN32開發
Visual C++5.0是一個全32位的軟件開發工具,它完全支持32位的Win32平臺開發。Win32平臺包括32位的Windows操作系統和軟件開發系統Win32 API。所謂API(應用程序接口)指的是一組由操作系統提供的函數。Win32 API是Windows平臺上的一個32位的軟件開發系統,它使應用程序可以充分利用32位Windows操作系統的能力。使用Win32 API寫成的應用程序可以在Windows95或更高版本以及Windows NT上運行。
由於Microsoft在Windows 3.x及其Win16 API上取得巨大的成功,因此,在研製Win32 API時,首先考慮的就是保證Win32與Win16 API兼容,只有讓軟件開發者能將Win16代碼很容易移植到Win32 API上,纔有實際意義。Win32 API在語法上只作了極小的改動,API的命名與Windows 的Win16 API相同,語義也相同,消息序號也相同。事實上,完全可以保存獨立的源代碼,並選擇編譯成16位的Win16程序或32位的Win32程序。
其次,如其名所示,在設計Win32 API時考慮到了充分利用32位處理器的能力。隨着硬件的發展,內存和CPU價格的降低和性能的提高,32位CPU的486、Pentium已成爲主流。據有關數據顯示,目前在我國家用計算機用戶中,使用Pentium系列處理器的計算機已佔80%以上。如何充分利用當前32位(和64位)處理器的能力,並預見將來處理器的發展,就成爲Win32設計時考慮的重要因素之一。
再次,爲了擺脫操作系統對Intel處理器的依賴,使應用程序可以運行於各種處理器平臺上,Win32設計時增強了它的可移植性,提供了Microsoft Windows95和Windows NT之間的透明的移植能力。雖然Windows95只能運行於Intel平臺上,但是Win32還支持Windows NT,而Windows NT已經被移植到許多非Intel的處理器上,如Alpha、RISC硬件平臺等。
Win32可以應用於特定的操作系統,這種系統可以直接控制和處理PC硬件資源,而不必象Win16 API那樣依賴於MS-DOS系統服務。然而,Win32不是簡單的由Win16從16位到32位的升級,更重要的在於它支持:
- 高性能的搶先式多任務和多線程
- 連續的32位地址空間和先進的內存管理
- 對所有的可爲進程共享的對象,解決了它的安全性問題
- 內存映射文件
2.3.1 搶先式多任務和多線程
我們知道Windows是一個多任務操作系統,它提供了一次運行多個應用程序的能力。但是,Windows 3.x和Windows95在多任務的實現上有所不同。
Windows 3.x的多任務是一種由協作、軟件方式產生的有限的非搶先式的多任務。它是藉助於每個應用程序的消息循環這種軟件協議方式來實現多任務的。Windows 3.x管理所有的消息,並存放於系統的消息隊列中。操作系統判斷消息應歸哪一個窗口去處理,再將消息發送給該窗口。每個應用程序窗口處於等待消息狀態,直到有消息來,然後進行處理,處理完畢將控制權交給操作系統。在對消息進行處理時,對於用戶用鍵盤或者鼠標輸入的任何命令,Win16都不會理睬。比如,我們用WORD載入一個文件時,其他程序都得等待文件I/O操作完成才能獲得響應。而且,一個應用程序切換到另一個應用程序時,需要較長的等待時間。各應用程序在取得消息、處理消息時是平等的,無優先級的,系統無法設置應用程序的優先級和時間片的大小。
Windows95的多任務是一種搶先式多任務。比如,我們在用資源管理器複製一個文件的同時,還可以啓動另外一個應用程序,如紙牌遊戲,而且隨時都可以切換回資源管理器,察看文件複製進度,系統始終保持較好的響應和靈活性。Windows95的搶先式多任務機制不是用Windows 3.x下的軟件調度來實現的。要了解搶先式多任務,我們需要首先了解一下進程和線程的概念。調入內存準備執行的應用程序叫做進程(process)。每個進程至少有一條線程,叫做主線程(primary thread)。一個進程包含代碼、數據和其他屬於應用程序的資源。一條線程包含一組指令,相關的CPU寄存器值和一個堆棧。
在搶先式多任務操作系統中,系統在所有運行的所有進程之間對CPU時間進行共享,從而保證每個進程都能頻繁的訪問處理器,並且實現指令的連續執行。這樣,每個Win32進程都需要分配一個優先級,系統調度程序利用這種優先級來決定哪一時刻該運行哪一個進程。具有高優先級的進程(嚴格的說應當是線程)就是當前運行的哪一個。更高優先級的線程可以中斷當前進程的執行。同一優先級的線程通過時間片來調度。一個線程處於以下三種狀態之一:正在執行,掛起,準備運行。在單處理器環境下(如Windows 95),同一時刻只能運行一個線程。有關多線程,我們還將在後面的章節裏作專門介紹。
爲了在Win32中支持多線程進程結構,Win32在原來Win16基礎上增加了:
對進程以及線程創建、操縱的支持
對一個進程內線程之間的同步和同步對象的支持
一個統一的共享機制。
2.3.2 連續的地址空間和先進的內存管理
對於各種操作系統和平臺來說,內存管理都是一個非常重要的問題。在Windows3.1下,有兩種形式的內存管理函數調用:局部的和全局的。全局內存管理函數從物理內存中分配一段,然後返回一個句柄值。該句柄可以轉換爲一個GlobalLock函數所使用的遠指針。基本處理過程如下:
- 申請一塊可移動的內存塊
- 鎖定該內存塊。因爲Windows引入了虛擬內存管理,可以把內存塊移動到硬盤交換文件中,所以在使用內存塊之前,必須將它鎖定在真正的內存RAM之中,也就是告訴操作系統,現在這塊內存暫時由應用程序來管理。
- 對該內存塊進行各種操作:如複製數據到內存塊。
- 解鎖內存,應用程序將對該內存的控制交與Windows。
下面給出一個程序片段,來說明內存管理函數的用法。
HGLOBAL memHandle;//內存句柄
char far* lpMem;//假設長度爲memLen
memHandle=GlobalAlloc(GHND,memLen+1);//申請內存塊,此處未做返回結果檢查,
//事實上,申請內存有時會失敗
memcpy(lpMem,string,textLen);//拷貝數據,其中string爲一字符串變量,textLen是這個
//字符串的長度
GlobalUnlock(memHandle);//解鎖內存
...
GlobalFree((HGLOBAL) memHandle);//釋放內存
全局內存對所有的應用程序都是可見的,不管是顯式的還是隱式的請求。因爲Windows 3.x的實現方式就是所有的進程在同一地址空間中運行。局部內存管理則是從64KB的段內分配對象並返回所分配內存的16位偏移量。
在Win32下,局部和全局內存管理函數基本相同,仍然可以使用可移動和可丟棄選項。但是它引入了連續(flat)的32位內存管理概念。
在Win32中,每個進程都有其特有的32位虛擬地址空間,該空間最大可達4GB。如圖所示,低端內存的2GB是用戶可用的,高端內存的2GB爲內核(Kernel)保留。其中,最高的1GB用於VxD、內存管理和文件系統。下面的1GB用於共享的Win32 DLL、內存映射文件和共享內存區域。進程所使用的虛擬地址不代表一個對象在內存的實際的物理位置(事實上,我們大部分的PC還沒有配置4GB內存)。操作系統爲每個進程維護一個映射表,根據該表將虛擬地址映射到真正的物理位置處(RAM或者交換頁文件中)。
圖2.7 Windows95的內存映射
在Win32下局部內存對象有一個32位句柄而不是Windows 3.x下的16位句柄,而且這個句柄是一個實際指針而不是一個相對於段的偏移量。
Win32和16位Windows一個重要區別是:在Win32下,所有的進程都有自己獨立的地址空間(在進程內部的線程仍然共享進程的內存變量),全局內存不再對所有的Windows應用程序都可見。由於每個應用程序都有自己的地址空間,一個進程分配的內存在該進程的地址之外就不再可見。DDE會話中使用的內存對接收者進程來說是透明的。這樣,進程的安全性就得到大大提高,程序更加強壯。一個進程崩潰一般不會影響另外一個進程的執行。但是,這也給多個應用程序共享內存帶來了困難。在許多情況下,需要在多個應用程序之間進行通訊和數據交換,這時,該怎麼辦呢?Win32引入了內存映射文件,很好的解決了這個問題。
2.3.3 內存映射文件
內存映射文件是由一個文件到一塊內存的映射。Win32提供了允許應用程序把文件映射到一個進程的函數(CreateFileMapping)。這樣,文件內的數據就可以用內存讀/寫指令來訪問,而不是用ReadFile和WriteFile這樣的I/O系統函數,從而提高了文件存取速度。
這種函數最適用於需要讀取文件並且對文件內包含的信息做語法分析的應用程序,如對輸入文件進行語法分析的彩色語法編輯器,編譯器等。把文件映射後進行讀和分析,能讓應用程序使用內存操作來操縱文件,而不必在文件裏來回地讀、寫、移動文件指針。
有些操作,如放棄“讀”一個字符,在以前是相當複雜的,用戶需要處理緩衝區的刷新問題。在引入了映射文件之後,就簡單的多了。應用程序要做的只是使指針減少一個值。
映射文件的另一個重要應用就是用來支持永久命名的共享內存。要在兩個應用程序之間共享內存,可以在一個應用程序中創建一個文件並映射之,然後另一個應用程序可以通過打開和映射此文件把它作爲共享的內存來使用。
2.3.4 Win32s:Windows 3.x對Win32 API的支持
我們經常會遇到Win32s這個詞,它與Win32是有區別的。Win32s的s的含義是指子集(subset)。它指的是,在一個Win32程序中移入一些DLLs和一個VxD,使它運行於配置80386以上處理器的Windows 3.x系統之上,並且以一種增強模式運行(但有一定限制)。運行在Windows 3.x/Win32s系統上的Win32程序支持32位指針和32位寄存器,只需要在系統調用之前稍作形式替換。如果程序中使用大的數據結構或很多的計算時,Win32s性能明顯優於16位Windows版本,根據Microsoft的測試,性能可以提高兩倍左右;如果程序只是大量的調用Windows API,則16位版本的性能可能會強於32位版本,因爲Win32s會對每一次API調用作一個從16位到32位的轉換。
Win32s子集同Win32相比,不支持:多線程,高級圖形API,異步文件I/O,Unicode和安全性;而且它是運行於16位的Windows系統上的。但是同Win16相比,有它的優越之處,目前在16位Windows程序開發方面有相當的潛力。
Visual C++4.1及以前版本支持Win32s,但Visual C++5.0不再支持Win32s。
2.3.5 Win32編程基礎
Win32數據類型
這裏的數據類型指的是一些關鍵字,這些關鍵字定義了Win32中的函數中的有關參數和返回值的大小和意義。Win32常用的數據類型有:
數據類型 | 描述 |
HANDLE | 定義一個32位無符號的整數,用作句柄 |
HINSTANCE | 定義一個32位的無符號整數,用作實例句柄 |
HWND | 定義一個32位的無符號整數,用作窗口句柄 |
HDC | 一個設備描述背景的句柄 |
LONG | 說明一個32位帶符號整數 |
LPSTR | 定義一個線性的32位字符串指針 |
UINT | 定義一個新的Win32數據類型,它會把一個參數強制轉換成Windows3.x應用中的16位值或Win32應用中的32位 |
WCHAR | 說明一個16位的UNICODE字符,用來表示世界上所有已知的書寫語言的符號 |
這裏需要解釋一下的是句柄。句柄是Windows編程的一個關鍵性的概念,編寫Windows應用程序總是要和各種句柄打交道。所謂句柄,就是一個唯一的數,用以標識許多不同的對象類型,如窗口、菜單、內存、畫筆、畫刷、電話線路等。在Win32裏,句柄是指向一個“無類型對象”(void*)的指針,也就是一個4字節長的數據。無論它的本質是什麼,句柄並不是一個真正意義上的指針。從構造上看,句柄是一個指針,儘管它沒有指向用於存儲某個對象的內存位置。事實上,句柄指向一個包含了對該對象進行的引用的位置。句柄的聲明是這樣的:
typedef void *HANDLE
由於Windows是一個多任務操作系統,它可以同時運行多個程序或一個程序的多個副本。這些運行的程序稱爲一個實例。爲了對同一程序的多個副本進行管理,Windows引入了實例句柄。Windows爲每個應用程序建立一張表,實例句柄就好象是這張表的一個索引。
Windows不僅使用句柄來管理實例,也用它來管理窗口、位圖、字體、元文件、圖標等系統資源。
標識符命名
在編程時,變量、函數的命名是一個極其重要的問題。好的命名方法使變量易於記憶且程序可讀性大大提高。Microsoft採用匈牙利命名法來命名Windows API函數和變量。匈牙利命名法是由Microsoft的著名開發人員、Excel的主要設計者查爾斯·西蒙尼在他的博士論文中提出來的,由於西蒙尼的國籍是匈牙利,所以這種命名法叫匈牙利命名法。
匈牙利命名法爲C標識符的命名定義了一種非常標準化的方式,這種命名方式是以兩條規則爲基礎的:
1.標識符的名字以一個或者多個小寫字母開頭,用這些字母來指定數據類型。下表列出了常用的數據類型的標準前綴:
在Windows裏定義數據類型的一些標準前綴
前綴 |
數據類型 |
c | 字符(char) |
s | 短整數(short) |
cb | 用於定義對象(一般爲一個結構)尺寸的整數 |
n | 整數(integer) |
sz | 以’/0’結尾的字符串 |
b | 字節 |
i | int(整數) |
x | 短整數(座標x) |
y | 短整數(座標y) |
f | BOOL |
w | 字(WORD,無符號短整數) |
l | 長整數(long) |
h | HANDLE(無符號int) |
m_ | 類成員變量 |
fn | 函數(function) |
dw | 雙字(DWORD,無符號長整數) |
2.在標識符內,前綴以後就是一個或者多個第一個字母大寫的單詞,這些單詞清楚地指出了源代碼內那個對象的用途。比如,m_szStudentName表示一個學生名字的類成員變量,數據類型是字符串型。
從16位的Win16 API遷移到Win32 API注意點
1.數據類型字長的變化:
我們編寫一個小程序來說明Win32下的常見數據類型的字長:
#include<windows.h>
#include<stdio.h>
void main(void)
{
printf("sizeof(int) is %d/n",sizeof(int));
printf("sizeof(BYTE) is %d/n",sizeof(BYTE));
printf("sizeof(WORD) is %d/n",sizeof(WORD));
printf("sizeof(DWORD) is %d/n",sizeof(DWORD));
printf("sizeof(LONG) is %d/n",sizeof(LONG));
printf("sizeof(PVOID) is %d/n",sizeof(PVOID));
printf("sizeof(LPVOID) is %d/n",sizeof(LPVOID));
}
使用Visual C++編譯運行該程序,輸出結果如下:
sizeof(int) is 4
sizeof(BYTE) is 1
sizeof(WORD) is 2
sizeof(DWORD) is 4
sizeof(LONG) is 4
sizeof(PVOID) is 4
sizeof(LPVOID) is 4
從上面的輸出結果我們看到:整數類型字長已經同長整數相同,PVOID近指針和LPVOID遠指針長度也相同。在編程過程中,我們要注意這些變化,凡是設計字長的問題最好還是採用可以移植的sizeof操作符來做。
2.內存模式變化:
在Win32平臺下,不再有微模式、緊湊模式、中模式、大模式、巨模式、自定義內存模式之分,也不再有64KB代碼段和數據段的限制。只有一種內存模式,Win32下的地址和代碼均在線性尋址的2GB的32位內存空間中。當然,編程時還是要考慮到實際內存限制的。
3.類型修飾符:
在Win32下,不再有遠指針、近指針、巨型指針之分,三種指針類型完全相同。32位的編譯器會忽略所有的_near、_far、_huge關鍵字並一視同仁來處理。在Win32中,象LPSTR和PSTR這種類型是等價的。
4.函數的變化:
Win32API設計時儘可能保證與Win16API兼容,但是仍然對一些函數作了修改。比如在Win16下的MoveTo在Win32下爲MoveToEx。如果在編譯程序時某個API函數沒找到,試着在這個函數名後面加上Ex,Ex表示它是Win16的擴展。