2010-07-09 205 views
2

我不正確理解C++程序的編譯和鏈接。有沒有辦法,我可以看看通過編譯C++程序(以可理解的格式)生成的目標文件。這應該有助於我理解目標文件的格式,如何編譯C++類,編譯器需要什麼信息來生成目標文件,並幫助我理解如下語句:需要幫助瞭解C++程序的編譯

如果一個類只用作輸入參數和返回類型,我們不需要包含整個類頭文件。前向聲明就足夠了,但是如果派生類從基類派生,我們需要包含包含基類定義的文件(取自「Exceptional C++」)。

我正在閱讀「鏈接和加載」一書來理解目標文件的格式,但我更喜歡專門爲C++源代碼量身打造的東西。

感謝,

Jagrati

編輯:

我知道,與納米我可以看看目前在目標文件的符號,但我想知道更多有關目標文件。

+4

我不認爲看對象文件將有助於理解你提到的觀點。思考「編譯器需要知道如何爲這個輸入創建機器代碼」這個問題可能更有用?例如,要將'A * a'傳遞給下一個函數,編譯器不需要知道A是什麼樣子,而是調用'a-> foo()',它就是這樣。從「A」派生,至少需要知道「A」的大小和所有方法簽名。 – 2010-07-09 07:46:04

+0

嗨克里斯托弗,我同意你的觀點。事實上,這就是我想從哪裏開始。但是,即使像「從A派生出來的東西,至少需要知道A的大小」對我來說也不是那麼明顯。爲什麼不能將A的大小知識推遲到說出運行時間,或者說在鏈接時間,而不是在創建目標文件時的編譯時間。因此,我得出結論,我可能需要從不同的角度理解編譯器在目標文件中放置什麼信息。 – xyz 2010-07-09 07:54:45

+1

我認爲Stanley B Lippman的書:「C++內部對象模型」可以幫助你理解一些主題 – 2010-07-09 08:50:04

回答

0

你有沒有試過用readelf(假設你在Linux平臺上)檢查你的二進制文件?這提供了有關ELF對象文件的非常全面的信息。老實說,雖然我不確定這對理解編譯和鏈接有多大幫助,但是我並不確定這會有多大幫助。我認爲正確的方法可能是掌握C++代碼如何映射到程序集的前後鏈接。

0

您通常不需要詳細瞭解Obj文件的內部格式,因爲它們是爲您生成的。所有你需要知道的是,對於你創建的每一個類,編譯器都會生成Obj文件,它是你的類的二進制字節碼,適合於你編譯的操作系統。然後,下一步 - 鏈接 - 將您的程序所需的所有類的目標文件放在一個EXE或DLL中(或其他任何非Windows OS-es的格式)。也可以是EXE +幾個DLL,這取決於你的意願。

最重要的是你分開你的類的接口(聲明)和實現(定義)。

總是隻放在你的類的頭文件接口聲明。沒有別的 - 這裏沒有實現。避免使用自定義類型的成員變量,這些類型不是指針,因爲對於它們來說,前向聲明是不夠的,你需要在頭文件中包含其他頭文件。如果你的標題包含了,那麼設計就會有氣味,並且會減慢構建過程。

類方法或其他函數的所有實現應該在CPP文件中。這將保證當有人包含您的頭文件時,編譯器生成的Obj文件將不再需要,並且您只能從CPP文件中包含其他文件。

但爲什麼要麻煩?答案是,如果你有這樣的分隔,那麼鏈接速度會更快,因爲每個類都使用了每個Obj文件。另外,如果你改變了你的類,這將在下一次構建時改變其他一些對象文件。

如果你在頭文件中包含了,這意味着當編譯器爲你的類生成Obj文件時,它應該首先爲你的頭文件中包含的其他類生成Obj文件,這可能需要其他Obj文件等等。甚至可能是循環依賴,然後你不能編譯!或者如果你改變了你的類中的某些東西,那麼編譯器將需要重新生成許多其他的Obj文件,因爲如果你沒有分離,它們會在一段時間後變得非常緊密依賴。

+0

RE:「避免使用自定義類型也是成員變量」 - 如何避免使用原始指針的W/O?我猜想會是聰明的指針,但任何其他的想法? – msi 2010-07-09 07:55:53

+0

@msiemeri:無論如何,我認爲建議是誇大了。您可能希望在少數情況下這樣做來打破依賴週期,但這與一般建議不同。是的,在這種情況下,應該使用scoped_ptr或類似的。 – peterchen 2010-07-09 08:26:07

0

nm是一個unix工具,它將顯示對象文件中符號的名稱。

objdump是一個GNU工具,它會告訴你更多的信息。

但是,這兩種工具都會顯示鏈接器使用的非常原始的信息,但不是爲人類設計的。這可能不會幫助你更好地理解在C++級別發生的事情。

1

第一件事,第一件事。反彙編編譯器輸出很可能不會以任何方式幫助您理解您的任何問題。編譯器的輸出不再是一個C++程序,而是簡單的彙編,如果你不知道內存模型是什麼,那麼閱讀起來就非常棘手。

論爲什麼是base所需的定義,當你宣稱它是一個基類的derived有幾個不同的原因(也可能更多,我忘了)的特殊問題:

  1. 當創建derived類型的對象,編譯器必須保留內存爲全實例和所有子類:它必須知道的base
  2. 大小當你訪問一個成員屬性的編譯器必須知道從隱含this指針偏移量,該抵消需要知道第th所採用的大小e base子對象。
  3. 當在derived的上下文中分析標識符並且在derived類中找不到標識符時,編譯器必須在查找封閉名稱空間中的標識符之前知道它是否在base中定義。如果在base類中聲明foo(),編譯器無法知道foo();derived::function()內是否爲有效呼叫。
  4. 當編譯器定義derived類時,必須知道在base中定義的所有虛函數的編號和簽名。它需要這些信息來建立動態調度機制 - 通常是vtable--,甚至要知道derived中的成員函數是否被綁定爲動態調度 - 如果base::f()是虛擬的,那麼derived::f()將是虛擬的,不管是否爲derived中的聲明具有virtual關鍵字。
  5. 多重繼承增加了一些其他要求 - 比如每個baseX必須在調用方法的最終重寫器之前重寫的相對偏移量(類型爲base2的指針指向multiplyderived的對象並不指向實例,但對base2子對象的情況下,這可能是由繼承列表base2之前宣佈的其他基地可以抵消年初

最後一個問題的意見:

因此,對象(除了全局對象)的實例化可以等到運行時,因此大小和偏移等可以等到鏈接時間,並且在生成目標文件時我們不一定要處理它。

void f() { 
    derived d; 
    //... 
} 

前面的代碼分配和在堆棧derived類型的對象。編譯器將添加彙編指令以爲堆棧中的對象保留一定量的內存。編譯器解析並生成程序集後,沒有對象的蹤跡,特別是(假設POD類型的一個簡單的構造函數:即沒有被初始化),該代碼和void f() { char array[ sizeof(derived) ]; }將生成完全相同的彙編程序。當編譯器生成將保留空間的指令時,它需要知道多少。