|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
我们在 Windows 下使用 C 语言开发的时候,一般都会有支持 Windows 的开发工具,比如我们学习 51 单! [. k% ^9 r9 h! c/ |
片机或者 STM32,所使用的的 Keil 开发软件。此类开发工具一般都会集编辑、编译于一体,我们只需要编辑1 d- d$ x4 N8 D( I& {" O
好代码,电机开发工具的编译按钮,就可以自动为我们编译出可执行的二进制文件了。Ubuntu 下的 C 语言
& J+ ~0 c+ Z0 t开发与 Windows 下的 C 语言开发是不一样的,并没有图形界面的开发工具,而且编辑和编译是分开的。我8 f7 w0 s+ a; H9 d. |6 d
们需要使用文本编辑软件先编写代码,然后使用编译工具(GCC)来编译,最终生成可执行的二进制文件。- j) m% t% \9 _6 K# J* w
如果我们的工程中有多个源文件,在编译的时候我们通常会通过 Makefile 文件来管理整个工程的源文件。! k; T# f2 M+ V8 G5 A3 a x
本章我们来学习如何在 Ubuntu 下进行 C 语言成的编辑,GCC 编译,Makefile 文件的使用。通过本章学习我) Z' }& f: y; j2 D
们可以掌握 Linux 下的 C 语言开发基本方法,为后面的学习做好准备。
9 J" i6 e* _( v, |1 U; [3.1 x Linux 下编写第一个 C C 程序 程序0 d0 \4 @+ X) t2 j7 G
本章节开始的部分我们介绍了 Ubuntu 下 C 程序开发分成两部分:编辑和编译。Ubuntu 下有很多种文本
& @, e; w: n/ w; k6 B编辑的工具,如 vim、Gedit、Emacs,这里我们使用前面章节介绍的 vim 文本编辑工具来编辑 C 程序。相信
6 P4 q3 H) u7 n* U- b大家在学习 C 语言的时候都是从“Hello World”这个程序开始的吧,下面我们通过这个程序,来学习下 Linux
# q/ v! g) j# i( v* Z下 C 程序开发的流程。
& c: a3 P# I: a0 B% y3.1.1 代码编写/ K' N0 Y: ~) n( {4 z
首先我们在用户根目录下建立文件夹“work”,用于保存所有 C 语言的程序,运行结果如下图所示:
6 Z% Q0 u j! d, y b
4 X/ h- {/ S: w! H) y8 E然后进入创建的文件夹 work,为了方便管理,我们每个例程都创建单独的文件夹,首先我们创建文件$ L) O6 x; U: M& q
夹“hello_world”来保存我们的第一个 C 程序,如下图所示:# y; u3 ` l& d9 w
5 N$ {& H J. z0 F然后进入上图中的 hello_world 文件夹,使用 vi 命令新建文件“main.c”,然后在里面输入下面的代7 Y0 y! W0 m, Q
码:
$ E. w! T/ J2 j! S) l) O1 V. \, t#include ' G4 y9 O* W9 v) l4 p0 b
int mian(int argc, char *argv[])+ e9 v, ?' Y5 b3 Q6 Y7 M
{* B# Y7 v! z( M- T: n7 u
printf("Hello World!\n");. J0 F: e9 R. H) G5 u. r8 c7 H
return 0;" r6 p! w9 o! b" x
}; j" O) V) }/ q: v* \2 V6 }* M
编写完以后保存并退出 vim 编辑器,然后可以使用 cat 命令查看代码是否保存成功,如下图所示:
$ m5 x2 Q. Q2 Q* g- j
3 R v4 d, K+ u% ]6 t) u: h通过上图可以看到代码已经编辑完成了。
% N5 E+ r" d- R4 Y. H9 R3.1.2 代码编译* C; q# ~. M$ M Q$ J
Ubuntu 下使用 gcc 编译器来编译 C 程序,我们在安装 Ubuntu 系统的时候,gcc 编译器 morning 安装好
d$ ~, G+ Z* h& }3 U3 ^4 ]$ L; w了,我们可以在终端输入“gcc -v”来查看下 gcc 的版本,如下图所示: e/ q% E2 D' b& ?& a0 }
$ _, m# u% o7 S9 m' q# d1 _# E
通过上图可以看到 gcc 的版本信息,说明我们的 Ubuntu 上已经安装了 gcc 编译器了,下面我们来看看8 m$ x& l0 ~3 z% f& W
怎么通过 gcc 编译我们的第一个 C 程序,我们在终端输入“gcc main.c -o main”,然后回车,会生成 main 文0 u$ G# V9 y0 C$ _
件,如下图所示:
2 m# T- d1 G) K4 l# s/ g/ x( ~
0 L% F9 ]/ I8 {* }3 j# p
在上面的“gcc main.c -o main”这个命令里面“-o“用来指定编译生成的文件名字,我们指定的是 main,
1 X% l: n1 T9 Z: f/ d. `7 V0 ]所以在图 3.1.2.2 中可以看到生成了文件“main”,如果我们想生成其它的文件名字,我们只需要修改“-o”
5 e6 x* ~( o! g9 P* ~后面的 main(修改成您希望生成的文件名字)。经过前面的步骤已经生成了可执行文件 main,现在我们演
, J5 v+ l7 j* k: L( K$ M4 R示下在终端如何运行一个可行性的程序,我们直接在终端当前目录下(可执行文件所在的目录下)输入/ V5 s/ t2 v0 x+ v M c! o
“./main”,然后回车,就可以运行可执行文件 main 了,运行结果如下图所示:, j1 |( D0 ^- j) S: \8 c
7 k& j9 I4 w. @* f. q% w+ I) @在上图中运行的命令“./main”,其中的“./”代表当前目录下。我们可以看到在终端打印出了 Hello
|+ y0 y: f7 {+ r, T5 n+ YWorld!。至此,Linux 下的 C 语言编辑和编译的一整套流程我们就介绍完了。
4 b# l" R* H# @0 u% ^; f3.2 c gcc 编译器 编译器' j1 ]! H* s/ f% k3 H5 O K# l
3.2.1 gcc 命令分析& C5 w- R" y, r& w4 K+ I( D+ P
在 3.1 节我们已经使用 gcc 编译了 Linux 下的第一个 C 程序,gcc 命令的格式如下:
- d1 I+ M/ g& m# U& Q9 Ggcc [参数] [文件名]: A- l! a2 y7 w/ k3 j) n% \
主要参数说明如下:" r9 R! Y& ^0 {$ |& W. H
-c 编译、汇编到目标代码(.o),不链接成可执行文件! J4 I- M k: x
-g 生成调试信息. C+ V/ o- o5 f8 F
-o 编译完成后生成的文件名,如果不使用该选项,默认生成 a.out 文件
/ [3 ]0 g f: q: j8 N-O 对程序进行优化编译,产生的可执行文件执行效率高% F! v4 K1 p$ \, h8 E0 y
-w 不生成任何警告
2 V- o7 k% a6 {; c-S 仅编译到汇编语言,不进行汇编和链接9 C* ]8 w8 }% h
3.2.2 编译警告错误处理
- f% |0 E- }% m1 C' Z. w我们是 Windows 下使用 Keil 或者其他开发工具,在编译的时候,如果程序有错误,开发工具会提示出: b( f; `+ o0 _% O' Z7 f3 V7 z! _
具体的错误信息,可以很方便的定位到问题点,快速的修改出现的问题,gcc 同样也有类似的功能,下面我/ k3 J' U5 M: v5 g& P
们来看下 gcc 的错误提示功能,首先我们在 work 目录下建立文件夹“test2”,然后使用 vim 在 test2 文件3 q" i: q) t- w. I1 L6 U! e
夹创建 main.c 文件夹,在 main.c 文件输入如下代码:1 C6 o6 b6 y5 x9 g9 H' @! @- d, `
#include ) w% _: ]" u4 C+ g8 ~ L2 K$ _
int main(int argc, char *argv[])
1 _+ l Z9 ?& N0 ^{' P! ~6 O9 T L; o9 A& I9 w
int a;
^+ a- F. _: \% @a = 1
8 H+ a; C3 _9 n$ V8 f0 I% o$ E& Gprintf("a=\n", a);
/ e [# y7 @* o) S% e0 E+ Treturn 0;, b, e) G0 ^8 y. v6 |" C1 J; C2 X
}3 F( [7 m/ e: f( f2 ?0 c- `+ q. m
上面代码有两处错误:. ]4 t$ ~# Q. B
第 7 行 最后缺少“;”& s' e: S* R% ~# b
第 9 行 printf 语法不对,应该为:printf("a =%d\n", a);
+ q% P+ o4 x7 L我们使用 gcc 编译 main.c,可以看到 gcc 会提示错误信息,如下图所示:
$ P+ ]6 D6 n2 w3 ~" i/ g: ~, o. E
- G! @* k/ d, u. m$ W( F3 g* O; h从上图中可以看到在 mian.c 文件的第 9 行 printf 前面缺少“;”我们在第 7 行“a = 1”后面加上“;”,
9 Z& D* b W1 D. e然后继续编译,如下图所示:
/ [4 q! J9 W4 T7 f+ {
% G% p/ {9 E% p) U: v从上图可以看出编译提示语法格式不对,我们把第 9 行修改成“printf("a=%d\n", a);”,然后在继续编
$ Q7 U; N7 G2 Z+ ~; j译,如下图所示:# R9 d8 p m' ?: L
, D, T: F9 J3 Z! K o; j我们可以看到这次 gcc 编译通过,最终生成了文件 main。我们在终端执行 main,运行结果如下图所示:
. ^# P! _2 Q7 P" X8 J
) |7 t: W3 u! J( T7 k
从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出; i9 m g. N( B/ T B( i
程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。
! X$ \0 K* E1 F4 N. {' x从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出
0 x* o6 i# \& m- K程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。2 s! C& _0 L3 ~! }/ s* C
3.2.3 gcc 编译流程
- }; C/ e( B7 b% Q! _3 dgcc 的编译流程可以分成四个步骤:; |1 S. j4 G( m3 K: N$ A
1.预处理,生成预编译文件(.文件)
* i' W% u8 s: L# K7 T8 g; H2.编译,生成汇编代码(.S 文件)0 \ p% \- c& q3 m
3.汇编,生成目标文件(.o 文件)+ D2 z2 w' h4 I/ g
4.链接,生成可执行文件
$ n- F9 H3 \2 j9 J3 3.3 初识 Makefile+ W# K' F5 T; w0 e0 t g
3.3.1 什么是 Makefile
+ j) F9 m4 A8 a5 A- I, e2 r0 \( U7 U在 3.2 章节我们了解了在 Ubuntu 系统下通过 gcc 编译器来编译 C 程序,在我们演示的历程中只有一个
' F/ t. g7 s7 F% W9 qC 文件,我们直接在终端输入 gcc 的编译命令,就完成了 C 程序的编译。我们在实际开发过程中,如果我们
3 Q- j8 s/ q* b8 u+ K的工程有几十个,或者几百几千个 C 文件,我们通过在终端输入 gcc 命令来编译,这显然是不现实的。为/ C8 T+ H% k; O$ h$ X
了解决这个问题我们可以使用“make”命令,它会解析 Makefile 文件中的指令(应该说是规则)来编译整
& c: @, ^7 j) {( m0 u; r; q个工程。在 Makefile 文件中描述了整个工程的所有文件的编译顺序,编译规则。9 @4 T4 a% l! g" L7 d/ a; y' B
作为一个专业的程序员,一定要掌握 Makefile 的,我们可以通过 Makefile 能了解到整个工程的处理过程
1 w+ H2 P( V9 {7 S% l的。
; f! A: I3 f2 e6 L由于Makefile涉及到很多的知识点,以至于可以单独写本书来讲述,所以本章我们只是讲解下Makefile
2 U2 s( A3 I/ Q) Q4 |( y" Z" {的基础入门,如果详细的研究 Makefile,可以给大家推荐《跟我一起写 Makefile》这个电子文档,该文档
; J2 s+ {; d) I( A; W# o6 C已经放在了:i.MX6UL 终结者光盘资料\09_其它参考资料里面了。
% a# y' T' h8 z) j* f& P3.3.2 第一个 Makefile" d5 B( C6 T* }5 f7 |6 u1 ` v
在本节我们建立这样一个工程,计算两个整形数的和,并将结果在终端显示出来。在这个工程中一共有* C$ w h, N$ e9 i6 W
main.c、calc.c 两个 C 文件和 calc.h 这个头文件。其中 main.c 是主文件,calc.c 负责接收 main.c 传过0 W1 x) k. Q$ o
来的数据,然后进行相加。main.c 文件的内用如下:, E- z" c* B5 p- {# k( y4 B7 \* c
#include 3 ^; Z# \6 m2 b9 E* ]# Z- ~4 z1 M
include "calc.h" y1 c7 y& p# a4 Z
int main(int argc, char *argv[])
3 ?8 ?' }3 ~" ]4 F* i{2 m" K6 [) A! B* H9 W. {
int a = 3, b = 6, sum;# ~+ r2 _& L6 T! V2 L: I4 J9 j
sum = calc(a, b);
% L" [8 K4 q! [1 w+ Cprintf("%d + %d = %d\n", a, b, sum);
9 a' o$ k, c$ G9 T0 X& x4 B3 ureturn 0;6 R1 w2 z) ^7 T4 z0 {. a% G
}
3 n- |- E, Z8 \calc.c 文件内容如下:, e; m8 y" v6 K9 B9 a+ {0 O, }
#include ( S7 e0 R' {, D* t# T& a6 ?5 d% @
int calc(int a, ing b)! z" `% S+ Y. p0 J. X) |
{
4 `# R5 H+ p& N6 J7 Mreturn (a+b);
. S+ j* w5 l; I' X- j. u7 X} }; H! E8 h6 c' R7 D: [
文件 calc.h 内容如下:- p3 d+ |: F+ `; x! ]' X
#ifndef _CALC_H% U: D9 H @, p% a, y( D
#define _CALC_H1 h1 U* Q, n! c( [* |
int calc(int a, int b);
4 @1 E @4 W. g5 ]' A+ R4 h/ `3 L& z1 N#endif
5 R& p/ j( E* r$ p5 `: v( t上面就是我们这个工程的所有源文件,我们在终端使用 gcc 编译这个工程,在终端输入“gcc main.c
" h! l% p( J& S5 q* J; I( ecalc.c -o main”,该命令的意思是用 gcc 对 main.c、calc.c 进行编译,然后输出可执行文件 main,运行/ S0 o. E. @5 T+ g0 Q# y; |/ h- y
结果如下图所示:
" x3 }* R" E+ c% p* t& W- l2 x
" q5 I0 \: J D9 l通过上图可以看到生成了可执行文件 main,我们在终端运行 main 执行文件,运行结果如下图所示:
0 U; I8 l1 ~6 G" ]; b7 Q
: C# U. G% m2 i3 d. U) U
我们可以看到上图的运行结果和我们设计的结果是一致的。由于我们的这个工程是有三个文件,如果5 d# i& D7 I. D2 N4 J1 ]
工程有几百个,几千个的文件,或者如果有一个文件被修改,使用上面的命令将会编译所有的文件,如果" y! G0 m. i0 {% H$ Y' }+ P
我们的工程有上万个文件,编译完一次工程所需要的时间就很可怕。最优的方法就是编译过一次以后,如6 g( ?+ \9 J. n3 }* q
果后面在编译,只编译修改的文件,这样就会节约很多时间,因此我们修改下编译方法,命令如下:
& M& K( y' S/ H& @+ [7 i, V G( {3 igcc -c main.c1 H# C7 f, t" \$ q) u% s: ]
gcc -c calc.c( T3 v3 H4 Q1 @: H' G" l4 Y% V( g; K
gcc main.o calc.o -o main
. ?# Y; R3 F* `7 }我们在终端输入上面的命令,结果如下图所示:
) _0 w; v# a; s7 z- Q0 S3 p: W9 S
9 h5 G/ t4 R* C# s6 [0 j
上图的第一条和第二条命令里面使用了参数“-c”是把 main.c 和 calc.c 编译成对应的.o 文件,最后# i* X& R0 a$ w' o, T/ C
一条命令是把编译生成的.o 文件链接成可执行文件 main。假如我们修改了 main.c 这个文件。只需将 main.c3 a* f0 F( j( S
这个一个文件重新编译下,然后在把所有的.o 文件重新链接成可执行文件,对应的命令如下:0 o) c' I# ~5 A5 r' k$ F
gcc -c main.c
! u- t: O' S: D/ @- S Kgcc main.o calc.o -o main. d* G5 t* w R4 K
可是这样还有一个问题,如果需要修改的文件有很多,这样工作量也会不小,所以我们需要一个工具:
; L8 `7 H; d- e* ]% w8 Y# ~# \5 _1.如果工程没有编译过,就会把工程中的.c 文件全部编译并连接成可执行文件, G7 D* i& v; p: Y7 R: {8 b3 h, W
2.如果工程中有某些文件修改了,只编译修改的文件并连接成可执行文件
. s! R% q( L8 w! e3.如果工程中的头文件修改了,那么就要编译所有引用这个头文件的.c 文件,并且连 接成可执. S; ^( I4 ]; ]3 M5 R4 O: E
行文件
% k" C' J) q' ?3 s# y% `我们开头说的 Makefile 就是完成这个功能的,下面我们在工程中建立一个 Makefile 文件来实现这样的功
: C/ q( y+ \' L a能(注意:文件名字必须为 Makefile,大小写是区分的)。我们使用 vim 创建 Makefile 文件(Makefile
: D1 b5 K5 T. l* b2 K和我们的 main.c、calc.c 在同一级目录下),然后输入下面的脚本:
/ p$ `. F& s) L/ U5 J1 _4 t& A3 Emain:main.o calc.o
1 a1 V/ M( ]; V2 S& wgcc -o main main.o calc.o
+ V4 j$ S/ S8 B! P& Umain.o:main.c
9 ]' o3 t1 g! `1 D# T6 Fgcc -c main.c# W( R; [: z2 I, ?
calc.o:calc.c
5 o' x0 x2 [: n! i+ f8 `gcc -c calc.c
! v7 w# ~9 j: M/ @6 `clean:* M: l' A6 R# d, |2 \4 d) K
rm -RF *.o
5 V% t0 ~/ J9 f/ erm -rf main
6 e' m) C7 Y) n" ]1 S" W8 _上面脚本缩进的行需要使用“Tab”键缩进,不要使用空格,这是 Makefile 的语法要求,编写完成的脚本
1 \1 R% f* l3 d6 H0 S- \如下图所示:
% ?9 N" {, e9 d6 Z* @1 z
) i. X/ r) k. m- x6 X* _编写好 Makefile,保存并退出,然后我们在终端输入“make”命令来编译我们的工程,make 命令会在
. b* G+ u6 A: B4 L3 i( \- T当前目录下查找“Makefile”文件,如果存在的话就按照 Makefile 里面的规则进行编译,如下图所示:. g6 w+ R- p- w K+ D
& K) ~) u- Z1 j: y: `( b+ ], F. {通过上图可以看到编译产生了 main.o、calc.o 和 main 执行文件,说明编译成功了。接下来我们修改
6 ]1 C/ P8 f7 n8 L下 main.c 这个文件,如下图所示:
; |7 ~' |4 s' U3 g- K7 v
1 D* q) g# ?8 S0 k9 _9 n1 K然后保存并退出,然后在终端输入“make”再次编译下工程,如下图所示:
/ w0 c8 @/ w& x0 }9 ]" ?
: e# n4 g. Z% S9 K
通过上图我们可以看到只重新编译了修改的 main.c,并最终重新链接生成可执行文件 main,我们在终5 ] p0 v( }9 M
端运行可执行文件 main,如下图所示:& a9 F b7 }6 t3 G, _- }/ H
9 m. Q/ o7 ~, W+ I8 s7 h从上图的运行结果可以看到最后的结果等于 10 了,和我们程序的设计结果是一样的。7 y; O. f9 v9 A) J/ ^, X5 b1 x8 Q
3.4 e Makefile 语法 语法+ _' c% G* a+ o f! j/ k$ a
3.4.1 初识 Makefile3 E7 l! y# x% [3 N4 ~% A- b
Makefile 文件是由一些列的规则组合而成的,格式如下:* H2 _, y% N9 B; n
target(目标文件) ...: prerequisites(依赖的文件) ...
0 }* K+ P$ E; Q F) w6 {, Ucommand(命令): | Y# Y. |, ~% {, l) Q0 v0 c& Y
...
/ B. M4 C3 s) N2 ?- I* ~...
( U5 n! N: T) ^. j比如 3.3.2 中写的 Makefile 的规则:: m; V, J0 O, `) f9 ~* h- I
main.o:main.c
+ n5 ~5 L# s z8 @6 ~gcc -c main.c
# M* Y; n7 J9 n' k8 e6 I这条规则的 main.o 是目标文件(将要生成的文件),main.c 是依赖的文件(生成 main.o 需要的文件),
1 d5 z( \1 o) \& ?6 H# R+ g“gcc -c main.c”是生成 main.o 需要运行的命令。e Makefile 中每行的脚本如果有缩进的情况,必须使用" h8 A5 Z/ s3 N
“ Tab ” 键缩进,切记不能使用空格缩进(这是 e Makefile 的语法要求),大家一定要切记!+ g9 ` f% l! R0 q6 F2 z
下面我们来分析一下图 3.3.2 章节中写的 Makefile 文件,脚本如下:3 p- F; ~+ h' V+ m: d
1 1 main:main.o calc.o; q# q3 @$ o, i& y
2 2 gcc -o main main.o calc.o
; t6 ^$ ~/ m& `, L- w3 3 main.o:main.c
$ m/ A4 P+ F* b r/ k9 a4 4 gcc -c main.c( g8 L: l, n! C- l
5 5 calc.o:calc.c
8 M. h9 Z) V" u6 6 gcc -c calc.c% T. m6 y- E' @
7 7
; }. k% }0 d2 v1 r8 Q8 8 clean:
4 @$ [( P1 X9 x7 y( S% a9 9 rm -rf *.o, f! p! d V0 ~( y8 i _, O* m
10 rm -rf main
5 M/ S1 `) j! s7 W! D" V2 H该脚本一共有 4 条规则,1、2 行是第一条规则,3、4 行是第二条规则,5、6 是第三条规则 8、9、10
) |/ E, D/ s+ Z8 q是第四条规则。我们在运行 make 命令的时候,会解析当前目录下的这个 Makefile 文件里面的规则,首先
. _- R/ f8 K M; T' j+ {解析第一条规则,第一条规则中的目标文件是 main,只要完成了该目标文件的更新,整个 Makefile 的功能/ X+ N6 }5 B# o+ f; u1 v! T5 Z7 O0 R
就完成了。在第一次编译的时候,由于目标文件 main 不存在,则会解析第一条规则,第一条规则依赖文件: q; e- K6 K1 B- E1 D
main.o、calc.o,make 命令会检查当前目录下是否有这两个.o 文件,经过检查发现没有,然后 make 会在
3 P! `2 h1 y# u2 R/ x! V$ kMakefile 中查找分别以 main.o、calc.o 为目标的规则(第二条,第三条规则)。执行第二条规则依赖的文' s+ d7 @# |3 J
件是 main.c,make 命令检查发现当前目录下有这个文件,然后执行第二条规则的命令“gcc -c main.c”# n' l- [2 ?. n# h6 [& o) W! G! _
生成 main.o 文件。然后执行第三条规则,第三条规则的目标文件是 calc.o,依赖的文件是 calc.c,make* V; x* b& Y8 {$ E) ^3 x6 A
命令检查发现当前目录下存在该文件,然后执行第三条规则的命令“gcc -c calc.c”生成 calc.o 文件,
% L) t7 @2 ?" q至此第一条规则依赖的 main.o、calc.o;两个文件已经生成了,然后运行第一条规则的命令“gcc -o main- {6 R' B4 p; D+ h ~ y
main.o calc.o”生成 main 文件。因为 make 命令运行的时候会从 Makefile 的第一条规则开始解析,然后5 E# {' c" w8 h! R2 h& H
根据第一条规则的依赖文件去遍历文件中的“对应规则”,然后在根据“对应规则”的依赖文件去遍历“对
4 p) S+ m' Q( ~. ~7 b5 G# h应的规则”,采用这样递归的方式会遍历出完成第一条规则所需要的所有规则。下面我们来看看第四条规* n% ]4 M' x& h4 K; W9 @
则的目标文件是 clean,我们通过查看发现该规则与第一条规则没有关联,所以我们在运行 make 命令的时
5 L! O0 M6 x- z- z- c& C候,不会遍历到该规则。我们可以在终端输入“make clean”命令来运行第四条规则,第四条规则没有依
# l. g! Z! d+ M2 r赖的文件,所以执行运行命令“rm -rf *.o”和“rm -rf main”,这两条命令的功能是删除以.o 问结尾的: v' W! f B0 C# B, ?
所有文件,删除文件 main,运行如下图所示:. Y8 C+ v: o1 I. u! l1 S
+ X6 Q2 T' B( X# j# l, i, R通过上图可以看到 main.o、mcalc.o 和 main 三个文件已经删除了。通过该规则我们可以清除编译产生
0 b+ s! k9 N3 a, T' q# P4 [; {! v" b& d的文件,实现工程的清理。7 T8 F; t) _! K4 `# W
我们再来总结一下 make 命令的执行过程:
+ C3 d3 v; V$ U9 a1.make 命令会在当前目录下查找以 Makefile 命名的文件/ j* U- E! {: I# @* }
2.找到 Makefile 文件,就会按照 Makefile 里面的规则去编译,并生成最终文件( |4 a( ?& X! E, h
3.当发现目标文件不存在或者所依赖的文件比目标文件新(修改时间),就会执行规则对应的命令来更新。! f, B, B, u. @* O# z6 j4 F& \) q' Z7 s
我们可以看到 make 是一个工具,他会通过 Makefile 文件里面的内容来执行具体的编译过程。. N/ [+ g& {8 I
3.4.2 Makefile 的变量9 s( c# V5 J, V7 V5 x& O
在 3.3.2 章节中的 Makefile 第一条规则:+ X& G7 \; H, T8 f3 \) N6 T
main:main.o calc.o
' N/ D% x; E4 C+ k+ E- v$ b2 A5 u, ?gcc -o main main.o calc.o8 \( q# d0 E5 w, @
在该规则中 main.o、calc.o 这两个文件我们输入了两次,由于我们的额 Makefile 文件内容比较少,
# \9 _4 `( e @. s" {, Q1 M8 j+ f如果 Makefile 复杂的情况下,这种重复的输入就会非常占用时间,而且修改起来也会很麻烦,为了解决这5 w9 R7 [* m; s4 l2 v4 S: v* B
个问题,Makefile 可以使用变量。Makefile 的变量是一个字符串。比如上面的规则我们声明一个变量,叫% C# o1 k& Y E3 W! \* y$ L
objects,objs 或者是 OBJ,反正不管是什么,只要能够表示 main.o、calc.o 就行了,我们修改上面的规1 m8 b3 D+ D' h- R8 y! K
则4 p' k3 x$ d! K/ B% S5 i: k
1 1 objects = main.o calc.o
- b$ a8 D/ \# n* D2 2 main ( objects)7 x+ T+ K# U9 D8 K( ?! _
3 3 gcc -o main $( objects): p* {: T( ]- O0 o1 `" A
我们来分析下修改后的规则,首先第一行是我们定义了一个变量 objects,并给赋值“main.o calc.o”,
6 G9 ~) o2 t5 r( V第二行、第三行用到了变量 objects。Makefile 中的变量引用方式是“$(变量名)”,变量 objects 的赋值
- W# U1 f' k: l2 [) e' U3 Y使用“=”,Makefile 中变量的赋值还可以使用“:=”、“?=”、“+=”,这四种赋值的区别如下:' ?9 X) X) z: q. Q U: Z! _3 e0 C/ @
1. “= = ” 赋值符
' \ N6 X: L8 G" V0 J我们先在用户根目录的 work 目录下创建一个 Makefile 脚本,输入下面的内容:: w4 F$ A, Y# G: o
1 ceshi1 = test
( ]5 p) {; ~$ ], C2 ceshi2 = $(ceshi1)
+ ]2 {7 M- r5 w$ y6 ?3 ceshi1 = temp( {, w2 A8 k2 B1 f0 X
4& x z- o. T- M9 r
5 out:
C( z$ ?9 I* E) ^* x6 z6 @Echo ceshi2 (ceshi2)' s) w9 g/ j6 j) B+ V% S+ O
第一行我们定义了变量并赋值“test”,第二行定义了变量 ceshi2 并赋值变量 ceshi1,第三行修改变量3 Y: J: d1 s; e9 M
ceshi1 的值为“temp”,第五行、第六行是输出变量 ceshi2 的值。我们在终端输入“make out”命令,如, ~7 d @' N" M) q
下图所示:
5 X6 _$ E, i7 R
2 K0 ?0 }+ }6 L在上图可以看到变量 ceshi2 的值是 temp,也就是变量 ceshi1 最后一次的赋值。
& T% J, G8 A: b8 g+ r. F) w2. “ := ” 赋值符$ M( w# S3 L. c# l6 n
我们修改“=”赋值符中的代码,第二行的“=”改成“:=”,代码如下:! j) h) G: t" ?8 S' S$ K
1 ceshi1 = test$ K: l: m8 J$ L. G) E# E; O
2 ceshi2 := $(ceshi1)
7 o6 g. r2 H# P3 ceshi1 = temp
; s8 U& b# z1 _4" B/ \; D& K. P% w" N- H( V, i
5 out:
0 e9 A& e( m+ e, z6 @echo ceshi2 (ceshi2)+ F2 Q' ]7 W7 r# X g
我们在终端输入“make out”命令,如下图所示:
D# _6 ?- Q& X5 l3 D
1 c/ k, \9 d6 |我们可以看到上图的运行结果输出变量 ceshi2 的值是 test,虽然在第三行我们修改了变量 ceshi1 的
$ ]4 M! v4 z' @! N; Z值,通过本实验我们可以看到“:=”赋值符的功能了。1 R$ Z4 q$ K$ B' [6 L
3. “ ?= ” 赋值符
* a& j4 N7 J4 @! v; ?- zceshi ?= test6 a& _- R8 L0 e- |, u0 y( A
“?=”赋值符的作用是如果前面没有给变量 ceshi 赋值,那么变量就赋值“test”,如果前面已经赋值了,
, f3 |9 L$ K/ _ Y% E就使用前面的赋值。
' N7 Y) ~3 M% q4. “ += ” 赋值符3 f! c+ g. x; y$ j, X$ J
objs = main.o
; W+ y6 z {3 ^objs += calc.o- v$ c7 G; J M1 @
上面的脚本最后变量 objs 的值是“main.o calc.o”,“+=”赋值符的功能是实现变量的追加。
' i) a' g! q4 g8 G7 ~/ X4 o3.4.3 条件判断
& n$ }+ _, p: t! [1 R2 ?' ^0 L" y" C使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量
z' o* m+ H% \ l( s( M的值,或是比较变量和常量的值。其语法有下面两种:
+ ?2 r: f$ P0 c% x8 D: \1.
: d& `: b& N6 N0 c<条件比较>
* l5 X* y. e! x[条件为真时执行的脚本] M& ^; w# E9 s7 C- F
endif
+ a2 Q$ p/ l" K( l9 j6 m6 J+ t- i2.0 b8 @3 E* l- b- D+ x
<条件比较>* _! e. b) E X8 _$ ~
[条件为真时执行的脚本]! a: x9 h/ u8 f3 F9 G
else! ~5 ~$ ^# {6 d! l4 O
[条件为假时执行的脚本]
% t" }* t9 d( ]5 _, G: \% W0 sendif
" m3 `% @7 ^$ z3 P0 c* w条件比较用到的比较关键字有:ifeq、ifneq、ifdef、ifndef。
. M, A% [2 P0 _# Fifeq 表示如果比较相等,语法如下:1 I6 h6 ~, x5 f f, x
ifeq(<参数 1>, <参数 2>)9 r' v( @( e+ D' r. \2 o! `# G
ifneq 表示如果不相等,语法如下:& @9 e, r% Q7 ?& {' U$ J
ifneq(<参数 1>, <参数 2>)- h9 Z# ^! I1 {
ifdef 表示如果定义了变量,语法如下:
- b1 v# s6 J, E! n' O: U/ Difdef <变量名>
1 e: h* o1 ]) I8 H9 e+ ?- @ifndef 表示如果没有定义变量,语法如下:
. r' U( E' {3 Kifndef <变量名> m+ w1 i; R9 K/ s5 E3 U
3.4.4 使用函数" }- ~$ F9 _. y/ @9 m+ E7 C6 P
在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所' q4 f* Q, v. d# E7 |
支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。4 h% f# M1 j. d/ z, l' P' R
函数的调用很像变量的使用,也是以“$”来标识的,语法如下:
% Z4 q. n# q% s3 R: j1 j- H$(<函数名> <参数集合>) z* U2 A! b8 d6 k9 s
或者:
# a" ^4 o! S5 |; P) _9 O/ Q${<函数名> <参数集合>}# t8 O0 F3 K/ `2 {# Z6 `4 {& L7 `
函数名和参数集合之间以空格分隔,参数集合的参数通过逗号分隔。函数调用以“$”开头,以圆括号或花
& z2 W i& a4 F括号把函数名和参数括起。感觉很像一个变量。函数中的参数可以使用变量。为了风格的统一,函数和变
3 q1 D6 m8 R4 \; B量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。. B0 f$ R: m1 ~8 |2 q3 }3 H) ~+ @
因为统一会更清楚,也会减少一些不必要的麻烦。- e% T6 l( u% f" |5 O9 T4 i9 L
接下来我们介绍几个常用的函数,其它的函数可以参考文档《跟我一起写 Makefile》。3 O5 e3 Q" s( T! q; ^
t 1.subst 函数
1 m8 {! q6 T3 `! B) A. K/ _$ s. E$(subst ,,); c; M9 N" k" |: f% ~( v4 \" D0 \
此函数的功能是把字串中的字符串替换成,函数返回被替换过后的字符串。如下示例:
* {; j' k5 ]8 }$(subst ee,EE,feet on the street)
3 _% L ^- |6 J8 H0 S以上脚本实现把字符串“feet on the street”中的“ee”字符串替换成“EE”字符串,替换后的字符串
8 @2 i1 T3 @* L# r; a; F为“feet on the strEEt”。$ ?& X, n: l, J) t, o
. 2. t patsubst 函数' X. p1 A0 B) N) i
$(patsubst ,,)& K: G* B( k# x* E& z- j6 u
此函数的功能是查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式- y: I5 B) D" @4 b3 ^
,如果匹配的话,则以替换。这里可以包括通配符“%”,表示任意长
4 B( u$ n7 ^2 u; s+ p度的字串。如果中也包含“%”,那么中的这个“%”将是中的那个
' t B$ A h v6 B2 l, R+ \“%”所代表的字串。(可以用“\”来转义,以“\%” 来表示真实含义的“%”字符)。函数返回被替换
2 M3 X# S% I5 F. L$ Q- I5 h/ Y0 N过后的字符串。如下示例:, ]5 X0 q) `3 M1 T3 @ ^
$(patsubst %.c,%.o,x.c bar.c)
' N! F3 n( W5 R. M7 l以上脚本实现把字串“x.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.o bar.o”+ v3 ]% t/ _. T
p 3.strip 函数' i6 |& i) [! Y9 f" ?
$(strip )2 f$ ?2 q0 s9 q
此函数的功能是去掉字串中开头和结尾的空字符,函数返回被去掉空格的字符串值。如下示例:% O" A7 G3 q% D( j0 ^; |1 S
$(strip a b c )
4 C' k4 A2 k3 Q/ s/ C3 n以上脚本实现把字串“a b c ”去掉开头和结尾的空格,结果是“a b c”。) ?2 Z3 i, |* ?' d% m
. 4. g findstring 函数( X4 L/ |# H2 j1 H/ \0 ^
$(findstring ,), Y( K h. T( I0 H' d2 t
此函数的功能是在字串中查找字串,如果找到,那么返回,否则返回空字符串,如下示
5 E* s5 @. [" T# F例:* t2 s, K3 V8 D- n- f8 y a
$(findstring a,a b c)
& d3 z6 [$ d7 f5 x9 S- ^. X$(findstring a,b c)
5 B2 M. A, x' _6 U) N以上脚本,第一个返回“a”字符串,第二个返回空字符串。. ~+ z" D8 ?* p4 ^; r
r 5.dir 函数2 R" G' d9 k; F) E4 }5 M
$(dir ). @. q- M( e# i
此函数的功能是从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部
+ n8 c/ A% z1 f- I分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分,如下示例:, s, s/ Q3 Q' y6 w9 ]% K3 O* e
$(dir src/foo.c hacks)- a1 h1 o$ C X
以上脚本运行结果返回“src/”。! Y5 I# I2 i; K6 O* C8 [
. 6. r notdir 函数+ w6 @* s+ C' j& W+ F# J/ c
$(notdir )0 s$ V8 `- U; E$ ~9 O2 B1 }
此函数的功能是从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后+ L I: V, C; L
的部分,返回文件名序列的非目录部分,如下示例:" W6 ~. w) U4 g* t8 N# `8 z' L" O
$(notdir src/foo.c)" k5 }$ G- }! s7 {: @4 `
以上脚本返回字符串“foo.c”& E; t6 n/ V- G5 }% ~' J
. 7. h foreach 函数
% o a2 C8 y' l( ^' S$(foreach ,,)
; k9 M: {1 s4 `( L n此函数的功能是把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含2 t7 c8 |6 X0 f5 B& A
的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔,
( V7 m) @. Q; q& o' K最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函
) q4 D2 ~4 R2 Q! W+ S7 \4 o数的返回值。所以,最好是一个变量名,可以是一个表达式,而中一般会使用这
/ f8 C D! A8 }& M2 w ~个参数来依次枚举中的单词。如下示例:* _: c' A: G4 ]9 J- b& _% U( i
names := a b c d
* I7 L+ X: p* T1 ~3 U5 ffiles := $(foreach n,$(names),$(n).o)
8 u( Z' \8 k& t5 O以上脚本实现$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出# [$ e! n9 ~ u) [
一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以$(files)的值是“a.o b.o c.o d.o”。
2 |+ O! W% C+ x; T$ }4 s7 z(注意,foreach 中的参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不, B, e5 d' r2 ~8 ]0 m' _6 d
在作用,其作用域只在 foreach 函数当中)。
6 R; j5 k3 f3 e& }1 l6 C( o* o& _7 c更多内容关注迅为电子7 }+ |: E" s! y- e
|
|