c文件编译源码-使用Makfile构建多文件和多目录C源代码的项目

有粉丝留言,想了解如何使用Makefile构建多文件、多级目录的工程,必须整理一下!

对于Makefile的介绍参考文章,初学者可以先看一下这篇文章:

””

为了让大家更直观的理解,我以之前写的一个小项目为例,本文根据该项目进行修改。

该项目详细设计及代码见下图:

””

1. 文件

废话不多说,开始吧!

我们把项目的所有功能函数放在以函数命名的c文件中,并放在对应名称的子目录中。

例如函数allfree()存放在allfree/allfree.c中,最终的目录结构如右图所示:

 peng@ubuntu:/mnt/hgfs/code/phone$ tree .
.
├── allfree
│   ├── allfree.c
│   └── Makefile
├── create
│   ├── create.c
│   └── Makefile
├── delete
│   ├── delete.c
│   └── Makefile
├── display
│   ├── display.c
│   └── Makefile
├── include
│   ├── Makefile
│   └── phone.h
├── init
│   ├── init.c
│   └── Makefile
├── login
│   ├── login.c
│   └── Makefile
├── main
│   ├── main.c
│   └── Makefile
├── Makefile
├── menu
│   ├── Makefile
│   └── menu.c
├── scripts
│   └── Makefile
└── search
    ├── Makefile
    └── search.c

11 directories, 22 files

直接看编译结果:

peng@ubuntu:/mnt/hgfs/code/phone$ make
make[1]: Entering directory '/mnt/hgfs/code/phone/allfree'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/allfree'
make[1]: Entering directory '/mnt/hgfs/code/phone/create'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/create'
make[1]: Entering directory '/mnt/hgfs/code/phone/delete'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/delete'
make[1]: Entering directory '/mnt/hgfs/code/phone/display'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/display'
make[1]: Entering directory '/mnt/hgfs/code/phone/init'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/init'
make[1]: Entering directory '/mnt/hgfs/code/phone/login'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/login'
make[1]: Entering directory '/mnt/hgfs/code/phone/menu'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/menu'
make[1]: Entering directory '/mnt/hgfs/code/phone/search'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/search'
make[1]: Entering directory '/mnt/hgfs/code/phone/main'
make[1]: Nothing to be done for 'all'.
make[1]: Leaving directory '/mnt/hgfs/code/phone/main'
gcc -Wall -O3 -o phone allfree/*.o create/*.o delete/*.o display/*.o init/*.o login/*.o menu/*.o search/*.o main/*.o -lpthread
phone make done

运行结果如下:

2、常用基础知识点说明【0】Makefile中符号'@''$''$$''-''-n'

'@'

通常,makefile 在执行之前会将其执行的命令行输出到屏幕上。 如果在命令行前添加“@”,则 make 不会回显该命令。 例如:

@echo  --compiling module----;  // 屏幕输出  --compiling module----
echo  --compiling module----;  // 没有@ 屏幕输出echo  --compiling module----   

'-'

一般删除、创建文件如果文件不存在或者已经创建过,那么如果想忽略这个错误继续执行,可以在命令后添加 - ,

-rm dir;
-mkdir aaadir;

'$' 欧元符号$,主要扩展打开makefile中定义的变量

'$$'$$符号主要是展开来打开makefile中定义的shell变量

[1] 外卡

说明:列出当前目录中所有与模式“PATTERN”格式匹配的文件名,但以空格分隔。 “PATTERN”使用 shell 可识别的键值,包括“?” (单个字符)、“*”(多个字符)等。示例

$(wildcard *.c) 

返回值是当前目录中所有.c源文件的列表。

[2] 帕特萨斯特

描述:将字符串“xccbar.c”中以.c结尾的短语替换为以.o结尾的字符。 例子:

$(patsubst %.c,%.o,x.c.c bar.c)

函数的返回结果为:

 x.c.o bar.o

[3] 不目录

说明:从文件名中删除路径信息示例:

SRC = ( notdir ./src/a.c ) 

消除文件ac的路径信息,使用(notdir./src/ac)消除文件ac的路径信息,使用(notdir./src/ac)消除文件ac的路径信息,使用( SRC)获取路径信息,不带路径 文件名,即ac.src。

[4]包含头文件路径

使用-I+头文件路径的方法可以指定编译器头文件的路径示例:

INCLUDES = -I./inc

$(CC) -c $(INCLUDES) $(SRC)

[5] 添加后缀

函数名称:添加后缀函数—addsuffix。 句型:

$(addsuffix SUFFIX,NAMES…) 

函数功能:为“NAMES...”中的每个文件名添加后缀“SUFFIX”。 参数“NAMES...”是一个以空格分隔的文件名序列,“SUFFIX”附加到该序列中每个文件名的末尾。 返回值: 由单个空格分隔且后缀为“SUFFIX”的文件名序列。 功能说明: 示例:

$(addsuffix .c,foo bar) 

返回值为:

foo.c bar.c

[6]包含另一个文件:include

在Makefile中使用include关键字来包含其他Makefile,这与C语言中的#include非常相似,被包含的文件将原样放置在当前文件的include位置。 命令例如:

include file.dep

即对当前Makefile中的file.dep文件进行扩展,从而将file.dep文件的内容包含到当前Makefile中。

include后可以有一些空字符,并且不能以[Tab]键开头。

[7] foreach

foreach 函数与其他函数有很大不同。 由于该函数是作为循环使用的,所以句型为:

$(foreach ,, )

该函数的含义是将参数中的短语一一取出放入参数指定的变量中,然后执行其中包含的表达式。

每次都会返回一个字符串。 在循环过程中,返回的每个字符串都将用空格分隔。 最后,当整个循环结束时,返回的每个字符串(用空格分隔)组成的整个字符串将是foreach函数的返回值。

因此,最好是一个变量名,可以是一个表达式,通常使用这个参数来依次枚举列表中的短语。

例子:

names := a b c d
files := $(foreach n,$(names),$(n).o)

里面的例子中,$(name)中的短语会被一一取出并存储到变量“n”中,“$(n).o”会根据每个“$(n)”计算出一个值时间。 用空格分隔,最后作为foreach函数的返回c文件编译源码,所以$(files)的值为“a.ob.oc.od.o”。

请注意,foreach 中的参数是临时局部变量。 foreach函数执行后,参数的变量将不再起作用,其作用域仅在foreach函数内。

[8] 呼叫

“call”函数是唯一可以创建具有各种参数的函数的参考函数。 使用该函数可以实现对用户定义函数的引用。 我们可以将变量定义为复杂的表达式,并使用“call”函数根据不同的参数对其进行扩展,以获得不同的结果。

功能句型:

$(call variable,param1,param2,...)

函数功能:在执行过程中,其参数“param”依次被形式参数化为临时变量“$(1)”和“$(2)”。 call函数对参数的个数没有限制,也可以没有参数值或者参数值的“调用”没有任何实际意义。

变量“variable”在执行过程中被扩展为函数上下文中有效的临时变量,变量定义中的“$(1)”作为第一个参数,将函数参数值中的第一个参数赋给它; 变量中的“$(2)”被形式参数化为函数的第二个参数值; 这样(变量**$(0)**代表变量“变量”本身)。 稍后对变量“变量”表达式进行求值。

返回值:参数值“param”替换“$(1)”、“$(2)”...之后,变量“variable”定义的表达式的评估值。

功能说明:

函数中的“变量”是变量名,而不是变量引用。 为此,“call”函数中的“变量”中一般不包含“$”(事实上,除非变量名是估计的变量名)。

当变量“variable”是make中嵌入的函数名(如“if”、“foreach”、“strip”等)时,“param”参数的使用需要注意,因为不恰当或不正确的参数将会导致函数的返回值不可预测。

函数中的多个“param”用冒号分隔。

变量“variable”不能在定义时定义为直接扩展! 只能定义为递归展开。

功能示例:

reverse = $(2)$(1)
foo = $(call reverse,a,b)
all:
 @echo "foo=$(foo)"

结果:

foo=ba

即 a 替换 (2)

3.编译详细说明

我们在根目录下执行make命令后,详细步骤如下:

includescripts/Makefile:将文件替换到当前位置,

使用默认的target all,依赖于scripts/Makefile中定义的$(Target)$(Target),即phone

$(Target) 取决于 mm

mm这个目标将会执行

@ $(foreach n,$(Modules),$(call modules_make,$(n)))

Modules是所有目录名的子集合,foreach会遍历字符串$(Modules)中的每个惯用法,每个惯用法都会给出n作为形参,同时执行语句:

call modules_make,$(n)

module_make 替换为 $(MAKE)-C$(1),

$(MAKE)有一个默认名称 make -C:单步进入子目录并执行make $(1):是第4步中的$(n)c文件编译源码,即各个目录的名称

最后第4步的那句是单步进入各个目录,执行各个目录下的Makefile

单步进入某个子目录,执行Makefile默认target是all,依赖Objs

Objs := $(patsubst %.c,%.o,$(Source))

patsubst 将字符串 $ource 中以 .c 结尾的短语替换为以 .o 结尾的字符

Source := $(wildcard ./*.c)

通配符会列出当前目录下的所有.c文件,所以第六步就是编译子目录下的所有.c文件,生成对应文件名的.o文件。

$(CC) $(CFLAGS) -o $(Target) $(AllObjs) $(Libs)

这些变量定义在文件scripts/Makefile中 $(CC): 替换为gcc,建议编译器 $(CFLAGS): 替换为-Wall-O3,这是编译时的优化级别 -o$(Target): 生成的可执行文件程序电话$(AllObjs):

AllObjs := $(addsuffix /*.o,$(Modules))

addsuffix 会将 /*.o 追加到 $(Modules) 中的所有单词和句子,即之前我们在子目录中编译生成的所有 .o 文件 $(Libs):替换为 -lpthread,这是必需的动态库

可以按照这一步来分析执行makeclean时的执行步骤。

结尾