1. 程式人生 > >C++程式編譯過程詳解

C++程式編譯過程詳解

C++程式編譯過程

一般來說,我們可以把C++程式編譯過程分為以下三步

編譯預處理

主要進行原始碼級別上的操作,前處理器執行原始碼中的預處理命令(以‘#’號開頭的語句),其中預處理命令可以分為以下幾類

a. 巨集定義命令[ #define 巨集名 替換內容 、#undef  巨集名]:進行程式碼替換, 凡是遇到識別符號為巨集名的都直接用“替換內容”進行替換。

b.條件編譯命令[ #if ...  、 #else 、 #elseif ...、 #ifdef ... 、#ifndef ... 、#endif 、#error messageStr]: 根據條件判斷來選取程式碼塊作為編譯程式輸入。

c.包含檔案命令[ #include<iostream>]: 用檔案內容替換這條命令。

d.預定義巨集名[ _LINE_、_DATE_、_TIME_、_FILE_]

e.預編譯模組[[#pragma]:一般被用作編譯器的拓展,用來設定編譯器狀態或指示編譯器完成特定動作,比如VC++的[#pragma once]用於防止標頭檔案被重複包含,[#pragma omp parallel for]用於VC++對OMP加速的支援。

e.特殊符號,比如#line, 如果在原始碼中出現,將會被解釋成當前行號。

編譯 優化 彙編

C++程式的編譯過程是分模組進行的,每一個模組獨立編譯,生成相應的.obj或.o檔案,一般情況下,每一個.c或.cpp檔案作為一個獨立的模組進行編譯。需要注意的是,.h檔案是不會被編譯的。這個編譯過程會檢查變數和函式是否有被宣告,進行詞法檢查,語法檢查、、、,生成彙編程式碼,優化彙編程式碼,最後經過彙編程式翻譯成目標機器程式碼,生成.obj或.o檔案。

***

因為每一個模組是獨立編譯的,所以對於定義在其它模組的函式或變數的使用都是未定義的,編譯時檢查到有宣告只是告訴模組--這個函式或變數定義在其它模組中了,當然這樣編譯後生成的檔案是無法直接執行的。因此,我們需要一個過程將各個編譯後的模組合理組合起來,並將各個模組中對其它模組的函式呼叫或變數引用設定到正確的地址,這個過程就叫做連結。連結完各個.obj或.o檔案後才能生成一個可執行檔案。

***

連結過程需要編譯時收集每一個模組的相關資訊才能完成,編譯每一個模組的同時還要生成三個表供連結過程使用:

(1 )匯出符號表:提供了本編譯單元具有定義,並且願意提供給其他編譯單元使用的符號及其地址。

(2)未解決符號表:提供了所有在該編譯單元裡引用但是定義並不在本編譯單元裡的符號及其出現的地址。

(3)地址重定向表:因為每個模組的邏輯地址都是從0開始的,這樣的地址是相對的,在連結成可執行檔案之前需要知道每個.obj檔案在可執行檔案中的起始位置,這樣(相應.obj的起始位置+匯出符號表中匯出符號對應的位置)才是一個正確的的地址。這樣,我們就需要告訴連結器,哪些地址是需要重定位的。這樣,連結器在進行連結時,首先會決定每一個.obj檔案在可執行檔案中的起始位置,然後檢視模組的地址重定向表,在需要重定向的位置上加上相應模組實際在可執行檔案中的起始位置,這樣的地址才是正確的地址。

***

對於每個模組,一般而言:

(1)用extern關鍵字修飾的符號是外部連結,它告訴編譯器,這個符號定義在其它模組,應該把這個符號放入未解決符號表。

(2)用static關鍵字修飾的符號都是內部連結符號,也就是說這些符號僅僅在模組內部可見,不會提供給其它模組引用,也就不會被放進匯出符號表。

(3)預設情況下,const常量是內部連結,不會被加到匯出符號表中。

(4 ) 預設情況下,函式和全域性變數都是外部連結符號,這些符號會被放入模組的匯出符號表,以供其它模組使用,但是可以用static關鍵字修飾,把它變成內部連結,這樣就不會被放進匯出符號表。

***?為什麼函式預設是外部連結?

答:

如果函式預設是內部連結,那麼大家會傾向於把函式連同其定義都放入標頭檔案中。然而,函式是多變的,可能會經常修改,這樣一來,所以包含它的模組都需要被重新編譯,很麻煩。另外一方面,如果函式中定義了靜態變數,這樣每一個包含該函式的模組都會有一個靜態變數(因為假設是預設內部連結),導致不一致。

***?為什麼const常量預設是內部連結而變數(全域性)預設是外部連結?

答:

因為它是常量,初始化後就不能改變,這樣即使每一個包含它的模組都有一份它的複製,那也不會導致不一致。如果變數預設是內部連結,它是可變的量,所以在每個包含它的模組中,它的值可能會被改變,從而導致不一致的狀況出現。

***?為什麼類的靜態資料成員不可以就地初始化?

答:

因為類體一般是放在標頭檔案中的,如果允許其靜態成員就地初始化,那就相當於允許在標頭檔案中定義變量了。

連結過程

連結器一般會一次做出如下動作

(1)決定每一個obj(模組)在可執行檔案中的位置。

(2)檢視每一個模組的重定向表,給需要重定向的值加上它所在模組在可執行檔案中的起始位置,形成正確的地址。

(3)檢查所有模組的匯出符號表,如果發現匯出符號有重複,會產生連結錯誤: duplicated external simbols...,然後停止連結過程。

(4)在匯出符號表中搜索未解決符號表中的符號,找到後會在相應位置填上正確地址,如果找不到,就會產生連結錯誤: unresolved external link...,然後停止連結過程。

 (5)完成連結過程,生成可執行檔案。

---------------------------------------------------------------------------------------------------------------------

C++編譯中的特殊情況:模板函式/模板類和行內函數的編譯

在瞭解C++程式一般編譯過程後,我們不難理解為什麼標頭檔案中一般只能放宣告而不能放定義,這是因為標頭檔案會被很多模組包含,如果標頭檔案中有定義,那麼連結這些包含這個標頭檔案的模組時就會出現符號重定義的錯誤了。

當然,我說的是一般情況,當然也有特殊情況,那就是行內函數和模板類,它們的定義是允許並且必須放在標頭檔案中的。

行內函數 --- 預設是內部連結----需要把定義一起放在標頭檔案中

用關鍵字inline修飾的函式稱為行內函數,就效果上來說,行內函數和巨集定義是一樣的,它會將函式呼叫用函式體進行程式碼替換,這樣省去了呼叫函式過程的開銷,對於頻繁使用並且短小精悍的函式來說,改成內聯,可以提高程式效率。使用行內函數要注意一下幾點:

(1)類體內實現的成員函式預設內聯。

(2)用inline修飾只是建議編譯器對函式使用內聯,至於內不內聯,由編譯器視函式實際情況來決定,如果函式語句很多,或函式中有迴圈,條件判斷等語句,編譯器一般不會內聯,如果行內函數的使用在行內函數定義之前,也不會內聯。

(3)inline是實現用的關鍵字,也就是說放在定義一起才有用,放在函式宣告處是沒用的。

*****?為什麼行內函數需要把定義一起放在標頭檔案中?

與巨集定義在預處理時就進行替換不同,行內函數的替換是在編譯時執行的,因為每個模組都是獨立編譯的,此時如果模組本身不知道函式的定義,也就無法內聯展開了,這就是為什麼要知道行內函數定義的原因。那麼就會存在問題了,標頭檔案中包含了定義,就不怕重定義嗎?大家不用擔心這個,編譯器會將行內函數視為內連結。

****?如果沒有把行內函數的定義一起放在標頭檔案裡會發生什麼?

會出現連結錯誤:Unresolved  external  simbols ...,這是因為在編譯時,因為找不到行內函數的定義而無法內聯展開,這時編譯器會認為它應該是在其它模組定義了。前面說過,行內函數是內部連結,所以這個函式符號在匯出符號表中找不到,於是會出現Unresolved連結錯誤。

模板類與模板函式 --- 需要把定義一起放在標頭檔案中

要理解模板類和模板函式的編譯過程,大家要記住以下幾點: (1)模板就是模板,它本身不能被編譯成二進位制程式碼,它的作用只是在編譯時根據型別生成相應程式碼而已。 (2)只有在模板函式(不管是普通的還是類的成員函式)被呼叫時,函式模板才會被例項化,也就是才會生成相應型別的函式以供呼叫。 (3)函式模板例項化時會增大原始檔程式碼量,並且生成的函式都是內連結。 和行內函數一樣,為了能在編譯時能生成相應實現程式碼,我們需要知道模板函式的定義,這也就要求模板類或模板函式的定義也要一起放在標頭檔案中。 ***?如果沒有把模板類或模板函式的定義一起放在標頭檔案中會怎樣? 如果包含它的模組呼叫了模板函式,那麼會出現Unresolved external simbols連結錯誤。因為如果編譯時,模組找不到模板函式的定義,它會認為這個函式肯定是定義在其它模組了,把這個問題留給連結程式去解決。當然,連結程式在匯出符號表中找不到這個函式(因為它壓根沒有被例項化過),所以出現以上錯誤。

相關推薦

C/C++程式編譯過程

C語言的編譯連結過程要把我們編寫的一個c程式(原始碼)轉換成可以在硬體上執行的程式(可執行程式碼),需要進行編譯和連結。編譯就是把文字形式原始碼翻譯為機器語言形式的目標檔案的過程。連結是把目標檔案、作業系統的啟動程式碼和用到的庫檔案進行組織,形成最終生成可執行程式碼的過程。過程圖解如下: 從圖上可以看到,

C++程式編譯過程

C++程式編譯過程 一般來說,我們可以把C++程式編譯過程分為以下三步 編譯預處理 主要進行原始碼級別上的操作,前處理器執行原始碼中的預處理命令(以‘#’號開頭的語句),其中預處理命令可以分為以下幾類 a. 巨集定義命令[ #define 巨集名 替換內容 、#undef

c語言編譯過程,預處理,編譯,彙編,連結(乾貨滿滿)

鍥子 我們在各自的電腦上寫下程式碼,得明白我們程式碼究竟是如何產生的,不想了解1,0什麼的,但這幾個環節必須掌握吧。 我們的程式碼會經過這4個環節,從而形成最終檔案,c語言作為編譯語言,用來向計算機發出指令。讓程式設計師能夠準確地定義計算機所需要使用的資料,並精確地定義在

C & C++的編譯過程 (整理在此方便以後)

C/C++編譯過程 C/C++編譯過程主要分為4個過程 1) 編譯預處理 2) 編譯、優化階段 3) 彙編過程 4) 連結程式 一、編譯預處理 (1)巨集定義指令,如#define Name TokenString,#undef等。 對於前一個偽指令,預編譯所要做的是

(轉)C++編譯過程

概念 1.編譯:把原始檔中的原始碼翻譯成機器語言,儲存到目標檔案中。如果編譯通過,就會把.C/.CPP轉換成.obj檔案(Windows系統)/.o(Linux系統)。 2.編譯單元:每個.c/.cpp就是一個編譯單元,每個編譯單元相互之間是獨立且相互不知的。一個編譯

c/c++預處理過程(二)之條件編譯及預定義的巨集

未經博主同意不得私自轉載!不準各種形式的貼上複製本文及盜圖! 首先對於上篇文章中巨集定義的補充: (1)#define NAME"zhangyuncong" 程式中有"NAME"則,它會不會被替換呢? (2)#define 0x abcd 可以嗎?也就是說,可不可以用不是

五.linux開發之uboot移植(五)——uboot配置和編譯過程

一.uboot主Makefile分析1 1、uboot version確定(Makefile的24-29行) Makefile程式碼部分 (1)uboo

nginx-1.15.5 windows下 64位版本編譯過程

編譯原始碼、工具、指令碼等和釋出在下面地址: 原始碼準備: 官網釋出的NGINX原始碼,不包含windows編譯部分,但是包含了主要的原始碼(片斷一): 下載地址:http://nginx.org/download/nginx-1.15.5.tar.gz windo

uboot移植之配置編譯過程4

/****************************************************************     uboot的連結指令碼:u-boot.lds     時間:2018年11月下旬  &

uboot移植之配置編譯過程3

/**************************************************************************************     2018.10下旬     針對三星官方為210移植過的

uboot移植之配置編譯過程2

/***********************************************************************************             2018.11月

uboot的移植之配置及編譯過程5

/***********************************************************************************     分析物件:原始碼主Makefile中進行配置時的一個重要的指令碼:mkconfig(MKCO

一個cpp檔案的編譯過程

一個CPP檔案的編譯過程 籠統的說一個CPP檔案的編譯過程就是以下幾步 Created with Raphaël 2.1.2預處理(做優化,生成.i檔案)編譯器(生成.s檔案)彙編器(生成.o檔案) 連結器(連線庫檔案和其他目的碼) 生成可執行檔案

Linux核心配置和編譯過程

一、引言:  本文件的內容大部份內容都是從網上收集而來,然後配合一些新的截 圖(核心版本:V2.4.19)。在每一配置項後會有一個選擇指南的部份,用來指導大家怎麼樣 根據自己的情況來做相應的選擇;還有在每一個大項和文件的最後會有一個經驗談,它是一些高手們在應對問題和處理特有

C程式編譯過程淺析

我現在一般都是用gcc,所以自然以GCC編譯hellworld為例,簡單總結如下。  hello.c原始碼如下: /* 何問起 hovertree.com */ int main() { printf(“Hello, world.\n”); return 0; } 通常我們使用gcc

C語言link過程

 詳解link   有些人寫C/C++(以下假定為C++)程式,對unresolved external link或者duplicated external simbol的錯誤資訊不知所措(因為這樣的錯誤資訊不能定位到某一行)。或者對語言的一些部分不知道為什麼要

c++程式編譯過程

編譯主要分為4個過程: 預處理-編譯優化-彙編-連結 1) 編譯預處理 預編譯程式完成的工作,可以說成是對源程式的“替換”工作。經過這個過程,生成一個沒有巨集定義、沒有條件編譯指令、沒有特殊符號的輸出檔案。 巨集定義命令;例如#define 條件編譯指

C/C++ 程式編譯與連結的過程(靜態連結)

我們知道一個程式的執行需要經過編譯和連結兩個階段,其過程究竟是怎樣的呢? 程式的編譯階段分為以下幾個步驟,分別是預編譯、編譯、彙編、生成二進位制可重定向檔案(.o)。 預編譯: 首先是原始碼檔案xxx.c和相關的標頭檔案被預編譯器編譯成一個.i檔案。

在linux中把彙編或c程式交叉編譯成二進位制檔案燒錄開發板過程

嵌入式開發中在宿主機中要編譯能在目標機中執行的程式檔案需要用到一種工具:交叉工具鏈。其中我們常用的交叉工具有以下幾種:交叉編譯器,交叉聯結器,交叉轉換器,交叉ELF檔案工具,交叉反彙編器。以下通過例項將這些做一些介紹。 分解以下講一個led.S檔案編譯成可燒錄的二進位制檔案

C/C++ 程式編譯預處理和條件編譯指令

** C/C++ 程式的編譯預處理和條件編譯指令詳解** 編譯預處理 (1)#include 包含指令作用為 將一個原始檔嵌入到當前原始檔中該點處。 #include<檔名> : 按標準方式搜尋,檔案位於C++系統目錄的include子目錄下 #include"檔名" :