|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
我们在 Windows 下使用 C 语言开发的时候,一般都会有支持 Windows 的开发工具,比如我们学习 51 单8 t5 F- M, W" ^
片机或者 STM32,所使用的的 Keil 开发软件。此类开发工具一般都会集编辑、编译于一体,我们只需要编辑
8 v6 J& q0 X1 q. ?0 \好代码,电机开发工具的编译按钮,就可以自动为我们编译出可执行的二进制文件了。Ubuntu 下的 C 语言
( B* N% o* I) M) j4 T- U* P开发与 Windows 下的 C 语言开发是不一样的,并没有图形界面的开发工具,而且编辑和编译是分开的。我& a; {+ T3 z' t7 T1 v
们需要使用文本编辑软件先编写代码,然后使用编译工具(GCC)来编译,最终生成可执行的二进制文件。/ A) L4 t Z' b3 S8 _3 p0 L- d! F
如果我们的工程中有多个源文件,在编译的时候我们通常会通过 Makefile 文件来管理整个工程的源文件。& |7 m' ^7 B9 w: e" |
本章我们来学习如何在 Ubuntu 下进行 C 语言成的编辑,GCC 编译,Makefile 文件的使用。通过本章学习我
z* T* Y9 [" S8 f' [; j们可以掌握 Linux 下的 C 语言开发基本方法,为后面的学习做好准备。
, O! s: e- J9 L; U; q3.1 x Linux 下编写第一个 C C 程序 程序
8 M$ y* D+ j* X. L' Q2 F) h" f本章节开始的部分我们介绍了 Ubuntu 下 C 程序开发分成两部分:编辑和编译。Ubuntu 下有很多种文本
% n# o( f/ u6 u" G) P6 d5 L编辑的工具,如 vim、Gedit、Emacs,这里我们使用前面章节介绍的 vim 文本编辑工具来编辑 C 程序。相信
1 k6 ~0 x0 |* R: Y. g, ]大家在学习 C 语言的时候都是从“Hello World”这个程序开始的吧,下面我们通过这个程序,来学习下 Linux
5 |* J! A8 W9 Z7 C9 r: {1 Y8 I下 C 程序开发的流程。
; k; T) p- S+ A5 Y6 U g$ I3 l- b3.1.1 代码编写: C3 [( W/ u# x! I( a7 o4 h
首先我们在用户根目录下建立文件夹“work”,用于保存所有 C 语言的程序,运行结果如下图所示:2 L' D) Q8 ?4 f( Z9 V
, L2 I' T% @% |' t
然后进入创建的文件夹 work,为了方便管理,我们每个例程都创建单独的文件夹,首先我们创建文件
3 |) J) Z- V& c' Z3 `夹“hello_world”来保存我们的第一个 C 程序,如下图所示:; q9 w" t6 w/ C! x3 t+ \
# f; s; Y8 h. s9 a9 S! ~ }
然后进入上图中的 hello_world 文件夹,使用 vi 命令新建文件“main.c”,然后在里面输入下面的代. a @/ Y8 i) d5 b9 Q% _
码:, u5 \ \# U# ~. ~( }$ L5 h$ m, E
#include
. D, D; j% U' S6 Z, zint mian(int argc, char *argv[])
9 u3 e9 O" c& o0 r1 a5 ] B3 O{
& o4 T' N# C, O0 P+ g, F/ N$ y+ sprintf("Hello World!\n");# V9 J' ]1 H! [
return 0;
* Z4 l; k/ U# R! R" P}! G. z4 H5 Y6 q% M4 y8 x
编写完以后保存并退出 vim 编辑器,然后可以使用 cat 命令查看代码是否保存成功,如下图所示:' f* X0 A# F8 R7 }1 x8 W; P
H( }: w6 A% k5 q: X
通过上图可以看到代码已经编辑完成了。. X1 F/ F' {( [8 b5 S
3.1.2 代码编译5 B' z: \& U/ ~1 @; _6 v1 N! m
Ubuntu 下使用 gcc 编译器来编译 C 程序,我们在安装 Ubuntu 系统的时候,gcc 编译器 morning 安装好; f6 ~7 f6 N( t; g9 v
了,我们可以在终端输入“gcc -v”来查看下 gcc 的版本,如下图所示:
0 G0 A) o! }" S. F: f. W/ a0 n
8 O/ |1 V8 I8 w: N/ ^
通过上图可以看到 gcc 的版本信息,说明我们的 Ubuntu 上已经安装了 gcc 编译器了,下面我们来看看
0 g# |/ t" y! g1 U. g怎么通过 gcc 编译我们的第一个 C 程序,我们在终端输入“gcc main.c -o main”,然后回车,会生成 main 文" s: D1 z" ?" b4 z
件,如下图所示:
( `2 ^: J8 y9 E7 k' ]
" ~0 A) t8 w. T在上面的“gcc main.c -o main”这个命令里面“-o“用来指定编译生成的文件名字,我们指定的是 main,$ r0 v0 U) v1 _" G
所以在图 3.1.2.2 中可以看到生成了文件“main”,如果我们想生成其它的文件名字,我们只需要修改“-o”/ P" }- V( V B0 ^
后面的 main(修改成您希望生成的文件名字)。经过前面的步骤已经生成了可执行文件 main,现在我们演
# C5 u8 I* n' ?9 y示下在终端如何运行一个可行性的程序,我们直接在终端当前目录下(可执行文件所在的目录下)输入
! Y# w- n* N0 ?! ?; X' U“./main”,然后回车,就可以运行可执行文件 main 了,运行结果如下图所示:; [" B% J; ~3 S2 S3 z
1 v* ?0 N1 r8 R3 d; E3 s7 c在上图中运行的命令“./main”,其中的“./”代表当前目录下。我们可以看到在终端打印出了 Hello
- p+ E2 x, E3 W* jWorld!。至此,Linux 下的 C 语言编辑和编译的一整套流程我们就介绍完了。- G7 A4 Z* G+ N( l9 o a# V% k
3.2 c gcc 编译器 编译器
, Y3 {1 L4 P F( N2 {3.2.1 gcc 命令分析
0 v, G% k1 `" O$ n在 3.1 节我们已经使用 gcc 编译了 Linux 下的第一个 C 程序,gcc 命令的格式如下:
6 H9 g$ X$ n. m9 [1 m$ jgcc [参数] [文件名]- r/ X1 w1 d4 b; M& U
主要参数说明如下:
. Z. ^1 }, C9 b2 d5 ^5 _ H6 y-c 编译、汇编到目标代码(.o),不链接成可执行文件
2 z6 X+ T9 |( ~; G A9 u-g 生成调试信息
1 P: J: V% T* Z-o 编译完成后生成的文件名,如果不使用该选项,默认生成 a.out 文件0 u/ D1 I8 k$ y1 n% s% T- J& \
-O 对程序进行优化编译,产生的可执行文件执行效率高/ l$ `2 V F X1 n: P
-w 不生成任何警告
& {6 R+ h5 A2 s# _! P5 X-S 仅编译到汇编语言,不进行汇编和链接& W+ U; k6 a0 C0 r% t( g9 w. k* q" U0 g
3.2.2 编译警告错误处理5 V1 A; h0 L [+ g5 q; M
我们是 Windows 下使用 Keil 或者其他开发工具,在编译的时候,如果程序有错误,开发工具会提示出1 n& ~0 ?( H+ M7 Q& B* R! F
具体的错误信息,可以很方便的定位到问题点,快速的修改出现的问题,gcc 同样也有类似的功能,下面我
~2 h6 e" }5 z B们来看下 gcc 的错误提示功能,首先我们在 work 目录下建立文件夹“test2”,然后使用 vim 在 test2 文件7 ?; t6 z. m" P1 O1 {+ v
夹创建 main.c 文件夹,在 main.c 文件输入如下代码:7 J5 n9 P0 E+ \' H6 ^# s+ m! `% c# T1 w
#include
p6 y9 n; n/ B8 A/ `int main(int argc, char *argv[])& w1 B1 H' k5 j% `4 G! U
{" L U6 _' z" L
int a;! p$ `6 Y; p2 ]8 |* b0 U- I
a = 1
1 G% T4 L2 Q6 G0 i9 e& V3 rprintf("a=\n", a);
3 k7 s1 X* C% m& Rreturn 0;
1 f8 Y/ e( ~- I9 b}
# J/ ?9 ^. o! f' h' O# i! w d上面代码有两处错误:
3 j7 V/ L* Z* ~; X S第 7 行 最后缺少“;”, F$ p# O. u$ \
第 9 行 printf 语法不对,应该为:printf("a =%d\n", a);
4 O3 H |" Q, g2 i$ a( [8 e/ o我们使用 gcc 编译 main.c,可以看到 gcc 会提示错误信息,如下图所示:* r& n ~5 O0 o5 x
6 U3 _9 F5 q5 M6 @1 u: {( P从上图中可以看到在 mian.c 文件的第 9 行 printf 前面缺少“;”我们在第 7 行“a = 1”后面加上“;”," H7 b) _% i5 D/ Z! T% b5 ?
然后继续编译,如下图所示:
: l) o7 P) E' j6 i' G
_4 E6 y& b. m6 ~& e从上图可以看出编译提示语法格式不对,我们把第 9 行修改成“printf("a=%d\n", a);”,然后在继续编2 H, X5 n+ X( d$ y
译,如下图所示:
' N V. f- x0 v3 Z( `
1 e/ h; J5 R( j# z* P+ K我们可以看到这次 gcc 编译通过,最终生成了文件 main。我们在终端执行 main,运行结果如下图所示:+ c6 w" q6 [8 e; {$ j0 j
- @. a& s7 ]! e% [8 F; k
从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出
5 K# U, z/ e8 m1 p" T5 P2 ~程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。
l+ D) W) v. p从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出2 Q1 ~7 O: e9 m/ l- _; h
程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。% s' P% _% ~5 |
3.2.3 gcc 编译流程9 F. k' S5 G9 H9 h
gcc 的编译流程可以分成四个步骤:
8 a' H5 C9 c- I1 P( j5 A" b- w( N1.预处理,生成预编译文件(.文件)4 G* [# T$ k2 J+ L9 {7 J+ T
2.编译,生成汇编代码(.S 文件)
, R; ^0 P; d8 e$ B7 }3.汇编,生成目标文件(.o 文件)
+ o2 L7 |, r9 t# W9 _* {3 W! a4.链接,生成可执行文件
* q S2 R4 ~% _! O% F' l3 3.3 初识 Makefile/ l$ S. i) r, ^
3.3.1 什么是 Makefile% ]+ | k! v& K6 `* I/ U6 l8 y
在 3.2 章节我们了解了在 Ubuntu 系统下通过 gcc 编译器来编译 C 程序,在我们演示的历程中只有一个1 G! T0 x0 a# y( R; ^6 ]7 d& I8 _3 a c* L
C 文件,我们直接在终端输入 gcc 的编译命令,就完成了 C 程序的编译。我们在实际开发过程中,如果我们$ Q6 d' m [. v
的工程有几十个,或者几百几千个 C 文件,我们通过在终端输入 gcc 命令来编译,这显然是不现实的。为
! g9 _( P3 Y3 {了解决这个问题我们可以使用“make”命令,它会解析 Makefile 文件中的指令(应该说是规则)来编译整: r9 M+ u+ A# }4 q6 f
个工程。在 Makefile 文件中描述了整个工程的所有文件的编译顺序,编译规则。" D8 {* E# ~% h' K' _3 c
作为一个专业的程序员,一定要掌握 Makefile 的,我们可以通过 Makefile 能了解到整个工程的处理过程
1 v s, ~( v( |5 O7 B; `1 V的。; Q f8 b9 w! H% b
由于Makefile涉及到很多的知识点,以至于可以单独写本书来讲述,所以本章我们只是讲解下Makefile
$ H$ v/ T$ X% e! U4 P1 H3 p的基础入门,如果详细的研究 Makefile,可以给大家推荐《跟我一起写 Makefile》这个电子文档,该文档8 Q* i. E/ K M0 L: Y
已经放在了:i.MX6UL 终结者光盘资料\09_其它参考资料里面了。2 ^' J6 L Y- {; P6 e" N; z3 Z, E* l! s
3.3.2 第一个 Makefile3 \- i+ H% q3 ]9 n! g/ t
在本节我们建立这样一个工程,计算两个整形数的和,并将结果在终端显示出来。在这个工程中一共有
" N* n' O2 E4 N4 I5 x0 r. Y% nmain.c、calc.c 两个 C 文件和 calc.h 这个头文件。其中 main.c 是主文件,calc.c 负责接收 main.c 传过
- _. Z1 i$ E. w来的数据,然后进行相加。main.c 文件的内用如下:
9 X D% [. D- Y1 C#include
% O: @1 w% [6 _. {; |$ X$ i5 ninclude "calc.h"
1 r2 N2 F+ s- g, R+ Jint main(int argc, char *argv[]): N! _/ p) ^) N/ K3 \* |7 @
{
( x6 l3 x3 n% g8 }. @! O" aint a = 3, b = 6, sum;
1 J( ?: S. D9 F& r5 t# Asum = calc(a, b);
f, M% G' ^1 L- \( p' o/ tprintf("%d + %d = %d\n", a, b, sum);) x U2 i; A$ |
return 0;( r4 ~7 Y/ i% v. h( U$ f% {
}4 e" a$ b- l6 s, K8 c
calc.c 文件内容如下:
, K' F% K) v! _#include
! Z. z% @8 G8 P3 {6 j! \: uint calc(int a, ing b)
8 L1 @1 s/ w4 z{) U( y; l' Z l% a8 y6 c! X; s4 A
return (a+b);8 h* O/ i& e* ^ A. ]7 @# e
}, P, S0 ~, U |
文件 calc.h 内容如下:
1 f/ u) L$ h9 F; o! q% q; o#ifndef _CALC_H
2 R1 Y7 s* R2 Y; u" A& }9 h#define _CALC_H, z3 d4 R3 \. j: T) K
int calc(int a, int b);0 B' A% I$ J* p; q8 ?+ A# @
#endif
$ [" `; \/ ]3 m0 s; @+ j( o F上面就是我们这个工程的所有源文件,我们在终端使用 gcc 编译这个工程,在终端输入“gcc main.c
! |2 k' b, _- v- p/ {+ Fcalc.c -o main”,该命令的意思是用 gcc 对 main.c、calc.c 进行编译,然后输出可执行文件 main,运行
4 @$ ?4 \; o8 N. J; W6 n+ q结果如下图所示: y0 d( _$ D8 Q8 U0 d4 u
1 y" L, r: l% u$ X7 q! t$ @" J通过上图可以看到生成了可执行文件 main,我们在终端运行 main 执行文件,运行结果如下图所示:
* D: y$ q; @. D# j8 T8 h% n7 s
! I- ?' B4 i: J i( s0 z' C我们可以看到上图的运行结果和我们设计的结果是一致的。由于我们的这个工程是有三个文件,如果5 k; s8 ~* }+ P( v9 J8 i" I2 V6 z
工程有几百个,几千个的文件,或者如果有一个文件被修改,使用上面的命令将会编译所有的文件,如果2 w% Z' E9 ~% K+ B3 ]
我们的工程有上万个文件,编译完一次工程所需要的时间就很可怕。最优的方法就是编译过一次以后,如
0 E8 o& o. `1 Q) I0 G. E1 m4 Y果后面在编译,只编译修改的文件,这样就会节约很多时间,因此我们修改下编译方法,命令如下:
% }# L" L( g1 f& jgcc -c main.c* b9 C. w. j+ U; W7 X
gcc -c calc.c
f/ i- y' q( n! y$ Fgcc main.o calc.o -o main
* J: C0 A3 r6 v( @ n Q3 \+ O( e" o我们在终端输入上面的命令,结果如下图所示:; K. Q3 l6 w1 Y- o) l. r/ |6 [1 {
4 O M- z4 C( e* g3 I' z
上图的第一条和第二条命令里面使用了参数“-c”是把 main.c 和 calc.c 编译成对应的.o 文件,最后
2 S2 C8 J7 n5 x一条命令是把编译生成的.o 文件链接成可执行文件 main。假如我们修改了 main.c 这个文件。只需将 main.c( k4 [9 P" X( a0 e) t
这个一个文件重新编译下,然后在把所有的.o 文件重新链接成可执行文件,对应的命令如下:
2 t# f- Z1 K+ C6 Bgcc -c main.c2 ~# ]6 z w4 _/ |+ T
gcc main.o calc.o -o main* X8 S+ M' n+ S5 g8 N; u
可是这样还有一个问题,如果需要修改的文件有很多,这样工作量也会不小,所以我们需要一个工具:
% q* l8 b+ E; t% z, |1.如果工程没有编译过,就会把工程中的.c 文件全部编译并连接成可执行文件% W' u+ p! l. M" B+ Z: |# _
2.如果工程中有某些文件修改了,只编译修改的文件并连接成可执行文件
L; e+ \9 F* w2 S, B/ d$ M3.如果工程中的头文件修改了,那么就要编译所有引用这个头文件的.c 文件,并且连 接成可执
( S, w7 Z8 ?& ~/ o# M行文件- B/ l/ j' n! X: o% M) M
我们开头说的 Makefile 就是完成这个功能的,下面我们在工程中建立一个 Makefile 文件来实现这样的功: w/ T4 S2 \& C. U9 P1 N/ s
能(注意:文件名字必须为 Makefile,大小写是区分的)。我们使用 vim 创建 Makefile 文件(Makefile$ S7 [, W: @! q% a: J
和我们的 main.c、calc.c 在同一级目录下),然后输入下面的脚本:& c) l( ?& q1 H& R! o( K
main:main.o calc.o. v' D r1 K( f+ {+ ?; P, T4 b
gcc -o main main.o calc.o
& m6 j3 }7 h$ T1 ]: c% ]5 I' Rmain.o:main.c+ ~) m) j) n& _ d; g
gcc -c main.c. J) e" u# t, J* }1 x
calc.o:calc.c. y8 n% L* o# }+ G( N
gcc -c calc.c: w, L$ W" e0 u& ?
clean:% I$ b% }1 ]1 C5 ?3 H% H
rm -RF *.o% {! l5 \! x3 N( U
rm -rf main2 i8 j9 ^' G; a( B8 m* L
上面脚本缩进的行需要使用“Tab”键缩进,不要使用空格,这是 Makefile 的语法要求,编写完成的脚本
8 ?: ]! r; D0 c1 G3 B8 E6 ]! \4 m如下图所示:
9 C0 b1 K$ _$ {8 c$ H7 y7 D
3 H6 U @. y* K
编写好 Makefile,保存并退出,然后我们在终端输入“make”命令来编译我们的工程,make 命令会在
! [' ^& \& a" K$ s, Q当前目录下查找“Makefile”文件,如果存在的话就按照 Makefile 里面的规则进行编译,如下图所示:
$ I3 \$ v; f# y9 y) J
! M9 X) G7 l. ]通过上图可以看到编译产生了 main.o、calc.o 和 main 执行文件,说明编译成功了。接下来我们修改
( O9 c% v3 ^) f" L) E下 main.c 这个文件,如下图所示:0 j3 M9 l. J; ~6 l9 V
* p2 j9 P5 M4 p @
然后保存并退出,然后在终端输入“make”再次编译下工程,如下图所示:
! v5 j5 E$ Z( E9 N' C, l1 ]" ~/ r4 `! W
! ~! S" V. p- u3 D+ v
通过上图我们可以看到只重新编译了修改的 main.c,并最终重新链接生成可执行文件 main,我们在终
: C- D& E5 W$ v; `0 _5 x端运行可执行文件 main,如下图所示:& R, Z) ]+ U1 ~- y, o
7 c. Y( F2 I* q从上图的运行结果可以看到最后的结果等于 10 了,和我们程序的设计结果是一样的。
3 i8 p' j+ y- Y4 m. k3.4 e Makefile 语法 语法5 s I' T" a& c
3.4.1 初识 Makefile
& N: G o. J8 HMakefile 文件是由一些列的规则组合而成的,格式如下:
3 R) r) b4 c, k6 b) }5 V! ftarget(目标文件) ...: prerequisites(依赖的文件) ...# ^: S) z/ H/ t/ {/ O5 {
command(命令)
# |9 {$ \4 ^& _: x# L; v...4 r7 v$ r7 r$ [- J( m' h# B
..., i) y: d. E" _+ f5 S
比如 3.3.2 中写的 Makefile 的规则:
2 |4 f; |% ~2 k( Z& S+ p( s1 Pmain.o:main.c
! R3 x2 \8 ^5 L) B" o/ r) ogcc -c main.c4 O0 y N( Z% S- \1 {
这条规则的 main.o 是目标文件(将要生成的文件),main.c 是依赖的文件(生成 main.o 需要的文件),
% Z7 s1 U9 }5 h' n# X“gcc -c main.c”是生成 main.o 需要运行的命令。e Makefile 中每行的脚本如果有缩进的情况,必须使用
" T/ K. i) h# y/ L& G6 U“ Tab ” 键缩进,切记不能使用空格缩进(这是 e Makefile 的语法要求),大家一定要切记!3 I2 P$ J! O1 e3 r1 ^6 o3 ^
下面我们来分析一下图 3.3.2 章节中写的 Makefile 文件,脚本如下:. m! N5 i$ C8 J- N* A" W9 y
1 1 main:main.o calc.o) ?8 W- p2 {7 z# }, ~4 m! d" S& w
2 2 gcc -o main main.o calc.o3 c: q! J6 D6 @7 j* R. p
3 3 main.o:main.c
3 b% W, X$ N) M3 m! }4 4 gcc -c main.c
: o8 e8 b' @% t* x' }6 }* i5 5 calc.o:calc.c
9 u, S- a7 h$ P) q4 U/ {' E6 6 gcc -c calc.c. y$ _& V! G5 L! Q9 j2 g
7 7* u. L5 q+ ?' s3 h
8 8 clean:
l ^) f$ N( l: T& k9 9 rm -rf *.o; ^0 O# H% O+ y( d
10 rm -rf main
6 ]; Q3 j/ L& s; ^8 E该脚本一共有 4 条规则,1、2 行是第一条规则,3、4 行是第二条规则,5、6 是第三条规则 8、9、10
$ p$ K9 ?% P, j q4 l3 C是第四条规则。我们在运行 make 命令的时候,会解析当前目录下的这个 Makefile 文件里面的规则,首先
. c5 n9 s" p8 U0 _) A$ G解析第一条规则,第一条规则中的目标文件是 main,只要完成了该目标文件的更新,整个 Makefile 的功能
' W. n& j M3 i+ p5 c, T就完成了。在第一次编译的时候,由于目标文件 main 不存在,则会解析第一条规则,第一条规则依赖文件7 v$ u7 w5 t+ d- B) p7 `
main.o、calc.o,make 命令会检查当前目录下是否有这两个.o 文件,经过检查发现没有,然后 make 会在
% ^ \5 H! r1 p/ G7 D. RMakefile 中查找分别以 main.o、calc.o 为目标的规则(第二条,第三条规则)。执行第二条规则依赖的文; z1 g' R/ U4 M+ b! S$ O0 M
件是 main.c,make 命令检查发现当前目录下有这个文件,然后执行第二条规则的命令“gcc -c main.c”2 V' f# ~; z \0 |. Y
生成 main.o 文件。然后执行第三条规则,第三条规则的目标文件是 calc.o,依赖的文件是 calc.c,make2 w' l; a2 H8 w) a- t' K C, S* }
命令检查发现当前目录下存在该文件,然后执行第三条规则的命令“gcc -c calc.c”生成 calc.o 文件,
& Y% _# |3 @. |至此第一条规则依赖的 main.o、calc.o;两个文件已经生成了,然后运行第一条规则的命令“gcc -o main
" E7 U' V8 a! B) H. Pmain.o calc.o”生成 main 文件。因为 make 命令运行的时候会从 Makefile 的第一条规则开始解析,然后
6 Z! x G5 o( T! c根据第一条规则的依赖文件去遍历文件中的“对应规则”,然后在根据“对应规则”的依赖文件去遍历“对
1 t* E" m5 b# d8 f6 g7 v* w应的规则”,采用这样递归的方式会遍历出完成第一条规则所需要的所有规则。下面我们来看看第四条规) H) ^; A* F4 c
则的目标文件是 clean,我们通过查看发现该规则与第一条规则没有关联,所以我们在运行 make 命令的时" D B# n% D% {9 Y6 w L
候,不会遍历到该规则。我们可以在终端输入“make clean”命令来运行第四条规则,第四条规则没有依! ]8 y7 I1 X" J( q' ~
赖的文件,所以执行运行命令“rm -rf *.o”和“rm -rf main”,这两条命令的功能是删除以.o 问结尾的
u9 @, d2 J" F, }4 z+ T* u& d所有文件,删除文件 main,运行如下图所示:
) J. `" C! S Y$ M
1 `8 }1 f. i8 K6 F' _; R4 E
通过上图可以看到 main.o、mcalc.o 和 main 三个文件已经删除了。通过该规则我们可以清除编译产生
( Q/ k% A' T+ J5 u! c的文件,实现工程的清理。
c0 _5 A! |' B8 e7 b/ V/ i# r我们再来总结一下 make 命令的执行过程:
( `1 P! N, b: x" b5 T3 f% u& O7 k1.make 命令会在当前目录下查找以 Makefile 命名的文件, p' s: Q. }) l, V2 H
2.找到 Makefile 文件,就会按照 Makefile 里面的规则去编译,并生成最终文件
# p4 r6 U z! p, z6 b) f( M- e3.当发现目标文件不存在或者所依赖的文件比目标文件新(修改时间),就会执行规则对应的命令来更新。$ \+ {' f/ \2 K4 C
我们可以看到 make 是一个工具,他会通过 Makefile 文件里面的内容来执行具体的编译过程。
9 c( i1 H- x+ M0 B! I* ^: m3.4.2 Makefile 的变量
i6 e9 A/ {, T5 |' D- o在 3.3.2 章节中的 Makefile 第一条规则:: |' Q8 _1 Z# X0 K' A7 `1 c' A& o
main:main.o calc.o5 C. t) n7 Y2 C8 X
gcc -o main main.o calc.o, a# N5 J, F- m" G4 S. O# }
在该规则中 main.o、calc.o 这两个文件我们输入了两次,由于我们的额 Makefile 文件内容比较少,
. l! O; e6 G; [$ Z, Z如果 Makefile 复杂的情况下,这种重复的输入就会非常占用时间,而且修改起来也会很麻烦,为了解决这
/ X6 d+ r* O3 d- X) t. E3 `个问题,Makefile 可以使用变量。Makefile 的变量是一个字符串。比如上面的规则我们声明一个变量,叫& d4 X- R8 l, N; D1 f# W. b* N9 _
objects,objs 或者是 OBJ,反正不管是什么,只要能够表示 main.o、calc.o 就行了,我们修改上面的规' s F j! X& W1 W7 B8 q2 L# r
则
, F5 L) N; [9 [0 e4 `1 1 objects = main.o calc.o" s' @' a1 h, J% X3 N
2 2 main ( objects) E! x: D5 e& f$ h! m! e1 D4 ~: d* H
3 3 gcc -o main $( objects)8 a1 c- h! C( q
我们来分析下修改后的规则,首先第一行是我们定义了一个变量 objects,并给赋值“main.o calc.o”,
! j& M; s7 |0 B1 P第二行、第三行用到了变量 objects。Makefile 中的变量引用方式是“$(变量名)”,变量 objects 的赋值
/ {7 I& d8 O/ Y# W2 {使用“=”,Makefile 中变量的赋值还可以使用“:=”、“?=”、“+=”,这四种赋值的区别如下:7 g2 f+ ] z2 W- j' _
1. “= = ” 赋值符9 M6 @# G1 r) U
我们先在用户根目录的 work 目录下创建一个 Makefile 脚本,输入下面的内容:
r: ?2 ~; [3 P" C1 ceshi1 = test
' H: D* u; L+ y2 ceshi2 = $(ceshi1)1 g# c' S& G# {) E8 i' A* Y
3 ceshi1 = temp# W2 T v2 U* A9 t
45 j5 _' k' w1 O9 T# d' X4 W
5 out:
3 {- {5 w4 z5 \1 V5 h6 @Echo ceshi2 (ceshi2)& K6 m% z( I6 B, P; o4 y
第一行我们定义了变量并赋值“test”,第二行定义了变量 ceshi2 并赋值变量 ceshi1,第三行修改变量
4 i' i& D1 i; zceshi1 的值为“temp”,第五行、第六行是输出变量 ceshi2 的值。我们在终端输入“make out”命令,如
: }: t* g n# f2 D, n1 K$ ]5 a下图所示:3 G/ i, z9 N6 f8 T4 q8 [$ d
% z" u: t# Z- z+ f0 ~# P1 ~6 E
在上图可以看到变量 ceshi2 的值是 temp,也就是变量 ceshi1 最后一次的赋值。
* g3 z8 t( A% j& g8 E2. “ := ” 赋值符
5 ]" P4 F9 A: M8 ?我们修改“=”赋值符中的代码,第二行的“=”改成“:=”,代码如下:* _$ I" e9 b: ]$ }6 T/ n+ F& H
1 ceshi1 = test
7 n: Y. z& @9 z9 L* h2 ceshi2 := $(ceshi1)! i3 |+ N# j& m5 \3 ?3 W5 v+ `
3 ceshi1 = temp: h! D0 ^" {9 Y; A
49 [7 B! d% D; M
5 out:
* F. K/ H: S6 z. I4 ~( k% |0 e6 @echo ceshi2 (ceshi2)
' T: R% y8 ?; Q/ v% s) H$ C我们在终端输入“make out”命令,如下图所示:# f7 K2 b7 j3 c! \# r" G9 @( l: n
3 o# ^; ]5 M2 |7 }6 Y K. ]我们可以看到上图的运行结果输出变量 ceshi2 的值是 test,虽然在第三行我们修改了变量 ceshi1 的- W% R/ c1 @5 o0 s1 ?- b8 h. z
值,通过本实验我们可以看到“:=”赋值符的功能了。
: e2 o7 u) \/ H5 [8 b3. “ ?= ” 赋值符
' A* J9 U; W# u1 l! mceshi ?= test; _' {4 v1 |( W7 P/ V
“?=”赋值符的作用是如果前面没有给变量 ceshi 赋值,那么变量就赋值“test”,如果前面已经赋值了,2 l, a/ G, X2 Q W
就使用前面的赋值。
; | K6 K- r! [1 }1 c! j+ f4. “ += ” 赋值符
2 {/ m0 ^: r, g" Uobjs = main.o6 X5 z7 R3 Q# \, M. y# v" D+ ~
objs += calc.o& o W6 I+ ?4 q1 D$ Z h
上面的脚本最后变量 objs 的值是“main.o calc.o”,“+=”赋值符的功能是实现变量的追加。$ p5 Q- d$ }/ m$ V* m+ p) s" e( `
3.4.3 条件判断3 L; n& l7 E* H# o$ e% ?4 B
使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量5 Q) P2 ]9 A) r2 j9 c+ d* h1 ~
的值,或是比较变量和常量的值。其语法有下面两种:! x0 |8 K& V9 W
1.
8 @( N, m5 D7 q$ c, r) p; u7 P/ i) M; p<条件比较>; p& [. i# p' R9 t' U/ b
[条件为真时执行的脚本]1 w/ c: I+ u; h3 ]. }* W( M- _' t/ @
endif" Q5 w- z: A9 o2 Q, G _
2.( I/ U& a/ b1 E1 s$ F" r; G# e
<条件比较>' w6 {$ H/ R6 h, w+ }9 _8 r& W
[条件为真时执行的脚本]
$ @ f- _% Y h$ ?* W6 ? }else
' l) u% ]' Q! L/ x: O[条件为假时执行的脚本]
* _; m. V4 G7 J* c6 k. p( H% mendif! i2 v, ^+ g, d3 j' Q6 n8 \# ]9 \. g. O* w
条件比较用到的比较关键字有:ifeq、ifneq、ifdef、ifndef。6 |9 i* V* C# A, Z* F7 G @ v
ifeq 表示如果比较相等,语法如下:. @6 j& c& I$ c1 H3 G
ifeq(<参数 1>, <参数 2>)
/ g; j' v2 p* a/ n4 Y$ Sifneq 表示如果不相等,语法如下:
! s2 o4 J! D& b$ u2 B, c% w# jifneq(<参数 1>, <参数 2>)5 {; k$ t( _" }4 k/ r! p) a2 w; q
ifdef 表示如果定义了变量,语法如下:
U$ k& e$ F g7 Nifdef <变量名>/ L" ]8 C! _$ o0 U
ifndef 表示如果没有定义变量,语法如下:
% \* Q3 k8 E# ^+ R% Bifndef <变量名>3 Z3 h4 L; r7 Z% Q
3.4.4 使用函数- H1 I* a; L! G
在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所
( `1 ^* a; X' L) t5 Y t% V8 S8 o# T支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。
) |! Z0 I+ V3 V& o; z# G函数的调用很像变量的使用,也是以“$”来标识的,语法如下:! I) ~% e! a# o0 d, x4 o- L R% {
$(<函数名> <参数集合>)
2 h, L e: Q9 a# _或者:3 T4 g! ^, m( M
${<函数名> <参数集合>}1 u2 I: ?; ~/ W g/ H/ I4 {9 \
函数名和参数集合之间以空格分隔,参数集合的参数通过逗号分隔。函数调用以“$”开头,以圆括号或花
+ J p4 ~9 q" G/ r( m# Y括号把函数名和参数括起。感觉很像一个变量。函数中的参数可以使用变量。为了风格的统一,函数和变$ @+ j# u6 w5 B6 P7 s: ?
量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。& H7 X" r( ?/ `& ]6 G2 \1 a
因为统一会更清楚,也会减少一些不必要的麻烦。
/ s& u$ `( N% d. J接下来我们介绍几个常用的函数,其它的函数可以参考文档《跟我一起写 Makefile》。6 ]6 U ]+ d) y0 \1 A9 O* l
t 1.subst 函数* k6 e8 ]. s8 S3 l5 n2 l
$(subst ,,)
4 Q( G6 W4 B. l6 }' i5 q0 i, D此函数的功能是把字串中的字符串替换成,函数返回被替换过后的字符串。如下示例:
: D8 e( t/ X' @" ~$ P2 _$(subst ee,EE,feet on the street)" l% ~& r1 K) _# G {, k
以上脚本实现把字符串“feet on the street”中的“ee”字符串替换成“EE”字符串,替换后的字符串! F, r( M3 _: Z: n9 m" a6 q; F5 D0 \
为“feet on the strEEt”。
# |1 X f+ v; A. c7 |1 `5 M/ O. 2. t patsubst 函数
) b: G1 \7 [ ]* y8 ] u$(patsubst ,,)
( o( u5 T9 R J4 Y此函数的功能是查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式
5 x5 ~ N( w! l- z# R,如果匹配的话,则以替换。这里可以包括通配符“%”,表示任意长
l2 }# M0 x9 M; h/ K2 F* h: v度的字串。如果中也包含“%”,那么中的这个“%”将是中的那个
' g5 m( z5 C9 ~. w7 X) ~: o“%”所代表的字串。(可以用“\”来转义,以“\%” 来表示真实含义的“%”字符)。函数返回被替换9 e7 g1 ` U+ v7 r, {( B
过后的字符串。如下示例:
4 {' e. e) Q! L3 S# D6 A$(patsubst %.c,%.o,x.c bar.c)" H" O+ J: t: e, w! _
以上脚本实现把字串“x.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.o bar.o”9 ~4 d# z5 A m1 d- w6 |$ o
p 3.strip 函数1 U" Q0 E# Q0 L+ }8 |: e
$(strip )
9 E n8 ^& M. \此函数的功能是去掉字串中开头和结尾的空字符,函数返回被去掉空格的字符串值。如下示例:5 K* R5 h. D# I& |3 h
$(strip a b c )
1 w: v# [: M- M; o: ?4 q5 ]* j* ~以上脚本实现把字串“a b c ”去掉开头和结尾的空格,结果是“a b c”。- i5 f `* |( i( A2 p. U/ z
. 4. g findstring 函数8 A7 T3 O( |. M1 ^& J5 T
$(findstring ,)/ I2 Z- s" A% T, ^- k. u
此函数的功能是在字串中查找字串,如果找到,那么返回,否则返回空字符串,如下示
, o2 t" O# {, R" X) n例:
( I, [: t$ J, U9 Q2 o2 G5 K& p+ U$(findstring a,a b c)
5 s% R7 D$ v' d: Q$(findstring a,b c)
7 Y, U& W3 f: c' t; S, P以上脚本,第一个返回“a”字符串,第二个返回空字符串。
' a$ Y& M" v5 t: x* l4 g1 x% \( kr 5.dir 函数
2 Z9 R& O( x* |, ]$(dir )
5 E7 f5 b( r3 S. O9 E4 }( t此函数的功能是从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部
' h4 d7 l0 I a. h% g分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分,如下示例:
# |5 X% D5 x+ ?1 P9 H( I1 w$(dir src/foo.c hacks)( f; j; a" q8 }( ^( f& I3 {
以上脚本运行结果返回“src/”。7 g5 g ], ?8 l6 j& }" s9 g
. 6. r notdir 函数6 g& u: G2 e. u
$(notdir )6 b5 b8 e* j/ O+ I; h& s
此函数的功能是从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后
& h1 }/ d, O0 r+ K; u的部分,返回文件名序列的非目录部分,如下示例:
: Z1 |! c' @/ n1 K q* c/ p$(notdir src/foo.c)
( G' c: T r9 B/ n' i; ?4 Z9 A以上脚本返回字符串“foo.c”) d# `0 U9 Y: w- Z& _4 D
. 7. h foreach 函数2 `: ~3 h% t4 l7 G' n
$(foreach ,,)
- w! l7 G$ l3 u% w5 X$ R6 I8 U此函数的功能是把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含
. E. E$ B# {! P1 G6 |. H的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,
3 X" v8 a6 C# ?9 `+ r& ]最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函8 U) o5 \! j, V4 b% M
数的返回值。所以,最好是一个变量名,可以是一个表达式,而中一般会使用这
' c# }. d& E/ h0 Z个参数来依次枚举中的单词。如下示例:! Q+ ~/ P/ I8 r2 o( Z. |8 ?
names := a b c d7 y( X# z1 R4 m" j2 N
files := $(foreach n,$(names),$(n).o)
4 r) R* N/ W% Z% o$ n以上脚本实现$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出
, K: _' W$ W; K6 M# |% e; R7 s2 f一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以$(files)的值是“a.o b.o c.o d.o”。1 |$ Y1 N' u6 `6 R
(注意,foreach 中的参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不
8 A/ T8 [+ f& ]$ w9 j6 U. `在作用,其作用域只在 foreach 函数当中)。
( ~1 F: T. Q( m1 `5 \6 n更多内容关注迅为电子
" x$ p) l0 \5 g5 ` |
|