以下內容純屬臆測,沒有科學依據,也不想(沒空)翻看權威資料。
C++編譯單元就是指每個cpp文件,整體上看(全局的東西,函數內部不算,類定義內部不算)無非就是變量(包括類的實例也算變量)、函數或者類的聲明和定義。其中變量占用內存空間,存放在運行時的“全局區”,這個內存空間的數據一般是可變的,可以隨時被修改;函數(體)不占用內存空間(本質上也占用,因其編譯完後變成一堆永遠不變的純用來運行的代碼或稱指令,而且占用空間較少,哪怕很大的程序編譯完後純代碼也不大,所以討論時常常認為其不占用空間),存放在“代碼區”,其內容永不被改變;類(體)沒有專門的存放區,因為在運行時根本不存在類了(純個人愚見),就像基本類型int一樣,任何類型在運行期都不起作用了,運行時本質(或者說全部的工作)就是“從某塊內存讀數據進入cpu,或者把cpu的數據存放入某塊內存,或者cpu內部進行運算”,任何類型只在編譯期有用,被編譯器用來進行錯誤檢查等,防止不同類型的變量混雜使用,防止出現一些非常難查找的異常,所以說類體只在編譯時有用,被編譯器使用,把函數內用到的類的成員翻譯成一個個帶有復姓的長名字,如類名.成員名,其中碰到new 類名時,就翻譯成調用類的構造函數(對運行期來說就是一個地址而已),總之類的成員(非靜態,靜態的東西本質就是全局的)不可能出現在全局定義的地方,也即不可能存放在全局區,當然可以在全局的地方定義一個類的實例,編譯器對待他和對待普通變量沒什麼區別,只不過占用的內存空間稍微大了些而已。
先看編譯過程,C++編譯器使用符號表,運行時就不用符號表了。符號表簡單理解為具有三列的表格(符號名、類型、內容),只有變量、函數和類能進入符號表中,其中類型列對於變量來說是描述該符號怎麼分配內存和初始化,類型列對於類來說就是類裡面含有什麼成員,怎麼進行成員初始化,內容列對於變量來說就是變量存放的具體數據(包括對象變量也是),內容列對於函數來說就是函數代碼體的首地址,內容列對於類來說是空的,不用的。C++編譯每個文件時,第一步從整體上掃描,無非就是聲明和定義,把所有定義的東西都裝進那個.cpp文件對應的符號表,就產生了符號表;第二步進行局部掃描,也稱錯誤檢查,首先進行符號檢查,就是掃描函數體和類體,碰到不認識的符號,先看該符號有沒有在本cpp文件聲明,如果聲明了就把當前符號記錄成問號(如 普通變量x和對象變量stu翻譯如下,x = 15翻譯成x? = 15,stu.no =10 0翻譯成stu?.第三個內存單元 = 100),如果符號沒事先聲明,也不在本cpp的符號表裡頭,那就報錯;然後對函數體和類體進行語法錯誤檢查,看看有沒有錯誤的地方;第三步是進行翻譯成機器語言,例如把函數名翻譯成地址,變量名也翻譯成地址等;其中最重要的就是可能把一行的文本代碼翻譯成很多行的機器代碼,例如碰到函數體內使用到new一個類則查找構造函數然後翻譯成一堆對對象成員的分配內存指令,碰到使用對象成員變量時翻譯成從對象變量首地址開始下移多少個位置才取出具體的數據;另外要注意碰到new的地方都依據符號表的類型列翻譯成具體的分配內存和初始化的指令(匯編有具體的指令集,不僅是new,變量定義語句也如此翻譯)。總之經過上面三步過程,編譯就算完了,其最終結果就是產生了一個符號表和一個.obj文件,其中符號表裡都是該cpp文件中定義的全局的東西(變量定義、函數定義或者類定義),obj文件都是函數體(特別注意只有函數體,沒有類體,類體中的函數本質也是函數體,只不過具有復姓而已,類體的成員變量此時無用了,已經都翻譯成某個對象成員便宜多少位地址形式的東西了,其本質就看成對象變量),該函數體就是所謂的指令,其中函數體裡面可能有一堆的含有問號的符號。
再看鏈接過程,鏈接時依然用到符號表,而且強烈依賴符號表,鏈接時第一步先掃描每個cpp文件對應的obj文件,碰到含問號的符號時就在所有的符號表裡查找,找到後把符號對應的內容代替問號;第二步鏈接器把所有的obj文件鏈接在一起形成一個大的obj(在內存中,這裡不考慮鏈接庫),把所有的符號表中變量的內容連在一起形成一個大的表(在內存中,函數和類都不用了);第三步是生存exe文件,具體過程是先劃分出數據區和代碼區(exe只有這兩部分),其中數據區存放符號表中定義的各種全局的變量,代碼區存放大obj的內容(即所有函數體的集合,注意只有函數體,即所有指令)。
以後運行exe時,操作系統都是先根據exe的數據部分分配出全局區(包括常量區),然後根據代碼部分分配代碼區,然後系統自己分配棧區和堆區,就可以開始運行了。
所謂靜態聯編就是指exe運行前(也稱編譯期)會起作用的語句,動態聯編實在運行期(main運行後開始算起)會起作用的語句。例如變量定義語句分兩種,全局變量就是編譯器就起作用的語句,因為全局變量在生成的exe文件中已經被描述成全局的東西了,exe文件運行成進程的開始就會先產生全局區,然後給全局表裡分配內存,也就可以看出全局變量定義語句在真正運行前(main函數運行前)就起作用了;而對於函數內的局部變量,則屬於運行前起作用的語句。因此我們說函數重載都是靜態聯編,因為重載在編譯時就確定是哪個函數了,也即編譯時就翻譯成具體的確定了的函數地址了;而運行時的多態如Father *p=new children(); p.fun(); 中的fun函數運行的是子類的函數,原因是p.fun語句在編譯器僅僅進行語法錯誤檢查,根本就沒有真正運行(函數體裡的語句除了靜態變量定義外,全部都在運行期才起作用),而在運行期才真正起作用,所以是動態聯編。
你會發現除了函數體內部的語句外,都是聲明性語句或定義性語句,所以我把整個代碼分成這三類,其中函數體內部除了靜態變量定義外,都是運行性語句,而且運行性語句只會在函數體內部出現(包括類的成員函數體)。這裡說的運行性語句是指main函數開始後真正起作用的語句。需要注意的是函數裡面出現的new一個對象或者定義一個變量的語句早在編譯器就依據符號表的類型列翻譯成一堆分配內存和初始化指令了(匯編有具體的指令集),但是編譯器僅僅是把其翻譯成一堆機器指令,並明確了怎麼分配和初始化(任何變量包括類一定都是在編譯器就明確怎麼分配內存和初始化),並沒有真正在內存中分配內存,真正分配內存實在man運行後,運行到該代碼時才起作用的,所以我也常常認為函數體內的變量定義性語句(包括new)是“半運行性語句”,因為好像匯編期間其也起了一點作用(雖然只是起到明確怎麼干,但沒有真正干)。
類在編譯器作用就看成是個模板,他本身自己不起任何作用,有用的是依據他建立的一個個對象的成員,編譯完成後,就完全不需要類了,而且編譯完成後的代碼裡面沒有類,所有的對象都翻譯成一個變量的首地址,對象的成員都翻譯成相對於首地址的偏移地址。尤其是運行期起作用的僅僅是函數體內的代碼,類就更不可能用到了,類的成員函數也早翻譯成復姓函數,跟類沒有關系,就跟普通函數一模一樣。所以類本質就是個模板,用來方便組織數據和行為,沒有他也一樣能做到,只不過有了它能使人不用考慮具體組織細節,而且更方便人們用面向對象思維思考問題,所以類是面向對象思維的一個實現工具,沒有類,一樣可以用別的方法實現面向對象思維。
Java編譯期和C++完全不一樣,他比C++簡單得多,也不會使用C++中的用於連接多個obj的符號表(填充多個obj中的問號符號)。java不存在鏈接,只有編譯,而且編譯也僅僅是把文本代碼翻譯成字節碼,其中最需要注意的就是,碰到import語句時,會把下面對於的類的前面加上包名,形成類的全名形式。
Java運行時先運行含有main函數的那個class字節碼文件,碰到第一次使用的類時,Java虛擬機的類加載器才去加載那個類到內存中運行。
C++可不是這樣,C++運行時早就沒有類的概念了,所有的東西都變成和普通變量和普通函數沒什麼區別的東西了,而且類函數早就變成普通函數進入exe的代碼區了,所以從代碼量來看,C++中的類的代碼即使沒運行到跟C++類有任何關聯的代碼,C++類的代碼也依然占用內存(例如exe的代碼區就包括了在類函數體裡定義的指令)。當然我們這裡討論的東西不包括C++的動態鏈接庫,實際上C++的動態鏈接庫使用了別的技術(從行為上來看動態鏈接庫更像java中的類加載器動態加載類的技術)。
Java類加載器(虛擬機類加載器)在兩個階段分別起作用,在編譯階段或者運行階段,第一次碰到使用某個類時(注意不是import,import只是說明把一些符號加上復姓而已),例如new一個類實例,或者聲明一個類實例等等,類加載器起作用,把該類加載進入內存,具體過程是根據父包名.子包名.類名,把其中的點號都變成斜槓,然後形成一個子目錄,附加到classpath父目錄後面,然後根據這個目錄查找到類文件並加載到內存中,一旦一個類已經加載到內存中,以後再用到該類時就不重復查找和加載,直接使用內存中的類就可以了。
這也解釋了為什麼.java文件中定義的每個類(包括內部類)都會生成一個以該類名作為文件名的.class文件。這是因為便於類加載器查找到這個類的具體定義,否則想想看,如果文件名不和類名一致,類加載器定位都類所在目錄後,由於不知道類在哪個文件中,就需要把所有的文件都打開然後在每個文件中查找,效率肯定非常低下。
而且之所以Java規定一個.java文件中最多只能有一個public類,並且一旦含有public類,則該.java文件名必須和public類名一致(包括大小寫)是因為,只有public的類才會對其他包輸出並且很可能被多次用到(被多個其他包的類文件用到),java類加載器每編譯一個java文件都會把含有public的類事先放到內存中存放起來,以後在編譯別的java文件時(注意編譯主要就是明確變量怎麼分配內存和初始化而且一定會明確和進行錯誤檢查),如果用到則直接從內存中取出,提高了編譯速度,而那些非public的類文件由於一般只被本包中的類文件用到一次或很少的幾次(例如一個非public的類被本包中的多個輸出類用到),所以編譯器編譯到這個類時不用事先把其放到內存,以後用的的話按需加載,這樣就大大提高了編譯效率。
有了上面的知識就容易分析兩者的區別了,include作用是在原地展開,import的作用僅僅是起到復姓的作用。c++中碰到include就會把頭文件內容原地展開,等於代碼長度增加了。java中碰到import則表示下面的代碼碰到某個符號時,在起前面加上復姓,變成包.類名的形式。我認為import根本沒有起到目錄查找的作用,因為import使得每個類都有復姓的形式,以後第一次加載該類的時候通過復姓在目錄中查找就行了,所以import語句時根本就沒有查找,只是在第一次加載類的時候才查找。
無論是c++還是java,理解類時都把他看成一個類型,定義一個類時是產生了一個模板,通過類實例化時是產生了一堆成員,只不過這些成員在內存上是緊挨著的並且都具有復姓。總之,類就可以看成具有復姓的一堆成員而已。
至於靜態鏈接庫,就理解為完全和cpp無任何區別,只不過提前把cpp編譯成二級制代碼而已。
至於動態鏈接庫,本質就是一堆共享的變量和函數(注意沒有類,動態鏈接庫中到處的類本質是導出加了復姓的變量和函數而已)。要輸出整個的類,對類使用_declspec(_dllexpot);要輸出類的成員函數,則對該函數使用_declspec(_dllexport)。其中導出類就相當於在類的所有成員前面加上_declspec(_dllexport)。所以本質上根本沒有導出類,導出的都是成員,即使是實例化一個類實例,也是使用了導出的構造方法而已。
所以可以推測動態鏈接庫實現方法大致如下:在源文件中用的變量或函數或者類實例都在源文件中留下了編譯成特定地址的符號,而在dll庫中含有相同的編譯成特定地址的符號,運行時碰到該地址就知道在dll的那個位置運行即可。
有關c++動態鏈接庫導出類的詳細過程參加文章:http://blog.csdn.net/clever101/article/details/3034743
Java和C#都有運行時的類加載器,當運行時第一次用到某個類時,類加載器就會把該類加載進內存,所以使用起來非常簡單,類甚至也可以看成一個具體的對象或變量,對其進行處理,但是C++運行時根本就不存在類了,所以無法直接處理類,所以根本不存在反射機制等。
另外Java和C#在第一次加載類進內存時還是有區別的,在Java中類存在一個個的.class字節碼文件中,並且加載器根據類名的復姓查找到位置並加載該類。而在C#中,沒對每個類生產單個類文件(這是C#先進之處,避免產生大量的文件),而是把多個類防止一個dll文件中(此dll文件稱為程序集),使用時在編譯器需要提前把用到的程序集引入到工程中(我猜測就是在生成的exe文件中寫上用了哪些程序集),編譯時碰到不認識的符號就從引入的哪些程序集裡查找,一旦找到就把對於的符號寫成“程序集名.命名空間名.符號名”的形式,以後在運行期間第一次碰到“程序集名.命名空間名.符號名”時就根據程序集名字在工作目錄中或System32目錄中動態加載該程序集進入內存,並從程序集中找到類信息供類加載器使用。總之,Java是通過“命名空間名.類名”來動態加載類(Java的包名就完全等價於命名空間名,因為包名唯一作用就是復姓作用),而C#是通過“程序集名.命名空間名.符號名”來動態加載類(就好像java中的一部分類打包成一個程序集並起個模塊名,然後根據模塊名.命名空間名.類名訪問一樣,這樣加個層級可更好的分門別類的管理大量的類)。
最後補充一點,C#中的using 命名空間名和Java的import作用是完全一模一樣的,都是僅僅起到復姓的作用,沒有其他任何作用,不要想復雜了。只不過import ss是在代表以下代碼中出現的類名全部替換成ss,而using ss是代表下面代碼不認識的符號前面自動加上ss作為姓。