前言
本文主要參考 Compiling Cpp - By Ubuntu,并經(jīng)過運(yùn)行測(cè)試完成。要自行測(cè)試請(qǐng)安裝好Gcc,:),windows下面用cygwin裝一下,mac下用包管理器homebrew裝一下,linux下應(yīng)該默認(rèn)已經(jīng)安裝了gcc。
C/C++程序的編譯其實(shí)主要有幾個(gè)階段:1.編譯預(yù)處理(包括include頭文件的復(fù)制,宏定義的展開處理等等),2.把預(yù)處理后的文件經(jīng)過語法(syntax)分析和語義(sematics)分析之后生成匯編文件,3.然后再利用匯編器和鏈接器(鏈接上動(dòng)態(tài)鏈接庫,插入靜態(tài)鏈接庫代碼等等)生成可執(zhí)行文件,比如Linux下就是elf格式的文件。
本文所用的指令(可執(zhí)行程序)為g++,而非gcc,因?yàn)?code>g++自動(dòng)鏈接了C++標(biāo)準(zhǔn)庫的動(dòng)態(tài)鏈接庫libstdc++.so,就像gcc自動(dòng)鏈接了C標(biāo)準(zhǔn)庫的動(dòng)態(tài)鏈接庫libglibc.so一樣。
Gcc這個(gè)編譯工具的一些常識(shí)
然后在實(shí)際寫小程序編譯時(shí)候,如果編譯器版本大于gcc-4.8,我建議大家加上compiler flag -std=c++11;如果編譯器版本大于gcc5.x,我建議大家加上compiler flag -std=c++14,因?yàn)樽罱吹揭粋€(gè)youtube的視頻 C++之父在CppCon2016上的KeyNote The Evolution of C++ Past, Present and Future , C++之父說目前建議大家學(xué)習(xí)C++的baseline至少為C++11標(biāo)準(zhǔn)。
另外請(qǐng)加上-wall這個(gè)compiler flag,讓編譯器報(bào)出所有可能的警告;然后如果你需要獲得Simd(single instruction multiple data)俗稱向量話還有其他的一些編譯優(yōu)化效果的話請(qǐng)加上-O3;然后如果你要調(diào)試程序的話,請(qǐng)加上-g,保留符號(hào)表信息。
C/C++編譯涉及的文件命名規(guī)范
這一部分參考了 Compiling Cpp - By Ubuntu 這篇Wiki的內(nèi)容。文件命名規(guī)范如所示(包括文件后綴名,文件類型和補(bǔ)充說明):
-
.h--- C/C++頭文件- Boost里面是.hpp
-
<xxx>,無后綴--- C++標(biāo)準(zhǔn)庫頭文件- 比如寫C程序時(shí)候
#include<string.h>,那么在C++中就是#include<cstring>,這里所示的xxx里面可以include C風(fēng)格的頭文件,比如在cstring這個(gè)文件名對(duì)應(yīng)的文件里面,我們#include<string.h>
- 比如寫C程序時(shí)候
-
.c .cpp--- C/C++源代碼(這些后綴也允許:.C .cc .cp .cxx .c++)- 需要編譯預(yù)處理
-
.ii--- C++源代碼- 不需編譯預(yù)處理,已經(jīng)經(jīng)過預(yù)處理復(fù)制了include頭文件里面的內(nèi)容,展開了宏定義的內(nèi)容
-
.s--- 匯編代碼文件- 語法和語義分析后的輸出
-
.o--- 對(duì)象文件- 經(jīng)過鏈接后才生成可執(zhí)行文件
-
.a--- 靜態(tài)庫 (archive)- 靜態(tài)鏈接庫,鏈接后會(huì)增加可執(zhí)行文件的長(zhǎng)度
-
.so--- 動(dòng)態(tài)庫(shared object)- 動(dòng)態(tài)鏈接庫,庫函數(shù)推遲到程序運(yùn)行時(shí)期載入。用戶只需要升級(jí)動(dòng)態(tài)鏈接庫,而無需重新編譯鏈接其他原有的代碼就可以完成整個(gè)程序的升級(jí)。
Gcc-生成可執(zhí)行文件-給個(gè)直觀感受(Hello-World)
- hello_world.cpp的源代碼
#include <iostream>
int main(int argc,char *argv[])
{
std::cout << "hello, world" << std::endl;
return(0);
}
- 然后build_hello_world.sh的腳本
mkdir -p build
g++ hello_world.cpp -o build/hello_world_elf_file
- 然后給我們的build_hello_world.sh腳本執(zhí)行權(quán)限并運(yùn)行
chmod +x build_hello_world.sh
./build_hello_world.sh
build/hello_world_elf_file
- 運(yùn)行可執(zhí)行文件的結(jié)果是熟悉的hello, world
hello, world
Gcc-編譯預(yù)處理(Hello-World)
- 在寫好了上一小節(jié)的
hello_world.cpp后,我們只需要一行命令就可以產(chǎn)生預(yù)處理文件,命令如下:
g++ -E hello_world.cpp -o hello_world.ii
引用下 Compiling Cpp - By Ubuntu 的一段話:
選項(xiàng) -E 使 g++ 將源代碼用編譯預(yù)處理器處理后不再執(zhí)行其他動(dòng)作。
本文前面所列出的 helloworld.cpp 的源代碼,僅僅有六行,而且該程序除了顯示一行文字外什么都不做,但是,預(yù)處理后的版本將超過 1200 行。這主要是因?yàn)轭^文件 iostream 被包含進(jìn)來,而且它又包含了其他的頭文件,除此之外,還有若干個(gè)處理輸入和輸出的類的定義。
- 查看在我的mac筆記本上生成的代碼行數(shù),輸出結(jié)果為37586,可見預(yù)處理的時(shí)候展開了很多內(nèi)容和include了很多內(nèi)容(復(fù)制頭文件中所有的內(nèi)容到當(dāng)前預(yù)處理文件的輸出文件,比如
hello_world.ii)
grep -c "" build/hello_world.ii
Gcc-生成匯編代碼(Hello-World)
引用下 Compiling Cpp - By Ubuntu 的一段話:
選項(xiàng) -S 指示編譯器將程序編譯成匯編語言,輸出匯編語言代碼而後結(jié)束。下面的命令將由 C++ 源碼文件生成匯編語言文件 helloworld.s:
g++ -S hello_world.cpp -o build/hello_world.s
- 產(chǎn)生出來的匯編代碼有點(diǎn)長(zhǎng),我就不粘貼上來了。
Gcc-處理多個(gè)源文件(Multiple-File-Hello-World)
- 主要包含三個(gè)文件,一個(gè)頭文件(hello_world_util.h),一個(gè)類文件(hello_world_util.cpp),另一個(gè)文件包含main方法(hello_world_main.cpp)
- hello_world_util.h
#include <iostream>
class SayUtil
{
public:
void sayStr(const char *);
};
- hello_world_util.cpp
#include "hello_world_util.h"
void SayUtil::sayStr(const char *str)
{
std::cout << str << "\n";
}
- hello_world_main.cpp
#include "hello_world_util.h"
int main(int argc, char const *argv[]) {
SayUtil say_util;
say_util.sayStr("hello, world");
return 0;
}
- build的時(shí)候,先生成
.o文件,然后鏈接產(chǎn)生可執(zhí)行文件,然后刪除中間文件,下面是我的build_mutiple_file_hello_world0.sh腳本內(nèi)容:
mkdir -p build
g++ -c hello_world_util.cpp -o build/hello_world_util.o
g++ -c hello_world_main.cpp -o build/hello_world_main.o
#link and generate executable
g++ build/hello_world_util.o build/hello_world_main.o -o build/multiple_file_hello_world0
#remove intermediate files
rm build/hello_world_util.o build/hello_world_main.o
- 然后也可以讓g++幫助我們來寫中間的生成
.o文件的過程,下面是我的build_mutiple_file_hello_world1.sh腳本內(nèi)容:
mkdir -p build
g++ hello_world_main.cpp hello_world_util.cpp -o build/multiple_file_hello_world1
Gcc-創(chuàng)建靜態(tài)鏈接庫
引用下 Compiling Cpp - By Ubuntu 的一段話:
靜態(tài)庫是編譯器生成的一系列對(duì)象文件的集合。鏈接一個(gè)程序時(shí)用庫中的對(duì)象文件還是目錄中的對(duì)象文件都是一樣的。庫中的成員包括普通函數(shù),類定義,類的對(duì)象實(shí)例等等。靜態(tài)庫的另一個(gè)名字叫歸檔文件(archive),管理這種歸檔文件的工具叫 ar 。
在講靜態(tài)鏈接庫之前,我主要先提一下幾個(gè)注意點(diǎn):
- 編譯器是比較笨的,所以如果我們需要在一個(gè)cpp文件中使用另一個(gè)cpp文件中的一個(gè)全局變量的時(shí)候,我們必須要通過
extern 類名 對(duì)象名;告訴編譯器這個(gè)對(duì)象名已經(jīng)在其他編譯單元中被定義了(分配了stack上的一個(gè)空間了)。比如,一個(gè)文件中定義了Say librarysay("Library instance of Say");,也就是在進(jìn)程的棧上分配了空間,并進(jìn)行了初始化;我們?cè)诹硪粋€(gè)文件中先得通過extern Say librarysay;聲明一下,表示這個(gè)librarysay對(duì)象在其他的編譯單元中被定義過了,我們后來要用這個(gè)全局變量,告訴編譯器一下,不要報(bào)錯(cuò)。這邊的extern必須要加上,因?yàn)榉駝t編譯器會(huì)認(rèn)為我們要在進(jìn)程的棧上定義一個(gè)名為librarysay的變量,調(diào)用默認(rèn)構(gòu)造函數(shù)。 - 然后就是其他編譯單元的全局函數(shù)使用了,我們必須先聲明一下這個(gè)函數(shù),表示我們要使用他,這個(gè)函數(shù)可以是在其他編譯單元進(jìn)行定義的,只要最后鏈接成可執(zhí)行文件的時(shí)候,我們能找到函數(shù)的定義,編譯器就不會(huì)報(bào)錯(cuò)了。然后。比如我
void sayhello(void);這個(gè)函數(shù)的定義在其他編譯單元,但我要使用,那么我就直接void sayhello(void);聲明一下,告訴編譯器這個(gè)東西我要用,然后是在其他編譯單元定義的。聲明這個(gè)函數(shù)的時(shí)候extern好像可以不加,比如void sayhello(void);和extern void sayhello(void);在我編譯測(cè)試運(yùn)行的時(shí)候都可以。因?yàn)檫@和上一點(diǎn)的聲明對(duì)象不同,函數(shù)沒有構(gòu)造語義學(xué)。
然后就是例子中要用到的4個(gè)文件了, 其中前三個(gè)會(huì)編譯成.o文件,然后通過ar打包到靜態(tài)鏈接庫,另一個(gè)是包含主方法的文件:
- say_util.h
#include <iostream>
extern void sayhello(void);
class Say {
private:
char *string;
public:
Say(char *str) { string = str; }
void sayOther(const char *str) {
std::cout << str << " from \"" << string << "\"" << std::endl;
}
void sayString(void);
};
- say_util.cpp
#include "say_util.h"
void Say::sayString() { std::cout << string << std::endl; }
// For built-in-library usage
Say librarysay("Library instance of Say");
- say_hello_func.cpp
#include <iostream>
void sayhello() { std::cout << "hello from a static library" << std::endl; }
- say_hello_main.cpp
#include "say_util.h"
int main(int argc, char *argv[]) {
//告訴編譯器這個(gè)Say類型的變量(symbol)
// librarysay在其他的編譯單元中,可作為全局變量使用
extern Say librarysay;
Say localsay = Say("Local instance of Say");
sayhello();
librarysay.sayOther("say Something from librarysay");
librarysay.sayString();
localsay.sayString();
return (0);
}
- build_say_hello_with_archive.sh腳本內(nèi)容,主要三步走:1.生成
.o文件 2. 通過ar打包成靜態(tài)鏈接庫 3. 然后再通過鏈接器鏈接起來,搞成一個(gè)可執(zhí)行文件。
mkdir -p build
g++ -c say_util.cpp -o build/say_util.o
g++ -c say_hello_func.cpp -o build/say_hello_func.o
#程序 ar 配合參數(shù) -r 創(chuàng)建一個(gè)新庫 libsay.a 并將命令行中列出的對(duì)象文件插入。
#采用這種方法,如果庫不存在的話,參數(shù) -r 將創(chuàng)建一個(gè)新的庫,而如果庫存在的話,將用新的模塊替換原來的模塊。
ar -r build/libsay.a build/say_util.o build/say_hello_func.o
g++ say_hello_main.cpp build/libsay.a -o build/say_hello_main
rm build/*.o
Gcc-創(chuàng)建動(dòng)態(tài)鏈接庫
引用下 Compiling C - By Ubuntu 的一段話:
共享庫是編譯器以一種特殊的方式生成的對(duì)象文件的集合。對(duì)象文件模塊中所有地址(變量引用或函數(shù)調(diào)用)都是相對(duì)而不是絕對(duì)的,這使得共享模塊可以在程序的運(yùn)行過程中被動(dòng)態(tài)地調(diào)用和執(zhí)行。
基于這一點(diǎn),我們?cè)谏?code>.o文件的時(shí)候,就得通過添加編譯flag
-fpic的形式告訴編譯器:生成的對(duì)象模塊采用浮動(dòng)的(可重定位的)地址。關(guān)于生成
.so文件(動(dòng)態(tài)鏈接庫文件的說明),再引用下 Compiling C - By Ubuntu 的一段話:
選項(xiàng) -o 用來為輸出文件命名,而文件後綴名 .so 告訴編譯器將對(duì)象文件鏈接成一個(gè)共享庫。通常情況下,鏈接器定位并使用 main() 函數(shù)作為程序的入口,但是本例中輸出模塊中沒有這種入口點(diǎn),為抑制錯(cuò)誤選項(xiàng) -shared 是必須的。
- 下面我將使用上一小節(jié)靜態(tài)鏈接庫中的源代碼,只不過把靜態(tài)鏈接庫改為使用動(dòng)態(tài)鏈接庫,來展示動(dòng)態(tài)鏈接庫的使用。值得注意的一點(diǎn)是,在使用動(dòng)態(tài)鏈接庫時(shí)候,
.so文件中的代碼會(huì)在運(yùn)行時(shí)候加載進(jìn)來,而不是像.a文件在編譯時(shí)候加載進(jìn)來,所以我們必須要設(shè)置環(huán)境變量,以供我們的系統(tǒng)在執(zhí)行可執(zhí)行文件時(shí)候能找到對(duì)應(yīng)的.so文件,比如先使用如下指令添加查找動(dòng)態(tài)鏈接庫的目錄為./build(當(dāng)前目錄的build文件夾目錄):
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./build/
- build_say_hello_with_shared_object.sh的腳本如下:
mkdir -p build
#添加-fpic 這個(gè)compiler flag,告訴編譯器,
#生成的對(duì)象模塊采用浮動(dòng)的(可重定位的)地址??s微詞 pic 代表“位置無關(guān)代碼”(position independent code)
g++ -c say_util.cpp -fpic -o build/say_util.o
g++ -c say_hello_func.cpp -fpic -o build/say_hello_func.o
g++ -shared build/say_util.o build/say_hello_func.o -o build/libsay.so
rm build/*.o
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./build/
g++ say_hello_main.cpp build/libsay.so -o build/say_hello_main_with_so
最后:我的實(shí)驗(yàn)代碼和腳本Github鏈接
- 鏈接地址
- 具體文件信息可以參看所給鏈接目錄下
ReadMe.md中的說明