經(jīng)常在一個項目中包含多個.c源文件,而且一個.c源文件包含了一堆的頭文件,這種情況下如何編寫makefile,使得能成功編譯整個項目?本博文對這些問題提出自己淺析的理解。涉及到對gcc命令使用與編譯流程理解及多文件時makefile的編寫。
有些場景下編譯的程序是不能依賴OS和標準的C庫的,并且需要C和匯編混合編譯,如bootloader程序,就需要制定參數(shù)-nostdlib,這樣的Makefile如下所示:
all:
arm-linux-gcc -O2 -Wall -nostdlib -march=armv4 -Wl,-T,ipl.lds uart.c ipl.c nfc.c nand.c sha1.c arm.s -o ipl.exe
arm-linux-objcopy -Obinary ipl.exe ipl.bin
clean:
rm -rf ipl.exe ipl.bin
很奇怪的是,在《跟我一起學(xué)Makefile-陳皓》的文檔中,第五部分第八章,有講到自動生成依賴性的問題。從字面意思,我理解為在.c源文件中每添加一個自己的頭文件都需要在makefile的對應(yīng)目標中添加對該頭文件的依賴。
添加對此頭文件的依賴是為了確保有此文件?但是在gcc編譯時,如果尋找不到該頭文件,是會停止編譯且報錯的。
然后我就直接理解為了:添加頭文件依賴是為了gcc編譯時讓此頭文件也作為輸入文件
例如:
?。踥bjc] view plain copy《abc.c》:
include “abc.h”
include 《stdio.h》
void main()
{
printf(“helloworld”);
}
編譯成.o文件時,應(yīng)該為:
[objc] view plain copygcc -c abc.c abc.h //錯誤示范
結(jié)果卻是相當(dāng)于執(zhí)行了:
?。踥bjc] view plain copygcc -c abc.c
gcc -c abc.h
是因為我的gcc版本較新?總之,折騰了很久后,我就是發(fā)現(xiàn),頭文件沒必要寫到依賴中,而gcc也不需要直接把此頭文件作為輸入,但是若頭文件不在當(dāng)前目錄下,需要用-I指出頭文件地址。(純粹個人階段性的理解,還希望大牛能指正)
在編程的時候,我們可以把一個完整程序的每個函數(shù)分離出來,寫成.c文件,最后再一起編譯和鏈接。這樣有利于程序功能模塊化,也方便檢查代碼錯誤。
.h文件:里面編輯該程序需要引用的頭文件。
#ifndef /#define / #endif : 防止該頭文件被重復(fù)引用。
整體用法:
#ifndef A_H //如果沒有a.h文件,#define A_H。如果有,結(jié)束定義
#define A_H //定義a.h文件
定義a.h需要的頭文件
#endif //結(jié)束定義部分
例子:
一個.h文件的書寫格式。
#ifndef A_H
#define A_H
定義程序所需的一些頭文件
#include《stdio.h》
#include《string.h》
#include《stdlib.h》
…
#endif
保存退出。
其實在編寫小程序的時候也可以不用#ifndef /#define / #endif,但是用它更安全,不易出錯。
c文件的書寫格式
在寫每個函數(shù)之前要加載頭文件,如果是.h文件,就需要寫#include”a.h”。
例如:引用之前的a.h文件。
add.c
#include”a.h”
int add(int a,intb)
{
return a+b;
}
保存退出。
程序編輯完成之后,需要編譯鏈接。
我們可以用gcc編譯每個.c文件。如果有三個.c文件a.c、b.c、c.c,編譯方法如下:
gcc a.c –o a.o //將三個.c文件編譯成.o文件
gcc b.c -o b.o
gcc c.c -o c.o
gcc a.o b.o c.o –o all //將三個.o文件編譯成一個可執(zhí)行文件
。/all //執(zhí)行程序
例如:
test.h
add.c
main.c
編譯:
執(zhí)行:
如果我們有很多個.c文件,這個方法就不太好了,這時,我們提出Makefile文件。
Makefile:自動編譯。先將每個.c文件的編譯過程提前寫在Makefile文件中,在運行程序時,系統(tǒng)直接用make命令使文件自動編譯,提高效率。
Makefile文件的書寫格式:
vim Makefile
文件類型:由哪個文件得到
得到過程
例如:
main:main.o //可執(zhí)行文件main是由目標文件main.o得到。
gcc main.o –o main //得到過程是將main.o編譯成main文件。
main.o:main.c
gcc -c main.c -o main.o
在Makefile文件中一定要將每一個.c文件按執(zhí)行順序先編譯成.o文件,再按順序?qū)?o文件編譯成可執(zhí)行文件。
每次編譯過后會產(chǎn)生很多的.o文件,對于程序運行沒什么太大意義,反而會占內(nèi)存,所以我們也可以在Makefile文件中添加清除命令(clean),如:
.PHONY:clean
clean: 刪除所有文件類型為.o的文件
rm –rf *.o
一些文件也可用下面符號表示:
$@: 代表規(guī)則里面的目標文件。
$《:代表規(guī)則當(dāng)中的第一個依賴文件。
%.c:%.o 所有.c文件全部編譯成.o文件
.PHONY:clean
如果有.PHONY:clean,外面也有clean文件,執(zhí)行make clean時,里面的.o文件會被刪除而外面的clean文件還在。確保了外面clean文件的安全性。如果沒有.PHONY:clean 語句,外面也沒有clean文件時,在執(zhí)行make clean也會刪除.o文件,如果外面有clean,則會報錯。
PHONY:目標并非實際的文件名,只是顯示在請求時執(zhí)行命令的名字。有兩種理由需要使用PHONY目標:避免和文件名沖突,改善性能。
例如:
vim Makefile
編譯和執(zhí)行(make:編譯, 。/all:執(zhí)行):
下面是經(jīng)驗分享:
一、gcc編譯的流程
gcc的編譯流程分為4步:(詳見:http://xredman.iteye.com/blog/700901)
預(yù)處理(Pre-Processing) -》 編譯(Compling) -》 匯編(Assembling) -》 連接(Linking)
預(yù)處理:處理#include、#define、#ifdef 等宏命令
編譯:把預(yù)處理完的文件編譯為匯編程序.s
匯編:把匯編程序.s編譯為.o二進制文件
鏈接:把多個二進制文件.o集合(鏈接)成一個可執(zhí)行文件
由此可見,
多頭文件.h時,在預(yù)處理階段處理,指明頭文件所在地址,但通常在makefile中是一個命令完成到第3步,生成.o
多源文件.c時,在鏈接階段處理,gcc命令要寫出所有源文件,不然會出現(xiàn)引用了卻未定義的函數(shù)\變量等
二、多文件,多頭文件時的gcc經(jīng)驗分享
情況1、一步直接由.c生成執(zhí)行文件
?。踥bjc] view plain copygcc [-I包含文件.h的目錄1 -I包含文件.h的目錄2.。。] 源文件1.c [源文件2.c 源文件3.c.。。] -o 執(zhí)行文件名
情況2、先編譯成.o,再由.o鏈接為執(zhí)行文件(makefile中常見,因為在大型項目時,可以實現(xiàn)重編譯部分文件而不需要每次都全部編譯源文件文件)
?。踥bjc] view plain copya、gcc [-I源文件1包含的文件.h的目錄] 源文件1.c [-o 源文件1.o]
//可以通過-o指定生成的二進制文件地址和位置
gcc [-I源文件2包含的文件.h的目錄] 源文件2.c [-o 源文件2.o]
。。。。
b、gcc 源文件1.o 源文件2.o 。。。。。。 -o 生成的執(zhí)行文件(默認為a.out)
三、多文件,多頭文件時的makefile經(jīng)驗分享
以例子說明最好理解,文件結(jié)構(gòu)如下圖:
?。ㄒ韵聻樵次募?頭文件的展示,沒興趣的可以跳過此部分,不影響整體理解)
《myhello.c》:
?。踥bjc] view plain copy[user@13:08 src]$cat myhello.c
#include 《stdio.h》
#include “test.h”
#include “abc.h”
void printhelloworld(void);
int main()
{
abc();
printtest();
printf(“\n”);
printhelloworld();
return 0;
}
void printhelloworld(void){
printf(“hello world\n”);
}
?。踰ser@13:08 src]$
《abc.c && abc.h》
[objc] view plain copy[user@13:10 src]$cat 。/common/abc.c
#include “abc.h”
void abc(void)
{
printf(“\nit is in funciton abc”);
}
[user@13:10 src]$cat 。/common/abc.h
#include 《stdio.h》
void abc(void);
[user@13:11 src]$
《test.c && test.h》
?。踥bjc] view plain copy[user@13:11 src]$cat 。/common/test/test.c
#include 《stdio.h》
void printtest(void)
{
printf(“\nit is in test.c”);
}
?。踰ser@13:11 src]$cat 。/common/test/test.h
void printtest(void);
?。踰ser@13:11 src]$
?。ùa展示到此結(jié)束)
簡單來說,在myhello.c的main中,需要調(diào)用。/common/abc.c的abc函數(shù)和。/common/test.c的printtest函數(shù),因而包含了他們的頭文件abc.h test.h
重點來了,makefile可以怎么寫(只是我的寫法的參考)
?。踥bjc] view plain copy[user@13:11 src]$cat Makefile
//目標(要生成的文件名)
TARGET := myhello
//編譯器的選擇(在Linux中其實可以忽略,因為cc指向的本來就是gcc)
CC := gcc
//編譯的參數(shù)
CFLAG := -Wall
//編譯包含的頭文件所在目錄
INCLUDES := -I. -Icommon/ -Icommon/test
//所有用到的源文件,注意:非當(dāng)前目錄的要+上詳細地址
SRCS = myhello.c 。/common/abc.c 。/common/test/test.c
//把源文件SRCS字符串的后綴.c改為.o
OBJS = $(SRCS:.c=.o)
//匹配所有的偽目標依賴,即執(zhí)行目標myhello.o & 。/common/abc.c & 。/common/test/test.c
.PHONY:all //all為偽目標all:$(OBJS)
//當(dāng)所有依賴目標都存在后,鏈接,即鏈接myhello.o & 。/common/abc.c & 。/commontest/test.c
$(CC) $(LDFLAG) -o $(TARGET) $^
//重定義隱藏規(guī)則,匹配上述目標:myhello.o & 。/common/abc.c & 。/common/test/test.c
%.o:%.c
//生成.o文件,注意,由于SRCS有個別包含詳細地址的,生成的.o文件也是詳細地址
$(CC) -c $(INCLUDES) $(CFLAG) $(CPPFLAG) $《 -o $@
//清空除源文件外的所有生成文件
clean: rm -rf $(basename $(TARGET)) $(SRCS:.c=.o)[user@13:14 src]$
make執(zhí)行下,結(jié)果怎么樣呢:
?。踥bjc] view plain copy[user@13:35 src]$make
//編譯了myhello.c,自動處理了頭文件abc.h test.h的包含關(guān)系
gcc -c -I. -Icommon/ -Icommon/test -Wall myhello.c -o myhello.o gcc -c -I. -Icommon/ -Icommon/test -Wall common/abc.c -o common/abc.o gcc -c -I. -Icommon/ -Icommon/test -Wall common/test/test.c -o common/test/test.o
//把生成的所有.o文件鏈接為執(zhí)行文件,如果有缺失,會提示函數(shù)/變量未定義
gcc -o myhello myhello.o common/abc.o common/test/test.o[user@13:35 src]$
說明:由于當(dāng)前博文編寫代碼是在Objective-C上,因此為了觀看方便而用注釋//,實際在makefile中注釋為#
此時的文件結(jié)構(gòu):
不知道注意到了沒,我通過-o指定地址,把生成的.o與源文件.c放在一起的,例如abc.o放在了abc.c的目錄,但同時的,鏈接時也需要給出詳細地址。
結(jié)論:
在gcc時,會自動解決頭文件.h的依賴關(guān)系,只需要指明頭文件的地址
在gcc鏈接時,才需要把所有的源文件.o列出了,否則出現(xiàn)引用了未定義的變量/函數(shù)
評論