程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 你好,C++(4)2.1.3 我的父親母親:編譯器和鏈接器 2.1.4 C++程序執行背後的故事,2.1.32.1.4

你好,C++(4)2.1.3 我的父親母親:編譯器和鏈接器 2.1.4 C++程序執行背後的故事,2.1.32.1.4

編輯:C++入門知識

你好,C++(4)2.1.3 我的父親母親:編譯器和鏈接器 2.1.4 C++程序執行背後的故事,2.1.32.1.4


2.1.3  我的父親母親:編譯器和鏈接器

從表面上看,我是由Visual Studio創建的,而實際上,真正負責編譯源代碼創建生成可執行程序HelloWorld.exe的卻是Visual Studio中集成的C++編譯器cl.exe和鏈接器link.exe。他們二老,才是我的親生爹媽。

為了便於人們的編寫、閱讀和維護,我們的源文件是使用C++這種人們可以理解的高級程序設計語言編寫的。然而,計算機卻並不理解這種高級語言,也就無法直接執行高級語言編寫而成的源文件。所以,這裡就需要一個翻譯的工作,將源文件中人們可以理解的C++高級語言翻譯成機器可以理解執行的機器語言。我老爸編譯器實際上是個翻譯官,他的工作就是將用C++這種高級語言編寫的源文件(.cpp)翻譯成用計算機可以看懂的機器語言表示的目標文件(.obj),大家通常將這一過程稱為編譯。

在Visual Studio中,我老爸的名字是cl.exe,大家可以在開始菜單中找到“VS2012 開發人員命令提示”,然後在打開的DOS窗口中通過cl命令請他老人家出手,將一個cpp源文件編譯成相應的obj目標文件。比如,要想讓我爸將我的源文件HelloWorld.cpp編譯成對應的目標文件HelloWorld.obj,可以使用下面的命令:

cl /c /EHsc HelloWorld.cpp

其中,cl是調用編譯器的指令,其後的選項用於指定編譯器的編譯行為。這裡的“/c”表示只編譯不鏈接;“/EHsc”指定編譯器使用何種異常處理模型;最後一個選項HelloWorld.cpp則是即將要編譯的C++源文件。源文件HelloWorld.cpp經過我爸編譯器的編譯後,得到的還只是一個無法直接執行的目標文件HelloWorld.obj,還需要我媽鏈接器將這個目標文件和Visual C++所提供的標准庫目標文件(比如,libcpmt.lib)整合成最終的可執行文件(從標准庫目標文件中查找程序目標文件所用到的外部函數等符號,然後填寫到程序目標文件以生成最終的可執行文件),這一過程就被稱之為鏈接。在“VS2012 開發人員命令提示”中,大家可以用如下的命令請我媽鏈接器link.exe來完成這一鏈接過程:

link HelloWorld.obj

當然,整個編譯鏈接的工作,也可以由我爸編譯器cl.exe一個人完成:

cl /EHsc HelloWorld.cpp

經過我爸我媽的編譯鏈接過程,我從一個源文件(HelloWorld.cpp)變成了一個可執行文件(HelloWorld.exe),我就這樣哇哇墜地了。整個過程,如圖2-6所示:

                       

圖2-6 編譯鏈接過程

2.1.4  C++程序的執行過程

一旦生成可執行文件,就可以給操作系統下達指令讓文件開始執行。一個程序的執行是從其主函數開始的。但是在進入主函數開始執行之前,操作系統會幫我們做很多准備工作。比如,當操作系統接到執行某個程序的指令後,它首先要創建相應的進程並分配私有的進程空間;然後加載器會把可執行文件的數據段和代碼段映射到進程的虛擬內存空間中;操作系統接著會初始化程序中定義的全局變量等。做好這些准備工作,程序就可以進入主函數開始執行了。

進入主函數後,程序會按照源代碼給我制定的人生規劃,一條語句一條語句地往下執行,一步一步地往下走。大家一定還記得,我的源代碼是這樣的:

int main()
{
    // 在屏幕上輸出“Hello World!”字符串
    cout<<"Hello World!"<<endl;

    return 0;
}

從這裡可以看到,進入主函數後,我的第一條語句就是:

cout<<"Hello World!"<<endl;

這條語句的意思是讓我在DOS窗口中顯示“Hello World!”這樣一串文字,於是我便開始控制DOS窗口,在其中顯示這串文字,完成程序員通過這行代碼交給我的任務。

接下來的一條語句是:

return 0;

這條簡短的語句宣告了我人生歷程的結束。它表示主函數的結束,整個程序執行完畢。圖2-7所示的是我短暫而光輝的一生!

 

 圖2-7  Hello World程序短暫而光輝的一生

知道更多:C++程序執行背後的故事

在上面的例子中,我們看到一個C++程序的執行過程,是從main()函數開始逐條語句往下執行的。這個過程看起來非常簡單,但在每條語句的背後,都還有著更多的故事。

在Visual Studio調試模式下的反匯編視圖(在調試模式下通過Alt+8快捷鍵打開)中,我們可以看到C++程序中的各條語句所對應的匯編代碼。這下,程序中各條語句做了什麼事情、各個功能是如何實現的,都一目了然了。HelloWorld程序雖然只是簡單地輸出一個字符串,但是當我們把這個程序拆解開,卻可以發現它背後做了很多事情。在匯編視圖下的HelloWorld程序如下(匯編代碼太長,我們只保留其中的關鍵操作):

#include <iostream>

using namespace std;

int main()
{
    // 完成准備工作
00DC4EC0  push        ebp 
00DC4EC1  mov         ebp,esp 
00DC4EC3  sub         esp,0C0h 
// …   
00DC4EDC  rep stos    dword ptr es:[edi] 
// 完成任務   
    // 在屏幕輸出“Hello World!”字符串
    cout<<"Hello World!"<<endl;
00DC4EDE  mov         esi,esp 
00DC4EE0  mov         eax,dword ptr ds:[00DD031Ch] 
00DC4EE5  push        eax 
00DC4EE6  push        0DCCC70h 
00DC4EEB  mov         ecx,dword ptr ds:[0DD0318h] 
00DC4EF1  push        ecx 
// 調用標准庫中的操作符來完成任務
00DC4EF2  call        std::operator<<<std::char_traits<char> > (0DC12A3h) 
00DC4EF7  add         esp,8 
00DC4EFA  mov         ecx,eax 
00DC4EFC  call        dword ptr ds:[0DD0324h] 
00DC4F02  cmp         esi,esp 
00DC4F04  call        __RTC_CheckEsp (0DC132Ah) 
 
    return 0;
00DC4F09  xor         eax,eax 
}

當我們啟動一個程序後,操作系統會創建一個新的進程來執行這個程序。所謂進程,就是應用程序的一個實例。操作系統創建進程的時候,會為其分配一定的內存空間(默認堆),作為其私有的虛擬地址空間。通常,一個應用程序的執行對應於一個進程,進程負責管理這個程序運行時的一切事物,例如資源的分配與調度等等。但是,作為程序執行的調度者,它並不負責程序的執行,具體的執行工作,則是由它所創建的線程來完成的。每個進程都有一個主線程,如果是多線程應用程序,還可以有多個輔助線程。線程並不擁有資源(它使用的是它所屬進程的資源),但是它擁有自己的執行入口、執行的順序系列和一個執行終點。

在這裡,當負責執行這個程序的主線程被創建以後,它就會進入main()函數開始執行。它首先會執行一些初始化工作,例如保存現場環境、對堆進行初始化以及完成程序參數的傳遞等等,然後才是執行具體的程序代碼。雖然C++程序代碼只有一行,但是在匯編視圖下,卻被分解成了多個步驟來完成。主函數的執行,也不過是對於一些寄存器的操作和對庫函數的調用而已。例如,在main()函數的第一句就是用“push ebp”保存當前地址(在匯編代碼中,ebp代表了當前地址)。這裡我們一定會感到奇怪,為什麼在進入main()函數後的第一件事不是在C++程序代碼中看到的輸出一個字符串,而是保存當前地址呢?實際上,我們從程序代碼中所看到的只是我們對於要實現的功能的描述,而真正地要實現這些功能,C++程序還要在背後為我們完成很多事情。這裡的“push ebp”保存當前地址,就是為了讓這個main()函數在執行完畢後,可以順利返回原來的地址繼續往下執行。除了對於寄存器的操作(push、mov以及pop等匯編指令)之外,匯編代碼中更重要的是通過“call”指令完成的對其他函數的調用。例如,“call  __RTC_CheckEsp (0DC132Ah)”這個call指令就是調用__RTC_CheckEsp()函數(由編譯器在調試版本中添加)在程序執行完畢後檢查堆棧是否平衡。

在匯編視圖下,我們可以看到每一條C++語句後面都有故事。只有了解了每一條語句背後的故事,才能真正地理解這一條語句。這同樣也告訴我們,如果我們發現某條語句的行為出現了異常而我們又無法從代碼層面找到原因,我們就需要從這條語句的背後尋找真正的原因。

2.1.5  程序的兩大任務:描述數據與處理數據

人們編寫程序的目的,是為了用程序解決現實世界中的問題。人們觀察發現,所有這些問題都是以數據作為輸入,然後對這些數據進行處理,最後獲得結果數據而使問題得到解決的。所以,既然我是用來幫助人們解決問題的,那麼我的任務自然也就離不開對數據的描述和對數據的處理。如圖2-8所示。

人們用公式給我下了一個定義: 

數據 + 算法 = 程序

其中,數據可以看成是對現實世界中的各個事物的抽象和描述。例如,在C++程序中,我們將現實世界中的各種數據抽象成各種數據類型,比如我們將整數抽象成int類幸,將小數抽象成double類型等。然後反過來用這些類型定義的變量來描述我們在生活中遇到的某個具體的數據。比如,用int類型定義的變量nWidth來描述某個矩形的寬度;用string類型定義的變量strName來描述某個人的名字;我們甚至還可以創建自定義的數據類型來描述更加復雜的事物,比如我們可以創建一個Human數據類型來抽象“人”這個復雜事物,然後用它定義一個變量來描述某個具體的人。總之,用數據對現實世界中的事物進行抽象和描述,是我的第一個任務。

圖2-8  我的人生目的

用數據對現實世界進行描述並不是我的最終目的,我的最終目的是對這些數據進行處理,從而獲得想要的結果數據。比如,我們用nWidth和nHeight描述了一個矩形的寬和高,然而,這並不是我們想要的結果數據,我們想要的是矩形的面積。所以,我們還必須對nWidth和nHeight這兩個數據進行處理,用“*”符號計算兩個數的乘積,才能獲得我們想要的矩形面積。對數據處理過程的抽象,人們稱之為算法。而我的第二個任務,就是描述和表達算法,對數據進行處理以獲得最終結果。

知道更多:數據結構+算法=程序

“數據+算法=程序”這個等式是由著名的“數據結構+算法=程序”變形而的。它由Pascal之父、結構化程序設計的先驅Niklaus Wirth先生最先提出,它抽象地概括了一個程序的最核心內容是其中的用於表達數據的數據結構和對數據進行處理的算法。而我們在這裡提出的“數據+算法=程序”,則是具體地描述了一個程序由它要處理的數據以及對數據進行具體處理的算法共同組成。兩個等式都是正確的,只是描述程序的角度不同而已。

數據和算法伴隨我的一生。在小小的HelloWorld.exe中,也同樣有數據和算法的存在。例如,向屏幕輸出“Hello World!”的語句:

cout<<"Hello World!"<<endl;

其中,“Hello World!”是一個要向屏幕輸出的字符串數據。整個語句則代表了對這個字符串數據的處理:將字符串顯示到屏幕上。數據和算法總是這樣形影不離,成為我終身要完成的兩大任務。




  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved