|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
我们在 Windows 下使用 C 语言开发的时候,一般都会有支持 Windows 的开发工具,比如我们学习 51 单
3 x2 c' J8 t/ a- I( B4 j! ~) L! t: z片机或者 STM32,所使用的的 Keil 开发软件。此类开发工具一般都会集编辑、编译于一体,我们只需要编辑0 ~3 I) C6 j5 a' `
好代码,电机开发工具的编译按钮,就可以自动为我们编译出可执行的二进制文件了。Ubuntu 下的 C 语言
) f, O! l1 x& H开发与 Windows 下的 C 语言开发是不一样的,并没有图形界面的开发工具,而且编辑和编译是分开的。我* L; a& |% Q9 I* O
们需要使用文本编辑软件先编写代码,然后使用编译工具(GCC)来编译,最终生成可执行的二进制文件。! [6 i" D# G1 W; f6 P+ y
如果我们的工程中有多个源文件,在编译的时候我们通常会通过 Makefile 文件来管理整个工程的源文件。
. r5 x4 o3 h$ Y2 y) u _* Q本章我们来学习如何在 Ubuntu 下进行 C 语言成的编辑,GCC 编译,Makefile 文件的使用。通过本章学习我$ ]! o0 z0 t. A+ \
们可以掌握 Linux 下的 C 语言开发基本方法,为后面的学习做好准备。4 D$ R0 @" b- R" v
3.1 x Linux 下编写第一个 C C 程序 程序
7 p3 T$ w4 N, j本章节开始的部分我们介绍了 Ubuntu 下 C 程序开发分成两部分:编辑和编译。Ubuntu 下有很多种文本" N' M+ q9 u& m% A. {( i' G2 n
编辑的工具,如 vim、Gedit、Emacs,这里我们使用前面章节介绍的 vim 文本编辑工具来编辑 C 程序。相信
" H" _* D2 ]. p. w7 D大家在学习 C 语言的时候都是从“Hello World”这个程序开始的吧,下面我们通过这个程序,来学习下 Linux2 J) H N- Q- q
下 C 程序开发的流程。& \' N W D+ i9 J% O
3.1.1 代码编写 \, D1 f1 T7 B
首先我们在用户根目录下建立文件夹“work”,用于保存所有 C 语言的程序,运行结果如下图所示:
! E# w, R( R: A/ N3 s' e
- s0 k4 W' O7 g8 @0 N& H$ I% z3 S4 J; d
然后进入创建的文件夹 work,为了方便管理,我们每个例程都创建单独的文件夹,首先我们创建文件
5 R5 T; L# E( E4 }; M夹“hello_world”来保存我们的第一个 C 程序,如下图所示:4 q+ `+ k; D# e
0 r+ R+ a- k! S9 b5 D1 E+ v w然后进入上图中的 hello_world 文件夹,使用 vi 命令新建文件“main.c”,然后在里面输入下面的代
; ^+ O+ V+ D+ Y! E4 Q& w! d$ Z码:
2 F+ b$ \* }0 c4 F3 {) k, B#include ; y* Y8 p3 t2 B4 A3 K+ @3 N
int mian(int argc, char *argv[])5 r: Y. R# k: s3 a, J
{
& M' [/ ^6 a# L* E- yprintf("Hello World!\n");$ O' P/ \- S0 f; a2 \/ R3 w' ^2 Z6 `
return 0;
: n1 l# \1 w% D( Z}1 C# X* T: K+ }7 Y
编写完以后保存并退出 vim 编辑器,然后可以使用 cat 命令查看代码是否保存成功,如下图所示:
. u- _1 L8 K4 S( j3 F& x
! r4 ^0 N% q- Z$ D. J1 Q通过上图可以看到代码已经编辑完成了。
' O. [* L! B7 _# p6 a0 @4 N( F4 g3.1.2 代码编译
7 w4 L5 T) R; Z2 k* q% m9 k# e/ BUbuntu 下使用 gcc 编译器来编译 C 程序,我们在安装 Ubuntu 系统的时候,gcc 编译器 morning 安装好1 t* }8 C" I0 t7 B
了,我们可以在终端输入“gcc -v”来查看下 gcc 的版本,如下图所示:5 i1 P! h" D. Y+ e
; x: f( p2 n5 m2 \3 U+ p" ?# I
通过上图可以看到 gcc 的版本信息,说明我们的 Ubuntu 上已经安装了 gcc 编译器了,下面我们来看看" q" k0 q% ~8 H5 w
怎么通过 gcc 编译我们的第一个 C 程序,我们在终端输入“gcc main.c -o main”,然后回车,会生成 main 文' N; G; H$ G4 O6 |7 {
件,如下图所示:
2 S, b4 b. I. ?+ @
: U, T, W. S/ s0 R u在上面的“gcc main.c -o main”这个命令里面“-o“用来指定编译生成的文件名字,我们指定的是 main,; L2 X; U/ P1 t" @
所以在图 3.1.2.2 中可以看到生成了文件“main”,如果我们想生成其它的文件名字,我们只需要修改“-o” j0 C" f9 f u* o9 v3 i9 q. d: e
后面的 main(修改成您希望生成的文件名字)。经过前面的步骤已经生成了可执行文件 main,现在我们演/ @3 }( T+ U9 d0 E$ K! N8 g
示下在终端如何运行一个可行性的程序,我们直接在终端当前目录下(可执行文件所在的目录下)输入
+ B7 l7 o) N5 S# U" b; ?! Q) O“./main”,然后回车,就可以运行可执行文件 main 了,运行结果如下图所示:
. |; c6 c# `) H
2 A/ f4 l" j. q' I; \3 P' Q在上图中运行的命令“./main”,其中的“./”代表当前目录下。我们可以看到在终端打印出了 Hello3 x2 M3 v/ M3 W \2 `) F
World!。至此,Linux 下的 C 语言编辑和编译的一整套流程我们就介绍完了。$ \( r" o' [/ B3 {
3.2 c gcc 编译器 编译器
; y! p" R4 z; c( E- v3.2.1 gcc 命令分析
' X- v I4 x8 k3 B" u: @" E& a在 3.1 节我们已经使用 gcc 编译了 Linux 下的第一个 C 程序,gcc 命令的格式如下:0 o: a. _: E7 G" R% _$ Y9 S" r
gcc [参数] [文件名]
8 [' b8 ~6 g+ ]/ e主要参数说明如下:
) a1 {* O; b& |) U0 d9 K-c 编译、汇编到目标代码(.o),不链接成可执行文件
" j1 d Q& I" ]$ z-g 生成调试信息 I' E. Y* d' s7 y9 w7 N) v
-o 编译完成后生成的文件名,如果不使用该选项,默认生成 a.out 文件
( M, X- v0 w( j; u8 I-O 对程序进行优化编译,产生的可执行文件执行效率高
9 ~% u# @4 t2 A# }: a& o-w 不生成任何警告
9 F% \4 a! m1 z" @' ^# y1 `* S# S-S 仅编译到汇编语言,不进行汇编和链接
; ?$ \ T$ p- _: ^3.2.2 编译警告错误处理
! E& Y7 w# Y4 O. k9 y% c# v3 w我们是 Windows 下使用 Keil 或者其他开发工具,在编译的时候,如果程序有错误,开发工具会提示出% V, q' Z! G) A2 S3 k
具体的错误信息,可以很方便的定位到问题点,快速的修改出现的问题,gcc 同样也有类似的功能,下面我% x' K5 a5 t) r0 ^4 e
们来看下 gcc 的错误提示功能,首先我们在 work 目录下建立文件夹“test2”,然后使用 vim 在 test2 文件* } S7 i7 H1 y7 l- x; T' N- w
夹创建 main.c 文件夹,在 main.c 文件输入如下代码:
* G: M, }$ z- h6 d7 m#include
. e5 X1 N! o# u$ Tint main(int argc, char *argv[])5 P# @) G, e# K2 Y* O" D% \
{) [. x6 |7 K3 p* v+ O9 Z0 c) Z3 t8 l
int a;
1 @/ f1 `% i ]0 _- oa = 1+ c2 W( j# ]! T4 p6 Y
printf("a=\n", a);7 L8 t7 i- Q! J/ i5 E/ f$ D
return 0;
' g- _9 n- \$ m" X; P; F! w! _}
% E `$ n5 W7 t- f* ?% O上面代码有两处错误:8 e3 U2 H& L9 W9 e! v K! e {
第 7 行 最后缺少“;”
# T; T# V/ {' Y4 E- U第 9 行 printf 语法不对,应该为:printf("a =%d\n", a);
2 z: w5 a0 a* E+ a我们使用 gcc 编译 main.c,可以看到 gcc 会提示错误信息,如下图所示:
; H. f V+ Z9 s. l. D% y
( X1 E' L6 q* _- l, M
从上图中可以看到在 mian.c 文件的第 9 行 printf 前面缺少“;”我们在第 7 行“a = 1”后面加上“;”,- B( l Y! A& k) A, z m8 o; x
然后继续编译,如下图所示:
, C# l) z0 A+ O2 c! }2 h3 m9 T
1 |6 s. e1 X# h( B1 r
从上图可以看出编译提示语法格式不对,我们把第 9 行修改成“printf("a=%d\n", a);”,然后在继续编
* [, P6 s% U1 Y8 T& E6 C译,如下图所示:* z- m) C0 I- C) n; _
: q$ J: ]" @, `0 @- k
我们可以看到这次 gcc 编译通过,最终生成了文件 main。我们在终端执行 main,运行结果如下图所示:8 i9 d9 F* f4 j
3 {) V. W: H4 b, j+ e" t从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出 i/ ~$ [0 O! s) o" h+ D$ D
程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。4 B; F( e: J/ n
从上图可以看到运行的结果和我们涉及到额结果一致,通过本例程可以看到 gcc 编译器不仅可以检测出3 P, W. P1 c; b, w4 d- W, Q. H
程序的错误,而且还会标记处错误在哪个文件的哪一行,很方便的帮助我们去修改问题。 R9 B9 i3 R% r; N D4 a: C
3.2.3 gcc 编译流程
1 I J. ^) q, g% _# o8 Vgcc 的编译流程可以分成四个步骤:( ^' J, h: Y, t
1.预处理,生成预编译文件(.文件)$ d' k9 l$ r; s5 i' H5 c( o
2.编译,生成汇编代码(.S 文件)
4 n. Q8 R9 `' w7 ^( d3.汇编,生成目标文件(.o 文件)
; M4 }7 v/ ?7 M/ @. Y$ ^( V4.链接,生成可执行文件5 O( }6 q3 I9 O! L' `. E& |
3 3.3 初识 Makefile
$ `$ z6 d# Q* S8 h. r9 a3.3.1 什么是 Makefile
8 _ v3 q4 \4 h% g+ m, e0 o在 3.2 章节我们了解了在 Ubuntu 系统下通过 gcc 编译器来编译 C 程序,在我们演示的历程中只有一个; o0 \& X" a }; k, z
C 文件,我们直接在终端输入 gcc 的编译命令,就完成了 C 程序的编译。我们在实际开发过程中,如果我们
4 \9 P+ D4 V4 q# } j/ \的工程有几十个,或者几百几千个 C 文件,我们通过在终端输入 gcc 命令来编译,这显然是不现实的。为
) L! b% ?& |: e% S了解决这个问题我们可以使用“make”命令,它会解析 Makefile 文件中的指令(应该说是规则)来编译整5 O1 N1 x$ X; O. ]) M5 s7 g
个工程。在 Makefile 文件中描述了整个工程的所有文件的编译顺序,编译规则。3 s* b2 B# S- M$ W" b
作为一个专业的程序员,一定要掌握 Makefile 的,我们可以通过 Makefile 能了解到整个工程的处理过程+ u0 T- r( V' \6 o% [' }& A5 ?
的。' A" U# d$ S% c: q1 h! d' W
由于Makefile涉及到很多的知识点,以至于可以单独写本书来讲述,所以本章我们只是讲解下Makefile7 `4 C1 F, }6 w0 J
的基础入门,如果详细的研究 Makefile,可以给大家推荐《跟我一起写 Makefile》这个电子文档,该文档
Y! w* o$ n" ^0 B' m3 D已经放在了:i.MX6UL 终结者光盘资料\09_其它参考资料里面了。4 Q+ A& M+ t, X) J
3.3.2 第一个 Makefile( |" u5 o- P) j, y7 S' n% t
在本节我们建立这样一个工程,计算两个整形数的和,并将结果在终端显示出来。在这个工程中一共有, x( Y3 p; a' ~
main.c、calc.c 两个 C 文件和 calc.h 这个头文件。其中 main.c 是主文件,calc.c 负责接收 main.c 传过& `8 g% ?/ J" D1 i. k
来的数据,然后进行相加。main.c 文件的内用如下:* z% h) o' w9 Q3 ]/ u
#include 4 o8 _8 A* P& L! r
include "calc.h"
- e8 W( g/ E) E; F) U& P0 ~, s$ [int main(int argc, char *argv[])
, q* H+ } `' ]( g{
7 M2 {; Q( A4 Rint a = 3, b = 6, sum;+ ?$ Z, o( _- Z' @* m1 A% |
sum = calc(a, b);
: Y9 v! E0 b C: h0 i/ Dprintf("%d + %d = %d\n", a, b, sum);. h) o- |4 r* S/ o g1 l2 ^# S
return 0;
3 B0 a1 @, R" G! c, @. W}( X2 V6 G% \. t9 o9 E4 @3 V
calc.c 文件内容如下:
( C: N# Z$ f$ L* I8 k: C' {#include
; C7 R) P0 s! ]( o! l, Q; tint calc(int a, ing b)
3 F- A N% \7 g% R" t6 ^{- Q( L, ^$ l U! X; ~
return (a+b);* I7 p5 r+ i' A8 i$ F
}) o5 p; S* y) r, z
文件 calc.h 内容如下:
2 {! U- j+ X% j p#ifndef _CALC_H; g9 K# K! x/ f5 e7 E* m
#define _CALC_H
0 q- ?9 h+ C. {7 \5 L6 gint calc(int a, int b);7 H. k( N/ k3 d. j" `! J o
#endif
$ T( Y: i- s8 U上面就是我们这个工程的所有源文件,我们在终端使用 gcc 编译这个工程,在终端输入“gcc main.c
4 T0 `% u0 m# A6 \* x/ h& Dcalc.c -o main”,该命令的意思是用 gcc 对 main.c、calc.c 进行编译,然后输出可执行文件 main,运行
/ R8 q3 K2 x+ p4 t D& T结果如下图所示:8 p" W% C- T& }" ~( @
k! O: N% d$ P N( c, u4 H通过上图可以看到生成了可执行文件 main,我们在终端运行 main 执行文件,运行结果如下图所示:5 X: E" V: [( g4 N+ O
& g1 z. u# f" [7 y5 y
我们可以看到上图的运行结果和我们设计的结果是一致的。由于我们的这个工程是有三个文件,如果
# N3 c Q- S& Q- i工程有几百个,几千个的文件,或者如果有一个文件被修改,使用上面的命令将会编译所有的文件,如果
- a$ C) Q6 E1 _% T我们的工程有上万个文件,编译完一次工程所需要的时间就很可怕。最优的方法就是编译过一次以后,如
* m. Z& `( h' F. z8 T果后面在编译,只编译修改的文件,这样就会节约很多时间,因此我们修改下编译方法,命令如下:
4 T& u8 X$ V$ F) G, mgcc -c main.c
4 ^5 F$ L. i5 V, t; dgcc -c calc.c( J: k. d1 W6 F; R; k
gcc main.o calc.o -o main
! D1 j$ ]% J$ S9 O( N6 o( @) h8 i" U我们在终端输入上面的命令,结果如下图所示:
" L. `8 e; F& D! S+ b* o
: Y W: f6 i" ^+ d( K; p上图的第一条和第二条命令里面使用了参数“-c”是把 main.c 和 calc.c 编译成对应的.o 文件,最后- X( e6 ?4 t- N' l; O( ]" O8 [
一条命令是把编译生成的.o 文件链接成可执行文件 main。假如我们修改了 main.c 这个文件。只需将 main.c: ?2 K0 l$ ~$ G8 o
这个一个文件重新编译下,然后在把所有的.o 文件重新链接成可执行文件,对应的命令如下:4 L5 x# z* ~ \' ~/ X* O# a
gcc -c main.c
- a, ~3 z0 r1 H1 U7 |( _& pgcc main.o calc.o -o main9 x1 z4 ~5 u) [* F- p7 A
可是这样还有一个问题,如果需要修改的文件有很多,这样工作量也会不小,所以我们需要一个工具:" x$ k0 L3 i4 _7 ?# M* a% G8 S: w
1.如果工程没有编译过,就会把工程中的.c 文件全部编译并连接成可执行文件4 m; f. d3 F M- J6 s
2.如果工程中有某些文件修改了,只编译修改的文件并连接成可执行文件5 \) x0 S4 k9 g0 B I& ^* s
3.如果工程中的头文件修改了,那么就要编译所有引用这个头文件的.c 文件,并且连 接成可执
5 P4 j* A! a; W' _5 S行文件+ M9 k4 F& Q6 J' J- |0 f' {: w
我们开头说的 Makefile 就是完成这个功能的,下面我们在工程中建立一个 Makefile 文件来实现这样的功
4 Q: {& {" [; q* e- Y) I能(注意:文件名字必须为 Makefile,大小写是区分的)。我们使用 vim 创建 Makefile 文件(Makefile
0 o" }4 l) O. U- u, j# V' k+ w和我们的 main.c、calc.c 在同一级目录下),然后输入下面的脚本:
4 }( b: L# _' }7 kmain:main.o calc.o
% T9 y% _. X1 }# e) W5 Z# }gcc -o main main.o calc.o# Z1 h! u( q8 t* T, G
main.o:main.c
6 \6 G" u) Y: D; P7 Pgcc -c main.c5 a4 \3 I9 \, N) E8 s8 ?
calc.o:calc.c
/ f1 u q" X0 W$ U/ i/ Qgcc -c calc.c
6 y: D. t4 c3 B" G( j9 _clean:
% f6 |2 R. M. E" Hrm -RF *.o* l$ V! ~7 H& f- k; a T! U
rm -rf main
J1 s' G- `( L6 e0 ?2 g, p, v上面脚本缩进的行需要使用“Tab”键缩进,不要使用空格,这是 Makefile 的语法要求,编写完成的脚本
8 p* x) j% v; s3 J6 |; [如下图所示:
$ f, _2 D1 l5 Y$ ]2 L+ s
9 r a, H1 C3 _' P0 b5 \0 v编写好 Makefile,保存并退出,然后我们在终端输入“make”命令来编译我们的工程,make 命令会在# _- b" I' h+ ]
当前目录下查找“Makefile”文件,如果存在的话就按照 Makefile 里面的规则进行编译,如下图所示:- ]6 ?- p( y# ^( ?
( f9 ]* s. t/ I" k/ N( ]2 O
通过上图可以看到编译产生了 main.o、calc.o 和 main 执行文件,说明编译成功了。接下来我们修改$ ?7 t) I! y3 \6 P! I, f
下 main.c 这个文件,如下图所示:& o; P8 n% z/ L' G" ^
! ]- ^/ B& C3 v( U% [7 E
然后保存并退出,然后在终端输入“make”再次编译下工程,如下图所示:3 C; y% L* m% G
4 H8 Q* k& N( ]
通过上图我们可以看到只重新编译了修改的 main.c,并最终重新链接生成可执行文件 main,我们在终
8 D6 {9 o, }/ Q2 {; p' T/ T端运行可执行文件 main,如下图所示:- d; N5 V' D7 p- i! B- `+ I
: _' M1 M* `2 q! G从上图的运行结果可以看到最后的结果等于 10 了,和我们程序的设计结果是一样的。
+ G* l3 T/ Y( {1 l: ?3.4 e Makefile 语法 语法& W7 i& d `, ~! l F
3.4.1 初识 Makefile
. N7 P8 j% O* T' i9 NMakefile 文件是由一些列的规则组合而成的,格式如下:
" {. A1 t0 j0 k% x( x& Q# Ytarget(目标文件) ...: prerequisites(依赖的文件) ...6 Z- x8 T# \7 M1 }
command(命令)
" ` h$ q7 }* M6 f% z...$ i: R C- @4 Q$ J2 J" O; A
...- ~% S8 P0 p% d6 S
比如 3.3.2 中写的 Makefile 的规则:4 S$ ]7 w* T t7 M) J9 b1 R
main.o:main.c
. y& ?% Q! @- Y8 F5 e2 agcc -c main.c
$ D+ W6 ]* A1 T这条规则的 main.o 是目标文件(将要生成的文件),main.c 是依赖的文件(生成 main.o 需要的文件),, Z, h. _0 f7 ]; {- i4 E, {+ `
“gcc -c main.c”是生成 main.o 需要运行的命令。e Makefile 中每行的脚本如果有缩进的情况,必须使用" D' R3 ?" Y$ L& N! U* x% q
“ Tab ” 键缩进,切记不能使用空格缩进(这是 e Makefile 的语法要求),大家一定要切记!
0 z5 M) h- H, f' _, a- d; n4 c0 r下面我们来分析一下图 3.3.2 章节中写的 Makefile 文件,脚本如下:
; v ~* F% m3 P! r3 @1 1 main:main.o calc.o
4 E: O: M( F- p* E8 v# O2 2 gcc -o main main.o calc.o0 [6 U u# M' G! Z- j S( ]
3 3 main.o:main.c: }% l1 y: L- o6 O& V* r( A1 K
4 4 gcc -c main.c
) W+ ~% [, k2 q6 W. h1 L" v5 5 calc.o:calc.c
5 U! j1 o# J5 X3 a/ ^6 6 gcc -c calc.c4 K% M# z$ w$ E, E
7 7$ Y4 s, t# ^5 G" Z5 D5 A9 z
8 8 clean: m( [1 l" J) D5 g
9 9 rm -rf *.o
: P3 I0 j$ @1 e. b# g2 I* C10 rm -rf main
% c+ V! Q9 o+ g/ H$ y, s& f. [0 w该脚本一共有 4 条规则,1、2 行是第一条规则,3、4 行是第二条规则,5、6 是第三条规则 8、9、10
8 J' ] v0 g. f1 D! j7 z是第四条规则。我们在运行 make 命令的时候,会解析当前目录下的这个 Makefile 文件里面的规则,首先
- L; y0 F0 w8 B* c0 h# L解析第一条规则,第一条规则中的目标文件是 main,只要完成了该目标文件的更新,整个 Makefile 的功能7 Y( b1 ^6 {$ R; ^, E% N2 `# t
就完成了。在第一次编译的时候,由于目标文件 main 不存在,则会解析第一条规则,第一条规则依赖文件
! i. b& J! p5 j: X! x! J0 Rmain.o、calc.o,make 命令会检查当前目录下是否有这两个.o 文件,经过检查发现没有,然后 make 会在
|$ m+ @4 x) B( a iMakefile 中查找分别以 main.o、calc.o 为目标的规则(第二条,第三条规则)。执行第二条规则依赖的文( O0 i% P/ {# l
件是 main.c,make 命令检查发现当前目录下有这个文件,然后执行第二条规则的命令“gcc -c main.c”
6 `' v1 i# f' H. T1 o/ S" F& N. c生成 main.o 文件。然后执行第三条规则,第三条规则的目标文件是 calc.o,依赖的文件是 calc.c,make
5 C3 X. u8 @8 v- a' s命令检查发现当前目录下存在该文件,然后执行第三条规则的命令“gcc -c calc.c”生成 calc.o 文件,
- \' m- [% W8 i s0 O6 q9 F/ W至此第一条规则依赖的 main.o、calc.o;两个文件已经生成了,然后运行第一条规则的命令“gcc -o main
* l/ c, P3 Q+ L, J4 m$ `* xmain.o calc.o”生成 main 文件。因为 make 命令运行的时候会从 Makefile 的第一条规则开始解析,然后' {0 L/ i" A: a
根据第一条规则的依赖文件去遍历文件中的“对应规则”,然后在根据“对应规则”的依赖文件去遍历“对
: u* ?0 @ L; Y; O应的规则”,采用这样递归的方式会遍历出完成第一条规则所需要的所有规则。下面我们来看看第四条规# a4 g2 }, G8 k1 V: o- e$ M
则的目标文件是 clean,我们通过查看发现该规则与第一条规则没有关联,所以我们在运行 make 命令的时 [6 N% j5 }3 P# z: y; G: x
候,不会遍历到该规则。我们可以在终端输入“make clean”命令来运行第四条规则,第四条规则没有依
7 z5 M# ^6 K; I- R赖的文件,所以执行运行命令“rm -rf *.o”和“rm -rf main”,这两条命令的功能是删除以.o 问结尾的
! ^! u: K+ q3 O: N! g% b0 l& H4 D所有文件,删除文件 main,运行如下图所示:
# X2 s) y1 ?( n, i# e
- X* F( ? Q0 F _7 K. t
通过上图可以看到 main.o、mcalc.o 和 main 三个文件已经删除了。通过该规则我们可以清除编译产生8 Q/ P0 \: ^& n9 k1 G. ?
的文件,实现工程的清理。
5 s, K& I$ F3 P$ a我们再来总结一下 make 命令的执行过程:
- G1 g# n; D' F, K A) x1.make 命令会在当前目录下查找以 Makefile 命名的文件! ^9 [. X! z2 Z- j% }0 H$ M
2.找到 Makefile 文件,就会按照 Makefile 里面的规则去编译,并生成最终文件/ ?) q' L; {+ R! Q. ~
3.当发现目标文件不存在或者所依赖的文件比目标文件新(修改时间),就会执行规则对应的命令来更新。
" k7 X) Q1 l) N' _& @! p我们可以看到 make 是一个工具,他会通过 Makefile 文件里面的内容来执行具体的编译过程。 h8 n) E) N s$ c4 V& W
3.4.2 Makefile 的变量 @# y7 E2 V4 ~
在 3.3.2 章节中的 Makefile 第一条规则:
' l* J* p( n- M" tmain:main.o calc.o
( k. q" g8 w1 igcc -o main main.o calc.o
0 h# p1 i& @1 p/ | {4 A e5 z在该规则中 main.o、calc.o 这两个文件我们输入了两次,由于我们的额 Makefile 文件内容比较少,- ~) T; W4 E( R* F0 C3 J+ Z
如果 Makefile 复杂的情况下,这种重复的输入就会非常占用时间,而且修改起来也会很麻烦,为了解决这4 [3 b# Z/ F, D# C+ v
个问题,Makefile 可以使用变量。Makefile 的变量是一个字符串。比如上面的规则我们声明一个变量,叫
/ V) o" S) w5 w# |9 bobjects,objs 或者是 OBJ,反正不管是什么,只要能够表示 main.o、calc.o 就行了,我们修改上面的规
" i8 [5 F0 v. D5 D则3 S) u; H7 P8 I6 ?- L$ c- [
1 1 objects = main.o calc.o
a( v; e/ }' g2 2 main ( objects)/ v1 L+ b8 u- p9 q* h
3 3 gcc -o main $( objects)
7 _8 v- T. G2 C* }7 ~5 Z' G* b/ P0 p我们来分析下修改后的规则,首先第一行是我们定义了一个变量 objects,并给赋值“main.o calc.o”,
9 Y$ h$ \, R1 ~第二行、第三行用到了变量 objects。Makefile 中的变量引用方式是“$(变量名)”,变量 objects 的赋值
* W; |# o8 k9 X5 e% v使用“=”,Makefile 中变量的赋值还可以使用“:=”、“?=”、“+=”,这四种赋值的区别如下:% z( z+ |) ?7 W( W# u
1. “= = ” 赋值符+ W" ]1 x9 ?' r+ \& H4 m
我们先在用户根目录的 work 目录下创建一个 Makefile 脚本,输入下面的内容:
5 Z' z! o, K3 \5 K9 o4 r: {1 ceshi1 = test
( s9 Q% ^3 p- G2 ceshi2 = $(ceshi1)
: N; e* W% R! a3 ceshi1 = temp
' [7 s, y+ x1 R4, _, v3 v/ W' t) D
5 out:
" G( M n" ^. _! i- q6 @Echo ceshi2 (ceshi2)" P. E3 b2 U9 X( z* b
第一行我们定义了变量并赋值“test”,第二行定义了变量 ceshi2 并赋值变量 ceshi1,第三行修改变量% c+ _; S) p9 D
ceshi1 的值为“temp”,第五行、第六行是输出变量 ceshi2 的值。我们在终端输入“make out”命令,如
6 O/ Y' I3 n7 v0 a7 w: Z下图所示:
* ^8 d1 m, f( l+ D
) r g& J2 f' e8 r
在上图可以看到变量 ceshi2 的值是 temp,也就是变量 ceshi1 最后一次的赋值。
" k! i. o) T2 Z3 R8 F8 I8 h. n* O2. “ := ” 赋值符
|' R) b" Q' I+ O' t7 R我们修改“=”赋值符中的代码,第二行的“=”改成“:=”,代码如下:
3 Z8 ]& O# _4 u8 d8 V0 r- ?9 N, ]1 ceshi1 = test) g: n6 V* ?$ R" \- m/ h
2 ceshi2 := $(ceshi1)
' U6 _1 a7 k9 M0 I* n3 ceshi1 = temp+ }2 w( v: p/ d" ^5 ?# b
4( `2 x" w8 [$ R/ z4 a! X% A9 }
5 out:7 w$ u' K; k8 R) Z
6 @echo ceshi2 (ceshi2)
- B8 P: A4 S, {% M我们在终端输入“make out”命令,如下图所示:
( {8 V4 I2 R) Q- t% N1 W! C7 g
w( J: v. H% z3 l4 P! a3 @& T我们可以看到上图的运行结果输出变量 ceshi2 的值是 test,虽然在第三行我们修改了变量 ceshi1 的+ m: O! l8 T; d8 b* V* R- `
值,通过本实验我们可以看到“:=”赋值符的功能了。
. C7 G4 i" B$ L L3. “ ?= ” 赋值符
( }. ~) D* g& c1 oceshi ?= test# \* K( \+ t+ a9 l* O6 Y& T
“?=”赋值符的作用是如果前面没有给变量 ceshi 赋值,那么变量就赋值“test”,如果前面已经赋值了,; P% Q8 N; k: _2 Y- p9 n
就使用前面的赋值。
8 t ^2 g3 d8 \4. “ += ” 赋值符
3 g; m9 o# d. T+ x {& lobjs = main.o+ ?* |" ^# M8 `( I$ x9 E3 h; v; F
objs += calc.o
- A3 e1 a1 v; Y' ~- Z0 v上面的脚本最后变量 objs 的值是“main.o calc.o”,“+=”赋值符的功能是实现变量的追加。1 k6 ^" [! r7 K8 N/ ?# K- Y
3.4.3 条件判断
' T3 ]* t, |$ v5 { d使用条件判断,可以让 make 根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量
. w2 i0 S! u0 E1 N% K* D的值,或是比较变量和常量的值。其语法有下面两种:
4 Q9 D* ^7 @8 R/ @: M) K1.
2 j- d9 k' U" i; l0 R<条件比较>) V- \0 L1 _2 j. R; X x2 U) U
[条件为真时执行的脚本]7 n [3 z! Z; r. N/ j7 a* m; j$ i
endif
8 a' d5 z7 t$ P, ?! v) a$ Z2.0 Z' y7 Y2 A9 ^9 {9 V1 }
<条件比较>
' B% i1 m# |& G( r[条件为真时执行的脚本], M2 o4 A- k9 `. |7 c& d
else; ^. z7 l6 H, x. A5 y
[条件为假时执行的脚本]
3 D7 w! I) [: F! A5 X3 Q5 L5 w, Jendif
8 \( O) B/ l. u- @4 ?: w条件比较用到的比较关键字有:ifeq、ifneq、ifdef、ifndef。
0 R; M" K6 p$ U5 c" Rifeq 表示如果比较相等,语法如下:3 F" E# d* b4 Q! f) A
ifeq(<参数 1>, <参数 2>). i; C" J! n' O8 S' ]6 o2 v
ifneq 表示如果不相等,语法如下:. d! `, G& [3 m
ifneq(<参数 1>, <参数 2>)
) M2 Y$ o* Z/ b. l# yifdef 表示如果定义了变量,语法如下:9 w+ \+ @% I5 ~) p2 R G' z
ifdef <变量名>
( \0 \$ }- W' d0 sifndef 表示如果没有定义变量,语法如下:
* f" v0 s% K0 \ifndef <变量名>2 h' ^, Q8 H% o$ C( t
3.4.4 使用函数
/ y& k& J, T0 ~$ e在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所
# V8 s# V. Z, w) l5 ~& g$ U2 f2 y支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。: f( |% g5 n. A6 l
函数的调用很像变量的使用,也是以“$”来标识的,语法如下:+ m r, F# |# `2 x5 F
$(<函数名> <参数集合>)1 q4 g0 `# h% v8 t" G$ z
或者:
: `' t4 P# T( W+ b: ]8 H- d${<函数名> <参数集合>}
: P( O3 {5 B0 X% D. I4 D% A函数名和参数集合之间以空格分隔,参数集合的参数通过逗号分隔。函数调用以“$”开头,以圆括号或花) z' y4 b- p. @
括号把函数名和参数括起。感觉很像一个变量。函数中的参数可以使用变量。为了风格的统一,函数和变
; f2 ?" t: `) R) z" ^8 x量的括号最好一样,如使用“$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。4 N u9 H) a/ S
因为统一会更清楚,也会减少一些不必要的麻烦。
) R/ j8 ?" u. P i2 ~1 H接下来我们介绍几个常用的函数,其它的函数可以参考文档《跟我一起写 Makefile》。
* l+ D" K& E8 \$ {2 F9 Wt 1.subst 函数
~) R% ]0 Z) ^8 C# v" [$(subst ,,)
* m% K/ W; B' v- e+ ~6 s此函数的功能是把字串中的字符串替换成,函数返回被替换过后的字符串。如下示例:
H4 P1 p2 H" M$(subst ee,EE,feet on the street)( A: \/ g9 s- i: L
以上脚本实现把字符串“feet on the street”中的“ee”字符串替换成“EE”字符串,替换后的字符串; ^. z* k6 n8 {
为“feet on the strEEt”。
8 T. L( `4 }; F# w) I. 2. t patsubst 函数* T" @% F8 t; H, T8 R
$(patsubst ,,)1 h- G4 p( a$ e: D; \
此函数的功能是查找中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式
/ h2 C, q) ~: {- c# ^- }% U,如果匹配的话,则以替换。这里可以包括通配符“%”,表示任意长+ ^" I4 v- s8 S) N
度的字串。如果中也包含“%”,那么中的这个“%”将是中的那个 {7 p. _$ U8 `: G; v9 R X" w
“%”所代表的字串。(可以用“\”来转义,以“\%” 来表示真实含义的“%”字符)。函数返回被替换; J7 f+ ?8 ~3 m' [' | @) g
过后的字符串。如下示例:& [ O. n/ w- I4 E
$(patsubst %.c,%.o,x.c bar.c)8 a0 F" h$ ?: @" Q
以上脚本实现把字串“x.c bar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.o bar.o”
\) a9 O4 V. Q$ z( {8 ?6 Kp 3.strip 函数
. M: p1 \6 w7 s3 e% P$(strip )
3 ~( d. }! Z$ g$ n* h) @' V6 Q" o此函数的功能是去掉字串中开头和结尾的空字符,函数返回被去掉空格的字符串值。如下示例:4 x9 T6 \- Y' R
$(strip a b c )
- c9 Q* F# u4 s以上脚本实现把字串“a b c ”去掉开头和结尾的空格,结果是“a b c”。# P. G t, D# J2 I1 R5 P: f7 B/ ?+ R( y
. 4. g findstring 函数3 s& h o! ?* b7 i0 j7 Z
$(findstring ,). T1 d6 j" W3 c6 K) z' c8 p
此函数的功能是在字串中查找字串,如果找到,那么返回,否则返回空字符串,如下示7 a! O, ~! J5 L" `6 H$ t5 G
例:
3 S7 k7 H& p+ V9 c$(findstring a,a b c)
: m; d) M. I& S$ x$(findstring a,b c)
5 |' M1 ?) V x! @ R2 h" X以上脚本,第一个返回“a”字符串,第二个返回空字符串。# |8 j4 k/ k8 K, |" N
r 5.dir 函数! Q. G) a8 A: l( u' H
$(dir )2 t2 l3 T' P, @+ y
此函数的功能是从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部4 L# X: `7 N4 f4 v5 ^# g# ~, m
分。如果没有反斜杠,那么返回“./”。返回文件名序列的目录部分,如下示例:
! r7 u& }% R- ~" a$(dir src/foo.c hacks)
. M0 w! i+ _, \$ B2 S以上脚本运行结果返回“src/”。" k0 g: x" P0 N- h
. 6. r notdir 函数
; T: `8 k6 `- D! `) W1 N; d+ n$(notdir )" u) U4 J4 \+ r% j3 ^ b) v0 E9 O
此函数的功能是从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠(“/”)之后* X* f; Z% [9 W# W* a
的部分,返回文件名序列的非目录部分,如下示例:9 R" j" Z) h2 a
$(notdir src/foo.c). x8 N6 q8 y; l" ]* C
以上脚本返回字符串“foo.c”0 W, i1 z3 v5 X5 ] z. l" ^9 Y
. 7. h foreach 函数$ X4 `$ ^/ i! e( H% J" O
$(foreach ,,)
9 x" S7 X4 A" B$ C) h y* }" V, O此函数的功能是把参数中的单词逐一取出放到参数所指定的变量中,然后再执行所包含
' P3 Y1 u- E- } Y1 ~3 t4 `的表达式。每一次会返回一个字符串,循环过程中,的所返回的每个字符串会以空格分隔," w% Y+ q# }3 v+ Q8 D% r
最后当整个循环结束时,所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach 函
( Q; Q9 i, ]6 D7 i9 K v, E数的返回值。所以,最好是一个变量名,可以是一个表达式,而中一般会使用这* r3 ^; v, e# z0 N
个参数来依次枚举中的单词。如下示例:
$ U+ U% \( F: q( g: mnames := a b c d1 E# S# w8 n. V' J# X& t
files := $(foreach n,$(names),$(n).o)
+ j/ k4 A2 Y& a* ]6 h( ]$ O1 X以上脚本实现$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次根据“$(n)”计算出9 [3 a, s) j5 }3 w, ]4 Q* T
一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以$(files)的值是“a.o b.o c.o d.o”。
( Y/ v" ?4 `, A' j(注意,foreach 中的参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不2 r/ K5 T# Y) o9 _: X
在作用,其作用域只在 foreach 函数当中)。8 d9 m) Z! a9 i
更多内容关注迅为电子$ X. l7 D0 H+ Z+ i
|
|