第三章 WIN32的時空觀
我的老父親看著地上玩玩具的小孫子,然後對我說:“這孩子和小時的你一樣,喜歡把東西拆開,看過究竟才罷手”。想想我小時侯,經常將玩具車、小鬧鐘、音樂盒,等等,拆得一塌糊塗,常常被母親訓斥。
我第一次理解電腦的基本原理,與我拆過的音樂盒有關。那是在念高中時的一本漫畫書上,一位白鬍子老頭在講解智能機的理論,一位留八字鬍的叔叔在說電腦和音樂盒。他們說,電腦的中央處理器就是音樂盒中用來發音的那一排音樂簧片,電腦程式就是音樂盒中那個小圓筒上密布的凸點,小圓筒的轉動相當於中央處理器的指令指標的自然移動,而小圓筒上代表音樂的凸點控制音樂簧片震動發音相當於中央處理器執行程式的指令。音樂盒發出美妙的旋律,是按工匠早已刻在小圓筒上的音樂譜演奏的,電腦完成複雜的處理,是根據程式員預先編製好的程式實現的。上大學之後,我才知道那個白鬍子老頭就是科學巨匠圖靈,他的有限自動機理論推動了整個資訊革命的發展,而那個留八字鬍的叔叔就是電腦之父馮.諾依曼,馮氏電腦體繫結構至今仍然是電腦的主要體系機構。音樂盒沒白拆,母親可以寬心。
有深入淺出的理解,才能有高深而又簡潔的創造。 這一章我們將討論Windows的32位作業系統中與我們編程有關的基本概念,建立WIN32中正確的時空觀。希望閱讀完本章之後,我們能更加深入地理解程式、進程和線程,理解執行檔案、動態串連庫和運行包的原理,看清全域資料、局部資料和參數在記憶體中的真相。
第一節 理解進程
由於曆史的原因,Windows是起源於DOS。而在DOS時代,我們一直只有程式的概念,而沒有進程的概念。那時侯,只有作業系統的正規軍,如UNIX和VMS等等,才有進程的概念,而且多進程就意味著小型機、終端和多使用者,也意味著金錢。我絕大多數的時間只能使用相對廉價的微機和DOS系統,只是在學作業系統這門課程時才開始接觸進程和小型機。
在Windows 3.X之後,Microsoft才在圖形介面的作業系統站住腳跟,而我也是在這時開始正式面對多任務和進程的概念。以前在DOS下,同一時間只能執行一個程式,而在Windows下同一時間可執行多個程式,這就是多任務。在DOS下運行一個程式的同時,不能執行相同的程式,而在Windows下,同一程式可以同時有兩個以上的副本在運行,每一個啟動並執行程式副本就是一個進程。更確切地說,任何程式的一次運行就產生一個任務,而每個任務就是一個進程。
當將程式和進程放到一起理解時,可以認為程式一詞說的是靜態東西,一個典型的程式是由一個EXE檔案或一個EXE檔案加上若干DLL檔案組成的靜態代碼和資料。而進程是程式的一次運行,是在記憶體中動態啟動並執行代碼和動態變化的資料。當靜態程式要求運行時,作業系統將為本次運行提供一定的記憶體空間,把靜態程式碼和資料調入這些記憶體空間,將程式的代碼和資料進行重定位映射之後,就在該空間內執行程式,這樣就產生了動態進程。
同一個程式同時運行著的兩個副本,意味著在系統記憶體中有兩個進程空間,只不過它們的程式功能是一樣的,但處於不同的動態變化的狀態之中。
從進程啟動並執行時間上來說,各進程是同時執行的,專業術語稱為並存執行或並發執行。但這主要是作業系統給我們的表面感覺,實際上各進程是分時執行的,也就是各進程輪流佔用CPU的時間來執行進程的程式指令。對於一個CPU來說,同一時間只有一個進程的指令在執行。作業系統是調度進程啟動並執行幕後操縱者,它不斷儲存和切換各進程在CPU中執行的目前狀態,使得每一個被調度的進程都認為自己是完整和連續地運行著。由於進程分時調度的速度非常快,所以給我們的感覺就是進程都是同時啟動並執行。其實,真正意義上的同時運行只有在多CPU的硬體環境中才有。稍後在講述線程一節時,我們將發現,真正推動進程運轉的是線程,進程更重要的是提供了進程空間。
從進程佔據的空間上來說,各進程空間是相對獨立的,每一個進程在自己獨立的空間中運行。一個程式既包括代碼空間又包括資料空間,代碼和資料都要佔據進程空間。Windows為每一進程所需的資料空間分配實際的記憶體,而對代碼空間一般都採用共用手段,將一個程式的一份代碼映射給該程式的多個進程。這意味著,如果一個程式有100K的代碼並需要100K的資料空間,也就是總共需要200K的進程空間,則第一次運行程式時作業系統將分配200K的進程空間,而運行程式的第二個進程時,作業系統只分配100K的資料空間,而代碼空間則共用前一個進程的空間。
上面所說的是Windows作業系統中進程的基本時空觀,其實Windows的16位和32位作業系統在進程的時空觀上有很大的差異。
從時間上來說,16位的Windows作業系統,如Windows 3.x等,進程管理是非常簡單的,它實際上只是一個多任務管理作業系統。而且,作業系統對任務的調度是被動的,如果一個任務不自己放棄對訊息的處理,作業系統就必須等待。由於16位Windows系統在管理進程方面的缺陷,一個進程運行時,完全佔有著CPU的資源。在那個年代,為了16位Windows可以有機會調度別的任務,微軟公司大力讚揚開發Windows應用程式的開發人員是心胸寬闊的程式員,以使得他們樂意多編寫幾行恩賜給作業系統的代碼。相反,WIN32的作業系統,如Windows 95和NT等,才是具備了真正的多進程和多任務作業系統的能力。WIN32中的進程完全由作業系統調度,一旦進程啟動並執行時間片結束,不管進程是否還在處理資料,作業系統將主動切換到下一進程。嚴格地說,16位的Windows作業系統不能算是完整的作業系統,而32位的WIN32作業系統才是真正意義上的作業系統。當然,微軟公司不會說WIN32彌補了16位Windows的缺陷,而是宣稱WIN32實現了一種稱為“搶佔式多任務”的先進技術,這是商業手段。
從空間上看,16位的Windows作業系統中的進程空間雖然相對獨立,但進程之間可已很容易地互相訪問對方的資料空間。因為,這些進程實際是在相同的物理空間中的不同的資料區段而已,而且不當的地址操作很容易造成錯誤的空間讀寫,並使作業系統崩潰。然而,在WIN32作業系統中,各進程空間完全是獨立的。WIN32為每一個進程提供一個可達4G的虛擬,並且是連續的地址空間。所謂連續的地址空間,是指每一個進程都擁有從$00000000到$FFFFFFFF的地址空間,而不是向16位Windows的分段式空間。在WIN32中,你完全不必擔心自己的讀寫操作會無意地影響到其他進程空間中的資料,也不用擔心別的進程會來騷擾你的工作。同時,WIN32為你的進程提供的連續的4G虛擬空間,是作業系統在硬體的支援下將實體記憶體映射給你的,你雖然擁有如此廣闊的虛擬空間,但系統決不會浪費一個位元組的實體記憶體。
第二節 進程空間
在我們用DELPHI編寫WIN32的應用程式時,很少去關心進程在運行時的內部世界。因為WIN32為我們的進程提供了4G的連續虛擬進程空間,可能目前世界上最龐大的應用程式也只用到了其中的部分空間。似乎進程空間是無限的,但4G的進程空間是虛擬,而你機器的實際記憶體可能與此相差甚遠。雖然,進程擁有如此廣闊的空間,但有些複雜演算法的程式還是會因為堆疊溢位而無法運行,特別是含有大量遞迴演算法的程式。
因此,深入地認識和瞭解這4G的進程空間的結構,以及它與實體記憶體的關係等等,將有助於我們更清楚地認識WIN32的時空世界,從而可在實際的開發工作中運用正確的世界觀和方法論解決各種難題。
下面,我們將通過簡單的實驗,來瞭解WIN32的進程空間的內部世界。這可能需要一些對CUP寄存器和組合語言的知識,但我盡量用簡單的語言來說明。
當啟動DELPHI時,將自動產生一個Project1的項目,我們就拿它開刀。在Project1.dpr原程式的任意位置設一斷點,比如,就在begin一句處設一斷點。然後運行程式,當程式運行到斷點時會自動停下來。這時,我們就可以開啟調試工具中的CPU視窗來觀察進程空間的內部結構了。 當前的指令指標寄存器EIP是停在$0043E4B8,從程式指令所在地址的最高兩位16進位數都是零,可以看出當前的程式處在4G進程空間相當底端的地址位置,其佔據$00000000到$FFFFFFFF的相當少的地址空間。
在CPU視窗中的指令框中,你可以向上查看進程空間中的內容。當查看小於$00400000的空間內容時,你會發現小於$00400000的內容出現一串串的問號“????”,那是因為該地址空間還未映射到實際物理空間的緣故。如果在這時,你查看一下全域變數HInstance的16進位值就會發現它也是$00400000。雖然HInstance反映的是進程執行個體的控制代碼,其實,它就是程式被載入到記憶體中的起始地址值,在16位Windows中也是如此。因此,我們可以認為進程的程式是從$00400000開始載入的,也就是從4G虛擬空間中的4M以後的空間開始是程式載入的空間。
從$00400000往後,到$0044D000之前,主要是程式碼和全域資料的地址空間。在CPU視窗中的堆棧框中,可以查看到當前堆棧的地址。同樣,你會發現當前堆棧的地址空間是從$0067B000到$00680000的,長度為$5000。其實,進程最小的堆棧空間大小就是$5000,它是根據編譯DELPHI程式時在Project中Linker頁中設定的Min stack size值,加上$1000而得到的。堆棧是由高端地址向底端增長的,當程式啟動並執行堆棧不夠時,系統將自動向地端地址方向增加堆棧空間的大小,這一過程將把更多的實際記憶體映射到進程空間。可在編譯DELPHI程式時,通過設定Project中Linker頁中Max stack size的值,控制可增加的最大堆棧空間。特別是在含有深層次的子程式調用關係或運用遞迴演算法的程式中,一定要合理地設定Max stack size的值。因為,調用子程式是需要耗用堆棧空間,而堆棧耗盡之後,系統就會拋出“Stack overflow”的錯誤。
似乎,從堆棧空間之後的進程空間就應該是自由的空間了吧。其實不然,WIN32的有關資料說,$80000000之後的2G空間是系統使用的空間。看來,進程能夠真正擁有的只有2G空間。其實,進程能真正擁有的空間連2G都不夠,因為從$00000000到$00400000的這4M空間也是禁區。
但不管怎樣,我們的進程可以使用的地址還是非常廣闊的。特別是堆棧空間之後到$80000000之間,是進程空間的主戰場。進程從系統分配的記憶體空間將被映射到這塊空間,進程載入的動態串連庫將被映射到這塊空間,建立線程的線程堆棧空間也將映射到這塊空間,幾乎所有涉及分配記憶體的操作都將映射到這塊空間。請注意,這裡所說的映射,意味著實際記憶體與這塊虛擬空間的對應,沒有映射為實際記憶體的進程空間是無法使用的,就象調試時CPU視窗指令框中的那一串串的“????”。
第三節 EXE和DLL
我們已經對進程和進程空間有了一定的瞭解,下面我們將要討論執行檔案EXE和動態串連庫DLL的區別,它們與進程空間的關係。
典型的Windows應用程式一般都由一個EXE檔案和若干個DLL檔案組成,Windows作業系統本身就是這種結構,這些都是大家熟知的內容。但是,你真正理解了EXE和DLL嗎?如果,你確信自己已經熟知EXE和DLL的內涵,請跳過本節的內容。如果,你一直搞不清EXE和DLL,那就請仔細聽我道來。
一個正確的EXE檔案,是可以直接啟動並執行程式。Windows作業系統會為該程式建立一個進程空間,程式是在進程空間內啟動並執行。進程空間是應用程式啟動並執行基本環境,沒有進程空間就根本無法運行程式。
在EXE檔案中,程式的資料參考關聯性和程序呼叫關係是用相對位址表示的,當程式載入到進程空間中的絕對位址上時,作業系統需要將對相對位址的引用和調用關係調整為對絕對位址的引用和調用關係,這一過程稱為“重定位”。需要重定位的地方稱為重定位項,它是儲存在EXE檔案的表頭資訊當中。
程式的運行是需要堆棧的,因為堆棧是程序呼叫必須具備的基本設施,也是過程中局部資料和過程參數生死存亡的地方。作業系統會根據記錄在EXE檔案頭中的堆棧大小值,確定堆棧空間的地址位置和大小,並首先映射最小堆棧空間的那部分記憶體。
如果,一個EXE程式與某些DLL有固定的參考關聯性,作業系統將把相關的DLL程式調入當前進程空間。EXE和DLL以及DLL之間的引用或調用關係是用引入表和引出表來描述的,它們也儲存在EXE或DLL的檔案的表頭資訊當中。作業系統根據引入表和引出表資訊,將程式模組間的引用和調用串連起來,這一過程稱為“動態串連”。
一個DLL檔案只是程式的一部分,它並不是完整的可執行程式。因此,DLL不能單獨地運行,Windows也不會為DLL模組建立進程空間。一個DLL模組必須被載入到一個EXE程式的進程空間中,才能發揮其程式功能。
一個DLL檔案也含有重定位資訊,作業系統將DLL載入到進程空間中時,同樣需要對DLL模組進行重定位。如果,被載入的DLL與其他DLL檔案又有固定的參考關聯性,則載入該DLL模組時將同時載入其引用的DLL模組,並完成行動態串連過程。
一個DLL檔案中記錄的堆棧大小總是為零,因為DLL只是為進程服務的,調用DLL模組時,使用的是進程的堆棧。對於單線程的應用程式,進程只有一個堆棧,就是進程的主線程使用的堆棧。對於多線程的應用程式,每個正在啟動並執行線程都有自己的堆棧。在多線程的情況下,DLL模組在那個線程中被調用,則DLL就使用該線程的堆棧。但所有這一切都還是在同一進程空間中進行的。
如果,你使用Windows API的LoadLibrary函數動態地載入一個DLL檔案,其返回的模組控制代碼的值,就是指向該DLL載入到進程空間中的地址值。如果,你反覆調用LoadLibrary載入同一個DLL,你會發現他們返回的模組控制代碼值是相同的,Windows只是增加該DLL的引用計數。其實,在不同Windows進程之間,DLL是共用的,只是,在不同的進程空間有不同的映射地址。
一個DLL模組的副檔名不一定都是*.DLL。象DELPHI的BPL包檔案,ActiveX對象的OCX檔案,以及裝置驅動程式的DRV和VXD檔案都是DLL。當然,一個EXE模組的副檔名也不一定要是*.EXE,只是,非*.EXE的可執行檔是不能通過雙擊滑鼠來執行的,需要由編寫程式語句來調入並運行。
DLL只有在進程空間內才能運行,這是一個基本的原則,請你一定牢記。
但有時,在Windows中的一個DLL本身已經是一個完整的程式,就缺少啟動並執行進程空間。這時,Windows就用Rundll32命令來運行該DLL程式。Rundll32是一個簡單的EXE檔案,就在WINDOWS目錄中,它僅僅為DLL程式提供了可啟動並執行進程空間。
此外,在多層體繫結構應用程式開發中,存在於DLL模組中的商業對象,也是需要啟動並執行進程空間的。如果,你的用戶端應用程式和DLL應用伺服器在同一台機器中,則DLL應用伺服器中的商業對象是直接在你用戶端程式的進程空間中啟動並執行,這就是所說的In-Process模式。而對於Out-of-Process的對象調用模式,一般要求應用伺服器是一個可執行檔EXE檔案。
當然,如果應用伺服器和用戶端程式是在不同的機器中,應用伺服器肯定要具備一個進程空間才能工作。例如,你無法通過直接的DCOM串連遠端機器上的DLL應用伺服器,這是因為DLL沒有可啟動並執行進程空間的緣故。不過,你可以通過Socket串連遠端機器上的DLL應用伺服器,這是因為DLL實際是在SCKTSRVR.EXE的進程空間中啟動並執行,而SCKTSRVR.EXE是監聽Socket已連線的服務程式。
在MTS模式的分布式多層應用程式開發中,所有的商業對象必須存在於DLL檔案的應用伺服器中。這些DLL中的商業對象共同存在於MTS的進程空間中,因此,MTS才可在自己的進程空間中調度和管理這些商業對象,以完成對象的事務控制、對象緩衝和串連共用等強大功能。
第四節 資料和代碼在哪裡
資料和代碼在哪裡?
俗話說,人過了三十歲,就不會再去思考“人為什麼要活著?”這些問題。可我編了十幾年的程式還是喜歡去想 “我為什麼要編程式?”、“程式為什麼要運行?”、“運行著的是程式還是我的思想?”......儘管我已經三十齣頭。這樣的思考是永遠沒有結果的,那隻是程式生涯中最浪費生命的苦行。這樣的苦行也是有所感悟的,常常讓我的靈魂更加融入程式的世界。程式有了我的靈魂,啟動並執行個性也似乎帶有我的風格。受益的有我,也有我的程式。
今天要和大家侃侃程式的資料和代碼的問題。這個話題還是離不開進程空間的概念,因為程式的所有代碼和資料都存在於進程空間之中。那麼,資料和代碼到底在進程空間的那些地方呢?
先來看看進程空間中都有些什麼地區。
一個進程空間,與我們編程有關的地區大致有這麼四種:待用資料區、動態資料區、堆棧區和代碼區。進程空間中存在若干塊這樣的記憶體地區,它們隨著程式的運行而動態變化著。一會兒有新的地區產生,一會兒又有些地區消失。如果把進程空間想象成大海,這些記憶體地區就是大海中的島嶼,隨著潮起潮落,島嶼若隱若現。每一個島嶼都有作業系統映射的實際記憶體作為依託。沒有映射實際記憶體的空白地區就是深不見底的海面,程式訪問這些空白地區是要被淹死的。
這四種記憶體地區各有各的用處。待用資料地區就是你定義的全域變數、常量、線程變數等生存的地方。動態資料區就是你動態分配的資料空間和動態建立的對象生存的地方。堆棧區既為子程式調用提供儲存返回地址的空間,又為局部變數、參數變數和傳回值提供臨時空間。代碼區是儲存程式指令的地區,CPU是從這裡提取指令來執行程式的。
這些記憶體地區的產生和消失與程式模組的載入和卸出,以及線程的建立與消亡有關。
我們在這裡說所的模組是指Windows應用程式的物理檔案模組,即Windows中HMODULE控制代碼所表示的模組概念。典型地,EXE檔案是一個模組檔案,各種DLL檔案也是一個模組檔案。一個應用程式一般由一個EXE檔案和若干個類型的DLL檔案組成。這種檔案在Windows中稱為PE檔案(Portable Executable File)。它含有一個PE資訊頭,其中有載入程式模組需要的重要訊息。
當一個物理檔案模組被載入到進程空間中時,Windows作業系統將根據該模組PE頭的資訊安排該模組的資料和代碼在進程空間中的位置和大小。這樣的載入包括EXE程式的執行載入,也包括啟動EXE時及時載入DLL,以及程式隨後動態載入DLL的過程。
模組載入到進程空間,將產生該模組需要的若干待用資料地區,也將產生該模組的若干指令代碼地區。若模組在運行過程中動態分配和釋放記憶體或建立和消滅對象,又將相應地產生和釋放動態資料地區。
你可以認為模組的代碼區是固定不變的,因為一般代碼在執行過程中不會變化(除非你編寫的是帶變異功能的……),這不會影響對程式執行過程的理解。但模組的代碼區也可能是經常變化的,這是Windows在背後搞的鬼。
想象將一個有幾兆或幾十兆代碼的程式模組載入到進程空間中,程式的啟動速度會很慢嗎?這會在進程空間中產生巨大的代碼地區嗎?有可能,但Windows會盡量不讓這種情況發生。它會在CPU要用到某塊代碼區的指令時,將該代碼區從磁碟檔案中調入進程空間並執行。同時,如果發現有些代碼塊長時間沒有被執行到,它又會釋放這些代碼區所佔據的記憶體空間。當這些被釋放的代碼再次被CPU用到時,代碼將被再次載入(當然載入的位置可能有所不同)。所以,即使是載入和運行一個巨大的模組檔案,速度也不會明顯降低,空間也不會明顯增大。
再想象一下,如果一個模組檔案被載入之後將該檔案拿走,Windows還能從消失的檔案中正確載入CPU要用到的代碼嗎。當然不行!因此,Windows載入模組檔案時就強制將該檔案鎖定,你是沒辦法修改和刪除的。除非載入的是磁碟片上的模組,而你偏要強行將磁碟片抽走。
同樣,線程的建立和釋放也會引起記憶體地區的變化。當線程建立時,Windows系統會在進程空間中為這個線程開闢兩塊記憶體地區。一塊是相對於該線程的全域待用資料地區,一塊是線程運行所需的堆棧地區。其中,堆棧區是線程運行必須的基本設施,和線程密切相關不可分割。一個線程一定會有一個堆棧,而一個堆棧一定對應一個線程。一個線程釋放之後,其相關的待用資料區和堆棧也就消失。
模組的待用資料區一般都是你定義的全域變數、常量等所在的地方。這些資料元素的訪問地址都是相對於模組而言的。也就是說,這些資料元素標識方法和結構關係都是相對於該模組的程式的。模組外的其他模組程式是不能直接標識和解釋本模組中定義的這些資料元素的,除非將這些資料元素的地址從模組內傳遞給其他模組的程式。例如,我們看看下面的兩個模組程式:
program ExeModule;
function TheVariable: Pointer; external 'DllModule.DLL';
begin
// aVariable := 56789; //無法直接標識和訪問另一模組中的資料元素。
Integer(TheVariable^) := 56789; //只能通過從另一模組擷取地址間接訪問其資料元素。
end.
這個程式編譯後將產生ExeModule.EXE模組檔案。當然它啟動時需要DllModule.DLL。
library DllModule;
var
aVariable : Integer;
function TheVariable: Pointer;
begin
result:=@aVariable;
end;
exports TheVariable;
begin
aVariable := 12345;
end.
第二個程式由於指明它是library程式,所以編譯後將產生DllModule.DLL模組檔案。雖然在DllModule模組中定義了一個全域變數aVariable,但ExeModule模組的程式卻無法直接標識和找到該變數。只能通過調用DllModule模組的TheVariable函數擷取aVariable的地址指標之後,間接訪問aVariable變數。
有朋友說,如果將aVariable放到一個獨立的PASCAL單元檔案中,然後在兩個模組的主程式中都uses這個單元,不就可以互相訪問了嗎?我們就來看看下面這些程式檔案。
這是VarUnit.pas單元檔案:
unit VarUnit;
interface
var
aVariable : Integer;
implementation
end.
這是ExeModule.dpr檔案,它將產生ExeModule.EXE檔案:
program ExeModule;
uses
VarUnit;
begin
aVariable := 56789;
end.
這是DllModule.dpr檔案,它將產生DllModule.DLL檔案:
library DllModule;
uses
VarUnit;
begin
aVariable := 12345;
end.
這兩個模組都引用了VarUnit單元。假如這兩個模組都在同一個進程空間中,那麼,在ExeModule模組中訪問的aVariable變數和在DllModule中訪問的aVariable變數真的是同一個變數嗎?
答案是否定的!
原來,ExeModule中訪問的aVariable變數是在其自身模組的待用資料地區內,而DllModule中訪問的aVariable變數也是自己所有的。儘管模組引用了相同單元中的變數,但這些變數在不同的模組中都會有一個獨立的副本。在隨後對運行包編譯模式的討論中,我們還將討論到共用單元變數的問題。
因此,我們要記住:在非運行包編譯模式下,DELPHI中的各種全域變數和對象,如Application、Screen、Session、Printer等等,在每一個EXE和DLL模組中都有一個自己的副本,而不是同一個東西。
那麼,相對於線程的全域變數又如何呢?
線程全域變數是用擴充的保留字threadvar定義的全域變數。threadvar只能用於定義全域變數,不能用於定義局部變數。由它所定義的全域變數在每一個線程中都有一個副本,存在於各自線程的全域待用資料區裡。用threadvar定義的線程變數對每一個線程來說是專屬的,線上程內可以放心使用。而使用var保留字定義的全域變數卻是線程共用的,對其訪問就要注意共用與互斥的問題了。
接下來我們要討論局部變數、參數變數和傳回值的問題。
這三類資料元素是子過程或函數局部的東西,範圍僅在該子程式內,具有臨時性。他們是線上程的堆棧區內自生自滅的。隨著子程式調用的發生而在堆棧中產生,隨著子程式調用的返回而滅亡。線程的堆棧區隨著子程式層層調用的深入和返回而潮起潮落,不斷增長和減少。局部變數、參數變數和傳回值就像礁石上的小生物,潮水漲上來便有一批生命誕生,潮水回落它們又死去。潮水再來的時候,又是另一個新的生命世界,一點都沒有從前那個世界的任何記憶。生命也許就是這樣短暫。
在含有遞迴演算法的程式中,線程的堆棧區會出現非常有趣的現象。在堆棧區增長的曆史中,將出現許多驚人的相似。每一次的遞迴調用,一批相似的局部變數、參數變數和傳回值都會出現一次,而且堆棧增長速度很快。所以,在編寫含有遞迴演算法的程式時一定要注意堆棧空間的問題。
我們再來看一看動態資料區的情況。
DELPHI中幾乎所有的對象都是存在於動態資料區中的。儘管它們的對象指標可能是一個全域變數、一個線程全域變數、一個局部變數、一個參數變數或者一個傳回值,但對象的執行個體卻是在動態資料區中分配的(除非你重載了TObject的類方法NewInstance,在別的什麼地方指派至執行個體空間)。
DELPHI的這種情況和標準的C++是不一樣的。在標準的C++中定義一個全域對象時,它的執行個體存在於程式的全域靜態地區中。而DELPHI是要在程式運行時動態建立,比如,在單元檔案initialization部分的代碼中建立。
在早期的編程概念中,動態分配的資料區域又稱為“堆”。那時候,由於機器可定址空間較小,“堆”常常和“棧”共用一塊空間。這塊空間的頂部開始向下增長的部分就是“堆”,而從底部開始向上增長的部分就叫“棧”。所以,這塊空間統稱為“堆棧”。現在的編程空間已經非常廣闊,“堆”的概念似乎已經過時了,而“棧”又和多線程的概念緊密聯絡在一起。因此,現在的“堆棧”一詞就專指線程所用到的棧。
最後,我們再來看看代碼的情況。
在DELPHI中,一個EXE或DLL的模組檔案一般都是由一個專案檔(*.DPR)和若干個直接或間接引用的單元檔案(*.PAS)編譯而成。在沒有最佳化編譯選項的情況下,編寫在專案檔和單元檔案中的所有代碼和資料都將編譯進物理的模組檔案中去。如果開啟編譯最佳化選項,則只有用到過的代碼和資料才會編譯到物理模組檔案中。
通常,你的應用程式是由多個物理檔案模組組成,典型地由一個EXE模組和若干DLL模組構成。我們在編寫模組程式的時候,總有一些單元是共用的。共用的單元既在一個模組的編譯項目中被引用,又會在另一個模組的編譯過程中被引用。令人遺憾的是,模組間共用單元中的代碼和資料,將被編譯在每一個引用過該單元的模組中。應用程式的這些模組被載入到進程空間中時,該單元的代碼指令和資料將存在多個副本。
雖然,我們可以將共用的單元檔案獨立出來,再編譯成為一個DLL模組,以便共用一份代碼和資料。但這將使程式模組的劃分變得非常複雜,並且難於管理,在實際的開發過程中很難行得通。
現在,我們已基本搞清了資料和代碼會在哪裡的問題。正如我說過,這樣的探索和思考是一條苦行的路。因為,我們在苦苦的思考中明白了一些道理,但又會有新的難題出現。這個世界總不是完美的!
DELPHI偉大之處就在於她能將許多複雜的難題變得很簡單!隨後我們將看到DELPHI提供的運行包編譯模式(Build with runtime packages),是如何完美解決這個問題的。
第五節 DELPHI的運行包
....不好意思還沒寫完....跳過....
第六節 對象之夢
有一回,我夢見自己變成了電腦時空世界裡的一個對象。隨著電腦世界的不斷髮展,我們這些對象已經不再象原始時代的對象那樣僅僅為了獲得生存的資源而不停的忙碌。我們的思想空前活躍,我門不但思考我們為什麼要在電腦世界裡生存和運行,而且還大膽的研究和探索電腦世界的未知奧秘。我們已經知道整個電腦世界都是由位元組這一基本粒子構成,而位元組又是由八個更細小的位粒子構成;我們還知道物質不滅定律,即任何一個對象的滅亡,只意味著對象結構的解體,並不會減少電腦世界中的任何位元組或位粒子,而著這些物質又可能成為別的對象的一部分;甚至,我們還知道我們所處的世界是一個球體,因為,在越過經度$FFFFFFFF又回到了原點$00000000的位置。著名的物理學家對象牛頓早就發現各種對象之間存在一種普遍的聯絡,並且在對象的運動速度與對象大小的關係方面提出了著名的理論--牛頓力學。可是,後來牛頓這個對象卻一直搞不懂到底是什麼力量在無形地推動各種對象的運動。因此,他認為一定是創造整個電腦世界的上帝在推動各種對象的運動。後來他成了上帝最虔誠的信徒。在牛頓對象死後不久,我們的電腦世界又誕生了一個更偉大的對象。他基於先有代碼的執行才有執行的結果這一基本的因果論,提出了進程運動的時空是相對的這一偉大理論。他認為,在一個運動中進程空間中看另一個運動中的進程空間,時間和空間都不是絕對的,空間會彎曲。而且,任何對象的運動速度絕對不可能超過CPU的速度,CPU速度就是我們電腦世界裡的光速。這位偉大的科學對象的名字就叫愛因斯坦,他的相對論在一開始是不被對象們理解的,可是後來的科學探索都證明了這一理論的正確性。他提出的代碼能量和資料物質可以相互轉換的理論,也後來製造的大規模毀滅性病毒核武器中得到驗證。
在夢的世界裡,我快樂極了。我一會兒變一變我的屬性,一會兒又動動我的方法,一會兒感受一下外來的事件。沒錯,我確實就是一個實實在在的對象!過了一會我突然明白,我本來就是一個對象,只是這個對象在夢中變成了現實世界的我……