要学好 C 语言 / C++ ,Makefile 可少不了
一、Makefile 簡介
1. Makefile 是什么?
Makefile 通常指的是一個含有一系列命令(directive)的,通過 Make 自動化編譯工具,幫助 C/C++ 程序?qū)崿F(xiàn)自動編譯目標文件的文件。這個文件的默認命名是 “Makefile”。
2. 為什么要使用 Makefile?
Makefile 文件描述了整個工程的編譯、鏈接的規(guī)則。
為工程編寫 Makefile 的好處是能夠使用一行命令來完成“自動化編譯”。只需提供一個(通常對于一個工程來說會是多個)正確的 Makefile,接下來每次的編譯都只需要在終端輸入“make”命令,整個工程便會完全自動編譯,極大提高了效率。尤其是在編譯一個僅有一小部分文件被改動過的大項目的情況下。
絕大多數(shù)的 IDE 開發(fā)環(huán)境都會為用戶自動編寫 Makefile。
3. Make 是怎么工作的?
Make 工作的原則就是:
一個目標文件當(dāng)且僅當(dāng)在其依賴文件(dependencies)的更改時間戳比該目標文件的創(chuàng)建時間戳新時,這個目標文件才需要被重新編譯。
Make 工具會遍歷所有的依賴文件,并且把它們對應(yīng)的目標文件進行更新。編譯的命令和這些目標文件及它們對應(yīng)的依賴文件的關(guān)系則全部儲存在 Makefile 中。
Makefile 中也指定了應(yīng)該如何創(chuàng)建,創(chuàng)建出怎么樣的目標文件和可執(zhí)行文件等信息。
除此之外,你甚至還可以在 Makefile 中儲存一些你想調(diào)用的系統(tǒng)終端的命令,像一個 Shell 腳本一樣使用它。
二、簡單了解編譯連接與執(zhí)行
1. 實驗介紹
按照 GNU make 官方手冊中采用的教學(xué)模式,在正式的學(xué)習(xí) Makefile 知識之前,本次實驗先介紹一些簡單的前導(dǎo)知識。實驗詳細介紹了 GNU GCC 編譯和鏈接的基本方法,通過編譯、鏈接、靜態(tài)鏈接、動態(tài)鏈接的實驗內(nèi)容讓用戶學(xué)習(xí)和理解 GCC 的基本使用方法。同時,用戶也將在實驗過程中體會到手動編譯鏈接的低效,從而體會到自動編譯的在項目工程管理中的重要性。
知識點
- GCC 編譯的使用方式
- GCC 鏈接的使用方式
- GCC 靜態(tài)鏈接的使用方式
- GCC 動態(tài)鏈接的使用方式
- GCC 靜態(tài)鏈接 + 動態(tài)鏈接混用的方式
代碼獲取
通過在 Terminal 中輸入以下命令可以將本課程所涉及到的所有源代碼下載到在線環(huán)境中,作為參照對比進行學(xué)習(xí)。
wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip && unzip make_example-master.zip && rm make_example-master.zip命令執(zhí)行后 WebIDE 的工作區(qū)中將會出現(xiàn)一個名為 make_example-master 的文件夾,文件夾中包含了課程所涉及到的源代碼,目錄結(jié)構(gòu)如圖所示:
2. 實驗步驟
本章節(jié)的源代碼位于 /home/project/make_example-master/chapter0 目錄中,請在 Terminal 中通過 cd 命令切換至該目錄后再進行實驗學(xué)習(xí)。
項目涉及到的代碼文件:
(1)main.c: 主要文件
(2)add_minus.c add_minus.h: 加減法 API 及實現(xiàn)
(3)multi_div.c multi_div.h: 乘除法 API 及實現(xiàn)
項目涉及到的 gcc 參數(shù):
| -c | 編譯、匯編指定的源文件(也就是編譯源文件),但是不進行鏈接 |
| -o | 用來指定輸出文件 |
| -L | 為 gcc 增加一個搜索鏈接庫的目錄 |
| -l | 用來指定程序要鏈接的庫 |
這一章節(jié)我們將正式開始進行簡易四則預(yù)算程序的編譯實驗,分步驟進行。
主程序的編譯、鏈接與執(zhí)行
打開 chapter0 文件夾查看 main.c 文件,內(nèi)容如下:
#include <stdio.h>int main(void) {printf("Hello Cacu!\n");return 0; }點擊 chapter0 文件夾并右鍵選擇 Open in Terminal 在終端中打開 main.c 所在的文件夾。
在 Terminal 中執(zhí)行以下命令,對 main.c 文件只編譯而不鏈接。
gcc -c main.c可以發(fā)現(xiàn)在當(dāng)前目錄中生成了一個新的文件 main.o。
通過 file 命令查看 main.o 的文件格式:
file main.o輸出結(jié)果如圖所示:
這說明 main.o 實際上是一個 relocatable object 文件。
通過以下命令為 main.o 文件賦予可執(zhí)行的權(quán)限:
chmod 777 main.ochmod 命令用于改變文件的讀寫以及運行許可設(shè)置,詳細解紹參考 Permissions
輸入以下命令嘗試執(zhí)行 main.o 文件:
./main.oTerminal 輸出可執(zhí)行文件格式錯誤,如圖所示:
說明 relocatable object 文件是不可執(zhí)行的。
接下來通過 GCC 對 main.o 文件進行鏈接操作,從而生成一個可執(zhí)行的程序 main。
在 Terminal 中輸入以下命令將 main.o 鏈接為 main 文件:
gcc -o main main.o可以發(fā)現(xiàn)當(dāng)前目錄新增了一個名為 main 的文件。
通過 file 命令查看 main 的文件格式:
輸出結(jié)果如圖所示:
說明 main 文件是一個可執(zhí)行的文件,于是通過以下命令來執(zhí)行 main 文件:
輸出結(jié)果如圖所示:
說明程序得到了正確的執(zhí)行。
靜態(tài)鏈接
編寫 add_minus.h 文件,在文件中對函數(shù) add() 和 minus() 進行聲明,不過在 chapter0 文件夾中已經(jīng)提供編寫好的 add_minus.h 文件,我們可以拿來直接使用。
文件內(nèi)容如下:
#ifndef __ADD_MINUS_H__ #define __ADD_MINUS_H__int add(int a, int b); int minus(int a, int b);#endif /*__ADD_MINUS_H__*編寫 add_minus.c 文件,實現(xiàn)函數(shù) add() 和 minus() ,同樣的在 chapter0 文件夾中已經(jīng)有編寫好的 add_minus.c 文件,我們可以拿來直接使用。
文件內(nèi)容如下:
#include "add_minus.h"int add(int a, int b) {return a+b; }int minus(int a, int b) {return a-b;對 add_minus.c 文件進行編譯,生成 add_minus.o 文件。
gcc -c add_minus.c修改 main.c 文件,為其增加加減法運算并編譯這個文件。
執(zhí)行以下命令給 main.c 打上 v1.0.patch 補丁:
patch -p2 < v1.0.patchpatch 命令可以處理 diff 程序生成的補丁文件,補丁格式可以是四種比較格式中任意一種, 然后把這些差異融入到原始文件中,生成一個打過補丁的版本。-p 選項表示剝離層級,通過在 Terminal 中輸入 man patch 命令可獲取詳細說明。
此時 main.c 文件內(nèi)容如下:
#include <stdio.h> #include "add_minus.h"int main(void) {int rst;printf("Hello Cacu!\n");rst = add(3,2);printf("3 + 2 = %d\n",rst);rst = minus(3,2);printf("3 - 2 = %d\n",rst);return通過以下命令對 main.c 文件進行編譯和鏈接:
gcc -c main.c gcc -o main main.o鏈接生成的 main.o 文件時,發(fā)現(xiàn)有錯誤出現(xiàn),錯誤內(nèi)容如圖所示:
原因在于鏈接過程中找不到 add 和 minus 這兩個 symbol。
現(xiàn)將 main.o 和 add_minus.o 鏈接成可執(zhí)行文件并執(zhí)行測試。
gcc -o main main.o add_minus.o執(zhí)行新生成的可執(zhí)行文件 main。
./main輸出結(jié)果如下:
說明程序得到了正常執(zhí)行。
重新編譯 add_minus.c 生成 add_minus.o 文件。
gcc -c add_minus.c通過 ar 命令將 add_minus.o 打包到靜態(tài)庫中。
ar rc libadd_minus.a add_minus.o可以發(fā)現(xiàn)在當(dāng)前目錄下,生成了一個名為 libadd_minus.a 的靜態(tài)庫文件。
用 file 命令查看 libadd_minus.a 的文件格式。
Terminal 輸出結(jié)果如圖所示:
實際上 libxxx.a 格式的文件可以簡單的看成指定的以 .o 結(jié)尾的文件集合。
鏈接 main.o 和靜態(tài)庫文件。
gcc -o main2 main.o -L./ -ladd_minus-L./:表明庫文件位置在當(dāng)前文件夾。
-ladd_minus:表示鏈接 libadd_minus.a 文件,使用 -l 參數(shù)時,前綴 lib 和后綴 .a 是需要省略的。
執(zhí)行 main2:
./main2Terminal 輸出結(jié)果如圖所示:
說明程序的到了正確的執(zhí)行。
動態(tài)鏈接
編寫 multi_div.h 文件,并在其中對函數(shù) multi() 和 div() 進行聲明。由于提供的源代碼中已經(jīng)包含了編寫好的 multi_div.h 文件,因此我們可以拿來直接使用。
multi_div.h 文件的內(nèi)容如下:
#ifndef __MULTI_DIV_H__ #define __MULTI_DIV_H__int multi(int a, int b); int div(int a, int b);#endif /*__MULTI_DIV_H__*編寫 multi_div.c 文件,實現(xiàn)函數(shù) multi() 和 div(),同樣的由于提供的源代碼中已包含了編寫好的 multi_div.c 文件,我們可以直接拿來使用。
multi_div.c 文件內(nèi)容如下:
#include "multi_div.h"int multi(int a, int b) {return a*b; }int div(int a, int b) {return a/b;通過以下命令將 multi_div.c 文件編譯成動態(tài)鏈接庫。
gcc multi_div.c -fPIC -shared -o libmulti_div.so-fPIC 選項作用于編譯階段,在生成目標文件時就得使用該選項,以生成位置無關(guān)的代碼。
命令執(zhí)行結(jié)束后,在當(dāng)前目錄下會生成一個名為 libmulti_div.so 的文件。
通過 file 命令來查看 libmulti_div.so 的文件格式。
Terminal 輸出結(jié)果如圖所示:
由此可知 libmulti_div.so 是一個 shared object 文件。
刪除之前的 main.c 文件,并編寫新的 main.c 文件,內(nèi)容如下:
#include <stdio.h>int main(void) {printf("Hello Cacu!\n");return 0; }通過以下命令為 main.c 打上 v2.0.patch 補丁:
patch -p2 < v2.0.patch此時 main.c 文件的內(nèi)容如下:
#include <stdio.h> /* #include "add_minus.h" */ #include "multi_div.h"int main(void) {int rst;printf("Hello Cacu!\n"); /*rst = add(3,2);printf("3 + 2 = %d\n",rst);rst = minus(3,2);printf("3 - 2 = %d\n",rst); */rst = multi(3,2);printf("3 * 2 = %d\n",rst);rst = div(6,2);printf("6 / 2 = %d\n",rst);return編譯 main.c 生成 main.o:
gcc -c main.c鏈接 main.o 與動態(tài)鏈接庫文件。
gcc -o main3 main.o -L./ -lmulti_div執(zhí)行生成的 main3 文件。
./main3輸出結(jié)果出現(xiàn)錯誤,如圖所示:
出現(xiàn)錯誤的原因是我們生成的動態(tài)庫 libmulti_div.so 并不在庫文件搜索路徑中。
解決辦法:
所以需要在 Terminal 中執(zhí)行下面的命令:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/project/make_example-master/chapter0/現(xiàn)在在 Terminal 中執(zhí)行下面的命令:
./main3輸出結(jié)果如圖所示:
說明程序得到了正確的執(zhí)行。
混合使用靜態(tài)鏈接與動態(tài)鏈接
刪除舊的 main.c 文件,并編寫新的 main.c 文件,內(nèi)容如下:
#include <stdio.h>int main(void) {printf("Hello Cacu!\n");return 0; }為新的 main.c 文件打上 v3.0.patch 補丁。
patch -p2 < v3.0.patch編譯 main.c 生成 main.o。
gcc -c main.c測試執(zhí)行混用靜態(tài)鏈接和動態(tài)鏈接的方式。
gcc -o main4 main.o -L./ -ladd_minus -lmulti_div由于我們之前已經(jīng)修改過 LD_LIBRARY_PATH 變量,所以此次無需再次修改。
執(zhí)行下面的命令:
./main4輸出結(jié)果如圖所示:
說明程序得到正確的執(zhí)行。
盡管我們知道無論是靜態(tài)鏈接還是動態(tài)鏈接都能達到鏈接對象文件生成可執(zhí)行文件的目的,但是我們還是得 z 注意靜態(tài)鏈接庫與動態(tài)鏈接庫之間的區(qū)別,詳細內(nèi)容參考 Static, Shared Dynamic and Loadable Linux Libraries
三、總結(jié)
上述內(nèi)容來自課程《Makefile 基礎(chǔ)入門實戰(zhàn)》,主要介紹了 GCC 編譯,鏈接的方法和靜態(tài)鏈接庫與動態(tài)鏈接庫的創(chuàng)建和使用方法。
后續(xù)課程內(nèi)容將學(xué)習(xí)以下內(nèi)容:
點擊《Makefile 基礎(chǔ)入門實戰(zhàn)》,即可可學(xué)習(xí)完整課程!
總結(jié)
以上是生活随笔為你收集整理的要学好 C 语言 / C++ ,Makefile 可少不了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CentOS VS Ubuntu,谁才是
- 下一篇: 从入门到放弃,C++ 真这么难?