編譯器與連接器
我從沒見過(不過應該有)任何一本C++教材有講過何謂編譯器(Compiler)及連接器(Linker)(倒是在很老的C教材中見過),現在都通過一個類似VC這樣的編程環境隱藏了大量東西,將這些封裝起來。在此,對它們的理解是非常重要的,本系列後面將大量運用到這兩個詞彙,其決定了能否理解如聲明、定義、外部變數、頭文件等非常重要的關鍵。
前面已經說明了電腦編程就是一個「翻譯」過程,要把用戶的程序翻譯成CPU指令,其實也就是機器代碼。所謂的機器代碼就是用CPU指令書寫的程序,被稱作低級語言。而程序員的工作就是編寫出機器代碼。由於機器代碼完全是一些數字組成(CPU感知的一切都是數字,即使是指令,也只是1代表加法、2代表減法這一類的數字和工作的映射),人要記住1是代表加法、2是代表減法將比較困難,並且還要記住第3塊內存中放的是圓周率,而第4塊內存中放的是有效位數。所以發明了匯編語言,用一些符號表示加法而不再用1了,如用ADD表示加法等。
由於使用了匯編語言,人更容易記住了,但是電腦無法理解(其只知道1是加頌隱法,不知道ADD是加法,因為電腦只能看見數字),所以必須有個東西將匯編代碼翻譯成機器代碼,也就是所謂的編譯器。即編譯器是將一種語言翻譯成另一種語言的程序。即使使用了匯編語言,但由於其幾乎只是將CPU指令中的數字映射成符號以幫助記憶而已,還是使用的空跡電腦的思考方式進行思考的,不夠接近人類的思考習慣,故而出現了紛繁復雜的各種電腦編程語言,如:PASCAL、BASIC、C等,其被稱作高級語言,因為比較接近人的思考模式(尤其C++的類的概念的推出),而匯編語言則被稱作低級語言(C曾被稱作高級的低級語言),因為它們不是很符合人類的思考模式,人類書野虧廳寫起來比較困難。由於CPU同樣不認識這些PASCAL、BASIC等語言定義的符號,所以也同樣必須有一個編譯器把這些語言編寫的代碼轉成機器代碼。對於這里將要講到的C++語言,則是C++語言編譯器(以後的編譯器均指C++語言編譯器)。
因此,這里所謂的編譯器就是將我們書寫的C++源代碼轉換成機器代碼。由於編譯器執行一個轉換過程,所以其可以對我們編寫的代碼進行一些優化,也就是說其相當於是一個CPU指令程序員,將我們提供的程序翻譯成機器代碼,不過它的工作要簡單一些了,因為從人類的思考方式轉成電腦的思考方式這一過程已經由程序員完成了,而編譯器只是進行翻譯罷了(最多進行一些優化)。
還有一種編譯器被稱作翻譯器(Translator),其和編譯器的區別就是其是動態的而編譯器是靜態的。如前面的BASIC的編譯器在早期版本就被稱為翻譯器,因為其是在運行時期即時進行翻譯工作的,而不像編譯器一次性將所有代碼翻成機器代碼。對於這里的「動態」、「靜態」和「運行時期」等名詞,不用刻意去理解它,隨著後續文章的閱讀就會了解了。
編譯器把編譯後(即翻譯好的)的代碼以一定格式(對於VC,就是COFF通用對象文件格式,擴展名為.obj)存放在文件中,然後再由連接器將編譯好的機器代碼按一定格式在Windows操作系統下就是Portable Executable File Format--PE文件格式)存儲在文件中,以便以後操作系統執行程序時能按照那個格式找到應該執行的第一條指令或其他東西,如資源等。至於為什麼中間還要加一個連接器以及其它細節,在後續文章中將會進一步說明。
❷ 匯編編譯器和連接器是如何協同工作的
NO,I 't now .《lntel匯編程序設計》的作業題可以找的啊......
..................................................................................................................................................................................................................
❸ Visual Unit 設定編譯器和連接器
預先安裝好VC,比如我安裝的是Microsoft visual studio 2005.
再來,Visual Unit 4.0 安裝,完成後,重新打開進入演示版。
在窗口的主界面,點擊菜單欄「工程」-》「打開示例工程」,選擇一個自己想打開的項目。
點擊菜單欄「工具」-》「設定」。裡面便有「編譯器」和「編輯器」。
點擊「編譯器」,「名稱」項,選擇你安裝VC對應的Visual C++ 版本,比如"Visual C++ 2005". 編譯器和鏈接器,同樣道理,選擇之前你安裝VC的根目錄下,對應的編譯器和鏈接器。比如:
「D:\Program files\Microsoft Visual Studio 2005\VC\bin\cl.exe」
「D:\Program files\Microsoft Visual Studio 2005\VC\bin\link.exe」。
這樣便可以了。
如果項目執行測試,還是有類似編譯器或者鏈接器的問題,請繼續看以下注意事項,檢查下工程屬性是否一致:
待工程打開載入後,再次點擊菜單欄「工程」-》「工程屬性」,
在「常規」界面,選擇你需要的測試代碼編譯器,比如我裝的VC2005,那麼我就選擇Visual C++ 2005.
點擊第五個tab "鏈接"。點擊「環境變數」。在變數名下拉列表中,點擊您安裝VC的版本對應的dir,比如「vc2005_dir」. 在下面的「值」一欄,點擊「瀏覽」找到VC對應的根目錄。比如「D:/Program files/Microsoft Visual Studio 2005」。「OK」確認。再點擊當前小窗口的「保存」。關閉小窗口。
退回到「工程屬性」窗口,檢查「庫文件搜索目錄」,是否是自己設定的vc2005_dir為頭的目錄,比如:「$(vc2005_dir)/vc/lib」.
其它地方比如「頭文件」里的目錄設置,亦是如此。不過只要你設置了一處的環境變數,其它地方也就會自動調用你設置的路徑下的編譯器了。
希望對你有幫助。:)
❹ 對單片機編程要用什麼軟體(編寫單片機程序用什麼軟體)
keil最流行
單片機開發中除必要的硬體外,同樣離不開軟體,我們寫的匯編語昌咐言源程序要變為CPU可以執行的機器碼有兩種方法,一種是手工匯編,另一種是機器匯編,目前已極少使用手工匯編的方法了。機器匯編是通過匯編軟體將源程序變為機器碼,用於MCS-51單片機的匯編軟體有早期的A51,隨著單片機開發技術的不斷發展,從普遍使用匯編語言到逐漸使用高級語言開發,單片機的開發軟體也在不斷發展,Keil軟體是目前最流行開發MCS-51系列單片機的軟體,這從近年來各模擬機廠商紛紛宣布全面支持Keil即可看出。Keil提供了包括C編譯器、宏匯編、連接器、庫管理和一個功能強大的模擬調試器等在內的完整開發方案,通過一個集成開發環境(uVision)將這些部份組合在一起。運行Keil軟體需要Pentium或以上的CPU,16MB或更多RAM、20M以上空閑的硬碟空間、WIN98、NT、WIN2000、WINXP等操作系統。掌握這一軟體的使用對於使用51系列單片機的愛好者來說是十分必要的,如果你使用C語言編程,那麼Keil幾乎就是你的不二之選(目前在國內你只能買到該軟體、而你買的模擬機也很可能只支持該軟體),即使不使用C語言而僅用匯耐灶純編語言編程,其方便易用的集成環境辯汪、強大的軟體模擬調試工具也會令你事半功倍。
❺ 編譯器的發展史
編譯器
編譯器,是將便於人編寫,閱讀,維護的高級計算機語言翻譯為計算機能識別,運行的低級機器語言的程序。編譯器將源程序(Source program)作為輸入,翻譯產生使用目標語言(Target language)的等價程序。源程序一般為高級語言(High-level language),如Pascal,C++等,而目標語言則是匯編語言或目標機器的目標代碼(Object code),有時也稱作機器代碼(Machine code)。
一個現代編譯器的主要工作流程如下:
源程序(source code)→預處理器(preprocessor)→編譯器(compiler)→匯編程序(assembler)→目標程序(object code)→連接器(鏈接器,Linker)→可執行程序(executables)
目錄 [隱藏]
1 工作原理
2 編譯器種類
3 預處理器(preprocessor)
4 編譯器前端(frontend)
5 編譯器後端(backend)
6 編譯語言與解釋語言對比
7 歷史
8 參見
工作原理
翻譯是從源代碼(通常為高級語言)到能直接被計算機或虛擬機執行的目標代碼(通常為低級語言或機器言)。然而,也存在從低級語言到高級語言的編譯器,這類編譯器中用來從由高級語言生成的低級語言代碼重新生成高級語言代碼的又被叫做反編譯器。也有從一種高級語言生成另一種高級語言的編譯器,或者生成一種需要進一步處理的的中間代碼的編譯器(又叫級聯)。
典型的編譯器輸出是由包含入口點的名字和地址以及外部調用(到不在這個目標文件中的函數調用)的機器代碼所組成的目標文件。一組目標文件,不必是同一編譯器產生,但使用的編譯器必需採用同樣的輸出格式,可以鏈接在一起並生成可以由用戶直接執行的可執行程序。
編譯器種類
編譯器可以生成用來在與編譯器本身所在的計算機和操作系統(平台)相同的環境下運行的目標代碼,這種編譯器又叫做「本地」編譯器。另外,編譯器也可以生成用來在其它平台上運行的目標代碼,這種編譯器又叫做交叉編譯器。交叉編譯器在生成新的硬體平台時非常有用。「源碼到源碼編譯器」是指用一種高級語言作為輸入,輸出也是高級語言的編譯器。例如: 自動並行化編譯器經常採用一種高級語言作為輸入,轉換其中的代碼,並用並行代碼注釋對它進行注釋(如OpenMP)或者用語言構造進行注釋(如FORTRAN的DOALL指令)。
預處理器(preprocessor)
作用是通過代入預定義等程序段將源程序補充完整。
編譯器前端(frontend)
前端主要負責解析(parse)輸入的源程序,由詞法分析器和語法分析器協同工作。詞法分析器負責把源程序中的『單詞』(Token)找出來,語法分析器把這些分散的單詞按預先定義好的語法組裝成有意義的表達式,語句 ,函數等等。 例如「a = b + c;」前端詞法分析器看到的是「a, =, b , +, c;」,語法分析器按定義的語法,先把他們組裝成表達式「b + c」,再組裝成「a = b + c」的語句。 前端還負責語義(semantic checking)的檢查,例如檢測參與運算的變數是否是同一類型的,簡單的錯誤處理。最終的結果常常是一個抽象的語法樹(abstract syntax tree,或 AST),這樣後端可以在此基礎上進一步優化,處理。
編譯器後端(backend)
編譯器後端主要負責分析,優化中間代碼(Intermediate representation)以及生成機器代碼(Code Generation)。
一般說來所有的編譯器分析,優化,變型都可以分成兩大類: 函數內(intraproceral)還是函數之間(interproceral)進行。很明顯,函數間的分析,優化更准確,但需要更長的時間來完成。
編譯器分析(compiler analysis)的對象是前端生成並傳遞過來的中間代碼,現代的優化型編譯器(optimizing compiler)常常用好幾種層次的中間代碼來表示程序,高層的中間代碼(high level IR)接近輸入的源程序的格式,與輸入語言相關(language dependent),包含更多的全局性的信息,和源程序的結構;中層的中間代碼(middle level IR)與輸入語言無關,低層的中間代碼(Low level IR)與機器語言類似。 不同的分析,優化發生在最適合的那一層中間代碼上。
常見的編譯分析有函數調用樹(call tree),控制流程圖(Control flow graph),以及在此基礎上的 變數定義-使用,使用-定義鏈(define-use/use-define or u-d/d-u chain),變數別名分析(alias analysis),指針分析(pointer analysis),數據依賴分析(data dependence analysis)等等。
上述的程序分析結果是編譯器優化(compiler optimization)和程序變形(compiler transformation)的前提條件。常見的優化和變新有:函數內嵌(inlining),無用代碼刪除(Dead code elimination),標准化循環結構(loop normalization),循環體展開(loop unrolling),循環體合並,分裂(loop fusion,loop fission),數組填充(array padding),等等。 優化和變形的目的是減少代碼的長度,提高內存(memory),緩存(cache)的使用率,減少讀寫磁碟,訪問網路數據的頻率。更高級的優化甚至可以把序列化的代碼(serial code)變成並行運算,多線程的代碼(parallelized,multi-threaded code)。
機器代碼的生成是優化變型後的中間代碼轉換成機器指令的過程。現代編譯器主要採用生成匯編代碼(assembly code)的策略,而不直接生成二進制的目標代碼(binary object code)。即使在代碼生成階段,高級編譯器仍然要做很多分析,優化,變形的工作。例如如何分配寄存器(register allocatioin),如何選擇合適的機器指令(instruction selection),如何合並幾句代碼成一句等等。
編譯語言與解釋語言對比
許多人將高級程序語言分為兩類: 編譯型語言 和 解釋型語言 。然而,實際上,這些語言中的大多數既可用編譯型實現也可用解釋型實現,分類實際上反映的是那種語言常見的實現方式。(但是,某些解釋型語言,很難用編譯型實現。比如那些允許 在線代碼更改 的解釋型語言。)
歷史
上世紀50年代,IBM的John Backus帶領一個研究小組對FORTRAN語言及其編譯器進行開發。但由於當時人們對編譯理論了解不多,開發工作變得既復雜又艱苦。與此同時,Noam Chomsky開始了他對自然語言結構的研究。他的發現最終使得編譯器的結構異常簡單,甚至還帶有了一些自動化。Chomsky的研究導致了根據語言文法的難易程度以及識別它們所需要的演算法來對語言分類。正如現在所稱的Chomsky架構(Chomsky Hierarchy),它包括了文法的四個層次:0型文法、1型文法、2型文法和3型文法,且其中的每一個都是其前者的特殊情況。2型文法(或上下文無關文法)被證明是程序設計語言中最有用的,而且今天它已代表著程序設計語言結構的標准方式。分析問題(parsing problem,用於上下文無關文法識別的有效演算法)的研究是在60年代和70年代,它相當完善的解決了這個問題。現在它已是編譯原理中的一個標准部分。
有限狀態自動機(Finite Automaton)和正則表達式(Regular Expression)同上下文無關文法緊密相關,它們與Chomsky的3型文法相對應。對它們的研究與Chomsky的研究幾乎同時開始,並且引出了表示程序設計語言的單詞的符號方式。
人們接著又深化了生成有效目標代碼的方法,這就是最初的編譯器,它們被一直使用至今。人們通常將其稱為優化技術(Optimization Technique),但因其從未真正地得到過被優化了的目標代碼而僅僅改進了它的有效性,因此實際上應稱作代碼改進技術(Code Improvement Technique)。
當分析問題變得好懂起來時,人們就在開發程序上花費了很大的功夫來研究這一部分的編譯器自動構造。這些程序最初被稱為編譯器的編譯器(Compiler-compiler),但更確切地應稱為分析程序生成器(Parser Generator),這是因為它們僅僅能夠自動處理編譯的一部分。這些程序中最著名的是Yacc(Yet Another Compiler-compiler),它是由Steve Johnson在1975年為Unix系統編寫的。類似的,有限狀態自動機的研究也發展了一種稱為掃描程序生成器(Scanner Generator)的工具,Lex(與Yacc同時,由Mike Lesk為Unix系統開發)是這其中的佼佼者。
在70年代後期和80年代早期,大量的項目都貫注於編譯器其它部分的生成自動化,這其中就包括了代碼生成。這些嘗試並未取得多少成功,這大概是因為操作太復雜而人們又對其不甚了解。
編譯器設計最近的發展包括:首先,編譯器包括了更加復雜演算法的應用程序它用於推斷或簡化程序中的信息;這又與更為復雜的程序設計語言的發展結合在一起。其中典型的有用於函數語言編譯的Hindley-Milner類型檢查的統一演算法。其次,編譯器已越來越成為基於窗口的交互開發環境(Interactive Development Environment,IDE)的一部分,它包括了編輯器、連接程序、調試程序以及項目管理程序。這樣的IDE標准並沒有多少,但是對標準的窗口環境進行開發已成為方向。另一方面,盡管近年來在編譯原理領域進行了大量的研究,但是基本的編譯器設計原理在近20年中都沒有多大的改變,它現在正迅速地成為計算機科學課程中的中心環節。
在九十年代,作為GNU項目或其它開放源代碼項目的一部分,許多免費編譯器和編譯器開發工具被開發出來。這些工具可用來編譯所有的計算機程序語言。它們中的一些項目被認為是高質量的,而且對現代編譯理論感性趣的人可以很容易的得到它們的免費源代碼。
大約在1999年,SGI公布了他們的一個工業化的並行化優化編譯器Pro64的源代碼,後被全世界多個編譯器研究小組用來做研究平台,並命名為Open64。Open64的設計結構好,分析優化全面,是編譯器高級研究的理想平台。
編譯器是一種特殊的程序,它可以把以特定編程語言寫成的程序變為機器可以運行的機器碼。我們把一個程序寫好,這時我們利用的環境是文本編輯器。這時我程序把程序稱為源程序。在此以後程序員可以運行相應的編譯器,通過指定需要編譯的文件的名稱就可以把相應的源文件(通過一個復雜的過程)轉化為機器碼了。
編譯器工作方法
首先編譯器進行語法分析,也就是要把那些字元串分離出來。然後進行語義分析,就是把各個由語法分析分析出的語法單元的意義搞清楚。最後生成的是目標文件,我們也稱為obj文件。再經過鏈接器的鏈接就可以生成最後的可執行代碼了。有些時候我們需要把多個文件產生的目標文件進行鏈接,產生最後的代碼。我們把一過程稱為交叉鏈接。
❻ 誰能幫我解釋一下C交叉編譯器,匯編器,連接器 和ucos系統的關系
沒有必然的聯系,只是ucos採用C語言擾岩編制,故而需要C編譯器。如果用匯編編制,那需要的就是匯編器。理論上,ucos還可以用其它編程語言來編制,那麼就需要相應語言的編譯器鎮李滑。不御臘過就ucos本身而言,跟編譯器是沒有關系的。
❼ 電腦里自帶匯編編譯器和連接器嗎
好像沒有,需要自己下載
dos下用masm5.0 link.exe
網上很多的
❽ C++從零開始——何謂類
前篇說明了結構只不過是定義了內存布局而已,提到類型定義符前還可以書寫class,即類型的自定義類型(簡稱類),它和結構根本沒有區別(僅有一點小小的區別,下篇說明),而之所以還要提供一個class,實際是由於C++是從C擴展而成,其中的class是C++自
己提出的一個很重要的概念,只是為了與C語言兼容而保留了struct這個關鍵字。不過通過前面括弧中所說的小小區別也足以看出C++的設計者為結構和類定義的不同語義,下篇說明。
暫時可以先認為類較結構的長足進步就是多了成員函數這個概念(雖然結構也可以有成員函數),在了解成員函數之前,先來看一種語義需求。
操作與資源
程序主要是由操作和被操作的資源組成,操作的執行者就是CPU,這很正常,但有時候的確存在一些需要,需要表現是某個資源操作了另一個資源(暫時稱作操作者),比如游戲中,經常出現的就是要映射怪物攻擊了玩家。之所以需要操作者,一般是因為這個操作也需要修改操作者或利用操作者記錄的一些信息來完成操作,比如怪物的攻擊力來決定玩家被攻擊後的狀態。這種語義就表現為操作者具有某些功能。為了實現上面的語義差枯散,如原來所說進行映射,先映射怪物和玩家分別為結構,如下:
struct Monster { float Life; float Attack; float Defend; };
struct Player { float Life; float Attack; float Defend; };
上面的攻擊操作就可以映射為void MonsterAttackPlayer( Monster mon, Player pla );。注意這里期望通過函數名來表現操作者,但和前篇說的將過河方案起名為sln一樣,屬於一種本末倒置,因為這個語義應該由類型來表現,而不是函數名。為此,C++提供了成員函數的概念。
成員函數
與之前一樣,在類型定義符敗槐中書寫函數的聲明語句將定義出成員函數,如下:
struct ABC { long a; void AB( long ); };
上面就定義了一個映射元素--第一個變數ABC::a,類型為long ABC::;以及聲明了一個映射元素--第二個函數ABC::AB,類型為void ( ABC:: )( long )。類型修飾符ABC::在此修飾了函數ABC::AB,表示其為函數類型的偏移類型,即是一相對值。但由於是函數,意義和變數不同,即其依舊映射的是內存中的地址(代碼的地址),但由於是偏移類型,也就是相對的,即是不完整的,因此不能對它應用函數操作符,如:ABC::AB( 10 );。這里將錯誤,因為ABC::AB是相對的,其相對的東西不是如成員變數那樣是個內存地址,而是一個結構指針類型的參數,參數名一定為this,這是強行定義的,後面說明。
注意由於其名字為ABC::AB,而上面僅僅是對其進行了聲明,要定義它,仍和之前的函數定義一樣,如下:
void ABC::AB( long d ) { this-a = d; }
應注意上面函數的名字為ABC::AB,但和前篇說的成員變數一樣,不能直接書寫long ABC::a;,也就不能直接如上書寫函數的定義語句(至少函數名為ABC::AB就不虛氏符合標識符規則),而必須要通過類型定義符「{}」先定義自定義類型,然後再書寫,這會在後面說明聲明時詳細闡述。
注意上面使用了this這個關鍵字,其類型為ABC*,由編譯器自動生成,即上面的函數定義實際等同於void ABC::AB( ABC *this, long d ) { this-a = d; }。而之所以要省略this參數的聲明而由編譯器來代勞是為了在代碼上體現出前面提到的語義(即成員的意義),這也是為什麼稱ABC::AB是函數類型的偏移類型,它是相對於這個this參數而言的,如何相對。
如下:
ABC a, b, c; a.ABC::AB( 10 ); b.ABC::AB( 12 ); c.AB( 14 );
上面利用成員操作符調用ABC::AB,注意執行後,a.a、b.a和c.a的值分別為10、12和14,即三次調用ABC::AB,但通過成員操作符而導致三次的this參數的值並不相同,並進而得以修改三個ABC變數的成員變數a。注意上面書寫a.ABC::AB( 10 );,和成員變數一樣,由於左右類型必須對應,因此也可a.AB( 10 );。還應注意上面在定義ABC::AB時,在函數體內書寫this-a = d;,同上,由於類型必須對應的關系,即this必須是相應自定義類型的指針,所以也可省略this-的書寫,進而有void ABC::AB( long d ) { a = d; }。
注意這里成員操作符的作用,其不再如成員變數時返回相應成員變數類型的數字,而是返回一函數類型的數字,但不同的就是這個函數類型是無法用語法表示出來的,即C++並沒有提供任何關鍵字或類型修飾符來表現這個返回的類型(VC內部提供了__thiscall這個類型修飾符進行表示,不過寫代碼時依舊不能使用,只是編譯器內部使用)。也就是說,當成員操作符右側接的是函數類型的偏移類型的數字時,返回一個函數類型的數字(表示其可被施以函數操作符),函數的類型為偏移類型中給出的類型,但這個類型無法表現。即a.AB將返回一個數字,這個數字是函數類型,在VC內部其類型為void ( __thiscall ABC:: )( long ),但這個類型在C++中是非法的。
C++並沒有提供類似__thiscall這樣的關鍵字以修飾類型,因為這個類型是要求編譯器遇到函數操作符和成員操作符時,如a.AB( 10 );,要將成員操作符左側的地址作為函數調用的第一個參數傳進去,然後再傳函數操作符中給出的其餘各參數。即這個類型是針對同時出現函數操作符和成員操作符這一特定情況,給編譯器提供一些信息以生成正確的代碼,而不用於修飾數字(修飾數字就要求能應付所有情況)。即類型是用於修飾數字的,而這個類型不能修飾數字,因此C++並未提供類似__thiscall的關鍵字。和之前一樣,由於ABC::AB映射的是一個地址,而不是一個偏移值,因此可以ABC::AB;但不能ABC::a;,因為後者是偏移值。根據類型匹配,很容易就知道也可有:
void ( ABC::*p )( long ) = ABC::AB;或void ( ABC::*p )( long ) = ABC::AB;
進而就有:void ( ABC::**pP )( long ) = p; ( c.**pP )( 10.0f );。之所以加括弧是因為函數操作符的優先順序較「*」高。再回想前篇說過指針類型的轉換只是類型變化,數值不變(下篇說明數值變化的情況),因此可以有如下代碼,這段代碼毫無意義,在此僅為加深對成員函數的理解。
struct ABC { long a; void AB( long ); };
void ABC::AB( long d )
{
this-a = d;
}
struct AB
{
short a, b;
void ABCD( short tem1, short tem2 );
void ABC( long tem );
};
void AB::ABCD( short tem1, short tem2 )
{
a = tem1; b = tem2;
}
void AB::ABC( long tem )
{
a = short( tem / 10 );
b = short( tem - tem / 10 );
}
void main()
{
ABC a, b, c; AB d;
( c.*( void ( ABC::* )( long ) )AB::ABC )( 43 );
( b.*( void ( ABC::* )( long ) )AB::ABCD )( 0XABCDEF12 );
( d.*( void ( AB::* )( short, short ) )ABC::AB )( 0XABCD, 0XEF12 );
}
上面執行後,c.a為0X00270004,b.a為0X0000EF12,d.a為0XABCD,d.b為0XFFFF。對於c的函數調用,由於AB::ABC映射的地址被直接轉換類型進而直接被使用,因此程序將跳到AB::ABC處的a = short( tem / 10 );開始執行,而參數tem映射的是傳遞參數的內存的首地址,並進而用long類型解釋而得到tem為43,然後執行。注意b = short( tem - tem / 10 );實際是this-b = short( tem - tem / 10 );,而this的值為c對應的地址,但在這里被認為是AB*類型(因為在函數AB::ABC的函數體內),所以才能this-b正常(ABC結構中沒有b這個成員變數),而b的偏移為2,所以上句執行完後將結果39存放到c的地址加2所對應的內存,並且以short類型解釋而得到的16位的二進制數存放。對於a = short( tem / 10 );也做同樣事情,故最後得c.a的值為0X0027004(十進制39轉成十六進制為0X27)。
同樣,對於b的調用,程序將跳到AB::ABCD,但生成的b的調用代碼時,將參數0XABCDEF12按照參數類型為long的格式記錄在傳遞參數的內存中,然後跳到AB::ABCD。但編譯AB::ABCD時又按照參數為兩個short類型來映射參數tem1和tem2對應的地址,因此容易想到tem1的值將為0XEF12,tem2的值為0XABCD,但實際並非如此。參數如何傳遞由之前說的函數調用規則決定,函數調用的具體實現細節在《C++從零開始(十五)》中說明,這里只需了解到成員函數映射的仍然是地址,而它的類型決定了如何使用它,後面說明。
聲明的含義前面已經解釋過聲明是什麼意思,在此由於成員函數的定義規則這種新的定義語法,必須重新考慮聲明的意思。注意一點,前面將一個函數的定義放到main函數定義的前面就可以不用再聲明那個函數了;同樣如果定義了某個變數,就不用再聲明那個變數了。這也就是說定義語句具有聲明的功能,但上面成員函數的定義語句卻不具有聲明的功能,下面來了解聲明的真正意思。
聲明是要求編譯器產生映射元素的語句。所謂的映射元素,就是前面介紹過的變數及函數,都只有3欄(或3個欄位):類型欄、名字欄和地址欄(成員變數類型的這一欄就放偏移值)。即編譯器每當看到聲明語句,就生成一個映射元素,並且將對應的地址欄空著,然後留下一些信息以告訴連接器--此.obj文件(編譯器編譯源文件後生成的文件,對於VC是.obj文件)需要一些符號,將這些符號找到後再修改並完善此.obj文件,最後連接。
回想之前說過的符號的意思,它就是一字元串,用於編譯器和連接器之間的通信。注意符號沒有類型,因為連接器只是負責查找符號並完善(因為有些映射元素的地址欄還是空的)中間文件(對於VC就是.obj文件),不進行語法分析,也就沒有什麼類型。
定義是要求編譯器填充前面聲明沒有書寫的地址欄。也就是說某變數對應的地址,只有在其定義時才知道。因此實際的在棧上分配內存等工作都是由變數的定義完成的,所以才有聲明的變數並不分配內存。但應注意一個重點,定義是生成映射元素需要的地址,因此定義也就說明了它生成的是哪個映射元素的地址,而如果此時編譯器的映射表(即之前說的編譯器內部用於記錄映射元素的變數表、函數表等)中沒有那個映射元素,即還沒有相應元素的聲明出現過,那麼編譯器將報錯。
但前面只寫一個變數或函數定義語句,它照樣正常並沒有報錯啊?實際很簡單,只需要將聲明和定義看成是一種語句,只不過是向編譯器提供的信息不同罷了。如:void ABC( float );和void ABC( float ){},編譯器對它們相同看待。前者給出了函數的類型及類型名,因此編譯器就只填寫映射元素中的名字和類型兩欄。由於其後只接了個「;」,沒有給出此函數映射的代碼,因此編譯器無法填寫地址欄。而後者,給出了函數名、所屬類型以及映射的代碼(空的復合語句),因此編譯器得到了所有要填寫的信息進而將三欄的信息都填上了,結果就表現出定義語句完成了聲明的功能。
對於變數,如long a;。同上,這里給出了類型和名字,因此編譯器填寫了類型和名字兩欄。但變數對應的是棧上的某塊內存的首地址,這個首地址無法從代碼上表現出來(前面函數就通過在函數聲明的後面寫復合語句來表現相應函數對應的代碼所在的地址),而必須由編譯器內部通過計算獲得,因此才硬性規定上面那樣的書寫算作變數的定義,而要變數的聲明就需要在前面加extern。即上面那樣將導致編譯器進行內部計算進而得出相應的地址而填寫了映射元素的所有信息。
#p#副標題#e#
上面難免顯得故弄玄虛,那都是因為自定義類型的出現。考慮成員變數的定義,如:
struct ABC { long a, b; double c; };
上面給出了類型--long ABC::、long ABC::和double ABC::;給出了名字--ABC::a、ABC::b和ABC::c;給出了地址(即偏移)--0、4和8,因為是結構型自定義類型,故由此語句就可以得出各成員變數的偏移。上面得出三個信息,即可以填寫映射元素的所有信 struct ABC { void AB( float ); };
上面給出了類型--void ( ABC:: )( float );給出了名字--ABC::AB。不過由於沒有給出地址,因此無法填寫映射元素的所有信息,故上面是成員函數ABC::AB的聲明。按照前面說法,只要給出地址就可以了,而無需去管它是定義還是聲明,因此也就可以這樣:
struct ABC { void AB( float ){} };
上面給出類型和名字的同時,給出了地址,因此將可以完全填寫映射元素的所有信息,是定義。上面的用法有其特殊性,後面說明。注意,如果這時再在後面寫ABC::AB的定義語句,即如下,將錯誤:
struct ABC { void AB( float ){} };
void ABC::AB( float ) {}
上面將報錯,原因很簡單,因為後者只是定義,它只提供了ABC::AB對應的地址這一個信息,但映射元素中的地址欄已經填寫了,故編譯器將說重復定義。再單獨看成員函數的定義,它給出了類型void ( ABC:: )( float ),給出了名字ABC::AB,也給出了地址,但為什麼說它只給出了地址這一信息?首先,名字ABC::AB是不符合標識符規則的,而類型修飾符ABC::必須通過類型定義符「{}」才能夠加上去,這在前面已多次說明。因此上面給出的信息是:給出了一個地址,這個地址是類型為void ( ABC:: )( float ),名字為ABC::AB的映射元素的地址。結果編譯器就查找這樣的映射元素,如果有,則填寫相應的地址欄,否則報錯,即只寫一個void ABC::AB( float ){}是錯誤的,在其前面必須先通過類型定義符「{}」聲明相應的映射元素。這也就是前面說的定義僅僅填地址欄,並不生成映射元素。
聲明的作用
定義的作用很明顯了,有意義的映射(名字對地址)就是它來做,但聲明有什麼用?它只是生成類型對名字,為什麼非得要類型對名字?它只是告訴編譯器不要發出錯誤說變數或函數未定義?任何東西都有其存在的意義,先看下面這段代碼。
extern"C" long ABC( long a, long b );
void main(){ long c = ABC( 10, 20 ); }
假設上面代碼在a.cpp中書寫,編譯生成文件a.obj,沒有問題。但按照之前的說明,連接時將錯誤,因為找不到符號_ABC。因為名字_ABC對應的地址欄還空著。接著在VC中為a.cpp所在工程添加一個新的源文件b.cpp,如下書寫代碼。
extern"C" float ABC( float a ){ return a; }
編譯並連接,現在沒任何問題了,但相信你已經看出問題了--函數ABC的聲明和定義的類型不匹配,卻連接成功了?
注意上面關於連接的說明,連接時沒有類型,只管符號。上面用extern"C"使得a.obj要求_ABC的符號,而b.cpp提供_ABC的符號,剩餘的就只是連接器將b.obj中_ABC對應的地址放到a.obj以完善a.obj,最後連接a.obj和b.obj。
那麼上面什麼結果,由於需要考慮函數的實現細節,這在《C++從零開始(十五)》中再說明,而這里只要注意到一件事:編譯器即使沒有地址也依舊可以生成代碼以實現函數操作符的功能--函數調用。之所以能這樣就是因為聲明時一定必須同時給出類型和名字,因為類型告訴編譯器,當某個操作符涉及到某個映射元素時,如何生成代碼來實現這個操作符的功能。也就是說,兩個char類型的數字乘法和兩個long類型的數字乘法編譯生成的代碼不同;對long ABC( long );的函數調用代碼和void ABC( float )的不同。即,操作符作用的數字類型的不同將導致編譯器生成的代碼不同。
那麼上面為什麼要將ABC的定義放到b.cpp中?因為各源文件之間的編譯是獨立的,如果放在a.cpp,編譯器就會發現已經有這么個映射元素,但類型卻不匹配,將報錯。而放到b.cpp中,使得由連接器來完善a.obj,到時將沒有類型的存在,只管符號。下面繼續。
struct ABC { long a, b; void AB( long tem1, long tem2 ); void ABCD(); };
void main(){ ABC a; a.AB( 10, 20 ); }
由上面的說法,這里雖然沒有給出ABC::AB的定義,但仍能編譯成功,沒有任何問題。仍假設上面代碼在a.cpp中,然後添加b.cpp,在其中書寫下面的代碼。
struct ABC { float b, a; void AB( long tem1, long tem2 ); long ABCD( float ); };
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
這里定義了函數ABC::AB,注意如之前所說,由於這里的函數定義僅僅只是定義,所以必須在其前面書寫類型定義符「{}」以讓編譯器生成映射元素。但更應該注意這里將成員變數的位置換了,這樣b就映射的是0而a映射的是4了,並且還將a、b的類型換成了float,更和a.cpp中的定義大相徑庭。但沒有任何問題,編譯連接成功,a.AB( 10,20 );執行後a.a為0X41A00000,a.b為0X41200000,而*( float* )a.a為20,*( flaot* )a.b為10。
為什麼?因為編譯器只在當前編譯的那個源文件中遵循類型匹配,而編譯另一個源文件時,編譯其他源文件所生成的映射元素全部無效。因此聲明將類型和名字綁定起來,而名字就代表了其所關聯的類型的地址類型的數字,而後繼代碼中所有操作這個數字的操作符的編譯生成都將受這個數字的類型的影響。即聲明是告訴編譯器如何生成代碼的,其不僅僅只是個語法上說明變數或函數的語句,它是不可或缺的。
還應注意上面兩個文件中的ABC::ABCD成員函數的聲明不同,而且整個工程中(即a.cpp和b.cpp中)都沒有ABC::ABCD的定義,卻仍能編譯連接成功,因為聲明並不是告訴編譯器已經有什麼東西了,而是如何生成代碼。
頭文件上面已經說明,如果有個自定義類型ABC,在a.cpp、b.cpp和c.cpp中都要使用它,則必須在a.cpp、b.cpp和c.cpp中,各自使用ABC之前用類型定義符「{}」重新定義一遍這個自定義類型。如果不小心如上面那樣在a.cpp和b.cpp中寫的定義不一樣,則將產生很難查找的錯誤。為此,C++提供了一個預編譯指令來幫忙。
預編譯指令就是在編譯之前執行的指令,它由預編譯器來解釋執行。預編譯器是另一個程序,一般情況,編譯器廠商都將其合並進了C++編譯器而只提供一個程序。在此說明預編譯指令中的包含指令--#include,其格式為#include 文件名。應注意預編譯指令都必須單獨佔一行,而文件名就是一個用雙引號或尖括弧括起來的文件名,如:#include "abc.c"、#include "C:abc.dsw"或#include C:abc.exe。它的作用很簡單,就是將引號或尖括弧中書寫的文件名對應的文件以ANSI格式或MBCS格式(關於這兩個格式可參考《C++從零開始(五)》)解釋,並將內容原封不動地替換到#include所在的位置,比如下面是文件abc的內容。
struct ABC { long a, b; void AB( long tem1, long tem2 ); };
則前面的a.cpp可改為:
#include "abc"
void main() { ABC a; a.AB( 10, 20 ); }
而b.cpp可改為:
#include "abc"
void ABC::AB( long tem1, long tem2 ){ a = tem1; b = tem2; }
這時,就不會出現類似上面那樣在b.cpp中將自定義類型ABC的定義寫錯了而導致錯誤的結果(a.a為0X41A00000,a.b為0X41200000),進而a.AB( 10, 20 );執行後,a.a為10,a.b為20。
注意這里使用的是雙引號來括住文件名的,它表示當括住的只是一個文件名或相對路徑而沒有給出全路徑時,如上面的abc,則先搜索此時被編譯的源文件所在的目錄,然後搜索編譯器自定的包含目錄(如:C:Program FilesMicrosoft Visual Studio .NET 2003Vc7include等),裡面一般都放著編譯器自帶的SDK的頭文件(關於SDK,將在《C++從零開始(十八)》中說明),如果仍沒有找到,則報錯(注意,一般編譯器都提供了一些選項以使得除了上述的目錄外,還可以再搜索指定的目錄,不同的編譯器設定方式不同,在此不表)。
如果是用尖括弧括起來,則表示先搜索編譯器自定的包含目錄,再源文件所在目錄。為什麼要不同?只是為了防止自己起的文件名正好和編譯器的包含目錄下的文件重名而發生沖突,因為一旦找到文件,將不再搜索後繼目錄。
所以,一般的C++代碼中,如果要用到某個自定義類型,都將那個自定義類型的定義分別裝在兩個文件中,對於上面結構ABC,則應該生成兩個文件,分別為ABC.h和ABC.pp,其中的ABC.h被稱作頭文件,而ABC.cpp則稱作源文件。頭文件里放的是聲明,而源
文件中放的是定義,則ABC.h的內容就和前面的abc一樣,而ABC.cpp的內容就和b.cpp一樣。然後每當工程中某個源文件里要使用結構ABC時,就在那個源文件的開頭包含ABC.h,這樣就相當於將結構ABC的所有相關聲明都帶進了那個文件的編譯,比如前面的a.cpp就通過在開頭包含abc以聲明了結構ABC。
為什麼還要生成一個ABC.cpp?如果將ABC::AB的定義語句也放到ABC.h中,則a.cpp要使用ABC,c.cpp也要使用ABC,所以a.cpp包含ABC.h,由於裡面的ABC::AB的定義,生成一個符號?AB@ABC@@QAEXJJ@Z(對於VC);同樣c.cpp的編譯也
❾ 簡述一下編譯器和鏈接器的作用
1、編譯器:
編譯器對源文件進行編譯,就是把源文件中的文本形式存在的源代碼翻譯成機器語言形式的目標文件的過程,在這個過程中,編譯器會進行一系列的語法檢查。如果編譯通過,就會把對應的CPP轉換成OBJ文件。
2、鏈接器:
當鏈接器進行鏈接的時候,首先決定各個目標文件在最終可執行文件里的位置。然後訪問所有目標文件的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執行文件上的起始地址)。
然後遍歷所有目標文件的未解決符號表,並且在所有的導出符號表裡查找匹配的符號,並在未解決符號表中所記錄的位置上填寫實現地址。最後把所有的目標文件的內容寫在各自的位置上,再作一些另的工作,就生成一個可執行文件。