二、Linux基础开发工具(2)

3. 编译器gcc/g++

3-1背景知识

  1. 预处理(进行宏替换/去注释/条件编译/头文件展开等)

  2. 编译(生成汇编)

  3. 汇编(生成机器可识别代码)

  4. 连接(生成可执行文件或库文件)

3-2 gcc编译选项

格式 :gcc [选项] 要编译的文件 [选项] [目标文件]

3-2-1 预处理(进行宏替换)

• 预处理功能主要包括宏定义,文件包含,条件编译,去注释等。

• 预处理指令是以#号开头的代码行。

• 实例: gcc --E hello.c --o hello.i :

• 选项"-E",该选项的作用是让 gcc 在预处理结束后停止编译过程。

• 选项"-o"是指目标文件,".i"文件为已经过预处理的C原始程序。

3-2-2 编译(生成汇编)

• 在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作, 在检查无误后,gcc 把代码翻译成汇编语言。

• 用户可以使用"-S"选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。

• 实例: gcc --S hello.i --o hello.s

3-2-3 汇编(生成机器可识别代码)

• 汇编阶段是把编译阶段生成的".s"文件转成目标文件

• 在此可使用选项"-c"就可看到汇编代码已转化为".o"的二进制目标代码了

• 实例: gcc --c hello.s --o hello.o

3-2-4 连接(生成可执行文件或库文件)

• 在成功编译之后,就进入了链接阶段。

• 实例: gcc hello.o --o hello

3-3 动态链接和静态链接

在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文 件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数, 但是每个源文件都是独立编译的,即每个*.c文件会形成一个*.o文件,为了满足前面说的依赖关系,则 需要将这些源文件产生的目标文件进行链接从而形成一个可以执行的程序 。这个链接的过程就是静态链接。静态链接的缺点很明显:

• 浪费空间:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对 同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有 printf.o,所以同一个目标文件都在内存存在多个副本;

• 更新比较困难:因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程 序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在 执行的时候运行速度快。

动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对 独立部分 ,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

动态链接其实远比静态链接要常用得多。

在这里涉及到一个重要的概念: 库

• 我们的C程序中,并没有定义"printf"的函数实现,且在预编译中包含的"stdio.h"中也只有该 函数的声明,而没有定义函数的实现,那么,是在哪里实"printf"函数的呢?

• 最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了 ,在没有特别指定 时,gcc 会到系统默认的搜索路径"/usr/lib"下进行查找,也就是链接到 libc.so.6 库函数中去,这样 就能实现函数"printf"了,而这也就是链接的作用

3-4 静态库和动态库

• 静态库是指**编译链接时,把库文件的代码全部加入到可执行文件中,**因此生成的文件比较大,但在运 行时也就不再需要库文件了。其后缀名一般为".a"

• 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由 运行时链接文件加载库, 这样可以节省系统的开销。动态库一般后缀名为".so",如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文 件,如下所示。 gcc hello.o --o hello

• gcc默认生成的二进制程序,是动态链接的,这点可以通过 file 命令验证。

注意:

• Linux下,动态库XXX.so, 静态库XXX.a

• Windows下,动态库XXX.dll, 静态库XXX.lib

3-5 gcc其他常用选项

4. 自动化构建-make/Makefile

4-1 背景

• 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力

• 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一 系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至 于进行更复杂的功能操作

• makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全 自动编译,极大的提高了软件开发的效率。

make是一个命令工具 ,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这 个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile 都成为了一种在工程方面的编译方法。

make是一条命令,makefile是一个文件 ,两个搭配使用,完成项目自动化构建

4-2 基本使用

make一下,然后执行程序

简单介绍一下关于makefile:

cpp 复制代码
test:test.c
gcc -o test test.c
.PHONY:clean
clean:
rm -f test

依赖关系

• 上面的文件test,它依赖test.c

依赖方法

• gcc -o test test.c ,就是与之对应的依赖关系

项目清理 • 工程是需要被清理的

• 像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动 执行,不过,我们可以显示要make执行。即命令------"make clean",以此来清除所有的目标 文件,以便重编译。

• 但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标的特性 是,总是被执行的。.PHONY:让make忽略源文件和可执行目标文件的M时间对比

• 可以将我们的 test目标文件声明成伪目标,测试一下。可以看到,make clean后test文件被删除了

4-3 推导过程

makefile内容如下:

编译:

• make是如何工作的,在默认的方式下,也就是我们只输入make命令。

那么: 1. make会在当前目录下找名字叫"Makefile"或"makefile"的文件。

  1. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到 myproc 这 个文件,并把这个文件作为最终的目标文件。

  2. 如果 myproc 文件不存在,或是 myproc 所依赖的后面的 myproc.o 文件的文件修改时间要 比 myproc 这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成 myproc 这个文件。

  3. 如果 myproc 所依赖的 myproc.o 文件不存在,那么 make 会在当前文件中找目标为 myproc.o 文件的依赖性,如果找到则再根据那一个规则生成 myproc.o 文件。(这有点像一 个堆栈的过程)

  4. 当然,你的C文件和H文件是存在的啦,于是 make 会生成 myproc.o 文件,然后再用 myproc.o 文件声明 make 的终极任务,也就是执行文件 hello 了。

  5. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个 目标文件

  6. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并 报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。

  7. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么就不工作啦。

明白了:代码字体格式保持原来那种代码块效果不变,只把标题层级改掉:

  • 4-4、4-5、4-6、4-7、4-8 改成二级标题:##
  • 它们下面的 4-4-1、4-4-2 这种改成三级标题:###
  • 内容不精简

下面是修改后的版本。


4-4 Makefile 的基本语法

前面我们已经知道,make 是一个自动化构建工具,而 Makefile 就是告诉 make:

哪些文件需要生成?

这些文件依赖哪些文件?

生成它们时要执行哪些命令?

也就是说,Makefile 本质上是在描述 目标、依赖、命令 三者之间的关系。

4-4-1 Makefile 的基本格式

一个最基本的 Makefile 规则格式如下:

复制代码
目标文件: 依赖文件
	生成目标文件所需要执行的命令

也可以写成更通用的形式:

复制代码
target: dependencies
	command

其中:

  • target 表示目标文件,也就是我们最终想要生成的文件
  • dependencies 表示依赖文件,也就是生成目标文件需要用到哪些文件
  • command 表示命令,也就是如何根据依赖文件生成目标文件

例如:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

这里的含义是:

  • myproc 是目标文件
  • myproc.c 是依赖文件
  • gcc myproc.c -o myproc 是生成目标文件的命令

也就是说,如果想得到 myproc,就需要依赖 myproc.c,然后执行下面的 gcc 命令。

这条 Makefile 规则,实际上等价于我们手动在命令行中输入:

复制代码
gcc myproc.c -o myproc

但是使用 Makefile 之后,我们就不需要每次手动输入这一长串命令了,只需要执行:

复制代码
make

即可。

这里有一个非常重要的细节:

Makefile 中命令前面必须是 Tab 键,不能是普通空格。

例如:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

第二行 gcc 前面必须是一个 Tab。

如果写成普通空格,make 很可能会报错:

复制代码
Makefile:2: *** missing separator.  Stop.

这个错误对于初学者来说非常常见,所以一定要注意。

4-4-2 一个最简单的 Makefile

假设我们有一个源文件 test.c:

复制代码
#include <stdio.h>

int main()
{
    printf("hello makefile\n");
    return 0;
}

如果不使用 Makefile,我们平时手动编译会这样写:

复制代码
gcc test.c -o test

如果使用 Makefile,可以这样写:

复制代码
test: test.c
	gcc test.c -o test

然后在命令行中直接输入:

复制代码
make

make 会自动寻找当前目录下的 Makefile 或 makefile 文件,然后执行其中的规则。

执行结果就相当于帮我们运行了:

复制代码
gcc test.c -o test

编译完成之后,当前目录下就会生成一个可执行程序:

复制代码
test

然后我们就可以运行:

复制代码
./test

输出结果:

复制代码
hello makefile

所以,最简单的 Makefile 其实就是把我们平时手动输入的编译命令写到文件里面,让 make 帮我们执行。

4-4-3 Makefile 文件名

make 默认会在当前目录下寻找下面几个文件名:

复制代码
GNUmakefile
makefile
Makefile

一般情况下,我们最常用的是:

复制代码
Makefile

注意首字母大写。

所以我们通常会这样创建文件:

复制代码
touch Makefile

然后把规则写进去。

当我们执行:

复制代码
make

make 就会自动读取当前目录下的 Makefile。

当然,也可以使用 -f 指定 Makefile 文件名:

复制代码
make -f mymakefile

不过初学阶段一般不用这么写,直接使用 Makefile 这个文件名即可。

4-5 Makefile 的执行逻辑

Makefile 看起来只是几行规则,但它并不是简单地从上往下全部执行。

make 真正厉害的地方在于:

它会根据目标、依赖和文件时间戳,判断哪些命令需要执行,哪些命令不需要执行。

4-5-1 make 默认执行第一个目标

例如有这样一个 Makefile:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

clean:
	rm -f myproc

当我们直接输入:

复制代码
make

它会默认执行第一个目标:

复制代码
myproc

所以会生成可执行程序 myproc。

如果我们想执行 clean,需要显式指定:

复制代码
make clean

这样 make 才会执行:

复制代码
clean:
	rm -f myproc

因此可以总结:

make 默认执行 Makefile 中的第一个目标。

所以一般来说,我们会把最终目标放在 Makefile 的最前面。

比如我们最终想生成 myproc,就把它写在第一条规则:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

这样直接执行 make 时,就能生成最终程序。

4-5-2 依赖关系与时间戳

make 的强大之处在于,它会根据文件的修改时间判断是否需要重新编译。

例如:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

第一次执行:

复制代码
make

因为当前目录下还没有 myproc 这个目标文件,所以 make 会执行命令:

复制代码
gcc myproc.c -o myproc

于是生成了可执行程序 myproc。

如果此时我们再次执行:

复制代码
make

可能会看到:

复制代码
make: 'myproc' is up to date.

意思是:

目标文件 myproc 已经是最新的了,不需要重新编译。

那么 make 是怎么判断的呢?

它会比较:

  • 目标文件 myproc 的修改时间
  • 依赖文件 myproc.c 的修改时间

如果 myproc.c 比 myproc 更新,说明源文件被修改过,就需要重新编译。

如果 myproc 比 myproc.c 更新,说明目标文件已经是最新的,就不用重新编译。

所以 make 的核心思想是:

如果目标文件不存在,或者依赖文件比目标文件更新,就执行对应命令。

换句话说:

复制代码
目标不存在:执行命令
依赖更新了:执行命令
目标比依赖更新:不执行命令

这也是为什么 make 可以提高编译效率。

它不会每次都全部重新编译,而是只重新编译真正需要更新的部分。

4-5-3 添加 clean 清理规则

在开发过程中,我们经常需要删除编译生成的文件。

比如删除可执行程序:

复制代码
rm -f myproc

如果每次都手动输入,也比较麻烦。

我们可以把它写进 Makefile:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

clean:
	rm -f myproc

这样以后只需要执行:

复制代码
make clean

就可以清理编译结果。

这里的 clean 并不是一个真正需要生成的文件,而是一个动作:

复制代码
clean 表示清理项目

不过这里有一个问题。

如果当前目录下刚好有一个文件也叫 clean,那么执行:

复制代码
make clean

make 可能会认为目标 clean 已经存在,并且是最新的,于是不会执行清理命令。

所以我们通常会把 clean 声明成伪目标。

4-5-4 伪目标 .PHONY

伪目标的意思是:

这个目标不是真的要生成一个文件,而只是一个命令名称。

写法如下:

复制代码
.PHONY: clean

完整写法:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

.PHONY: clean
clean:
	rm -f myproc

这样,无论当前目录下有没有 clean 这个文件,执行:

复制代码
make clean

都会执行清理命令。

常见的伪目标还有:

复制代码
.PHONY: clean all

其中:

  • clean 通常用于清理生成文件
  • all 通常用于表示最终要生成的所有目标

例如:

复制代码
.PHONY: all clean

all: myproc

myproc: myproc.c
	gcc myproc.c -o myproc

clean:
	rm -f myproc

这里的 all 也是一个伪目标,它本身不生成文件,只是依赖 myproc。

执行:

复制代码
make

由于第一个目标是 all,所以 make 会先构建 all,而 all 依赖 myproc,于是最终会生成 myproc。

执行:

复制代码
make clean

则会清理生成的程序。

4-6 Makefile 中的变量和自动化变量

当 Makefile 变复杂之后,如果还一直重复写文件名和命令,就会非常麻烦。

例如:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

这里 myproc 出现了两次,myproc.c 也出现了两次。

如果以后想改程序名,就需要到处改。

所以 Makefile 支持变量。

4-6-1 普通变量

Makefile 中定义变量的基本格式:

复制代码
变量名=变量值

例如:

复制代码
CC=gcc
TARGET=myproc
SRC=myproc.c

使用变量时,要写成:

复制代码
$(变量名)

例如:

复制代码
$(CC)
$(TARGET)
$(SRC)

所以原来的 Makefile:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

可以改成:

复制代码
CC=gcc
TARGET=myproc
SRC=myproc.c

$(TARGET): $(SRC)
	$(CC) $(SRC) -o $(TARGET)

展开之后就相当于:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

这样写的好处是:

如果以后想把目标文件名从 myproc 改成 test,只需要改变量:

复制代码
TARGET=test

不用到处修改命令。

完整 Makefile 可以写成:

复制代码
CC=gcc
TARGET=myproc
SRC=myproc.c

$(TARGET): $(SRC)
	$(CC) $(SRC) -o $(TARGET)

.PHONY: clean
clean:
	rm -f $(TARGET)

4-6-2 常见变量名

虽然 Makefile 中变量名可以自己定义,但是实际开发中有一些比较常见的习惯写法。

复制代码
CC=gcc
CXX=g++
CFLAGS=-Wall -g
CXXFLAGS=-Wall -g
TARGET=myproc
SRCS=main.c add.c sub.c
OBJS=main.o add.o sub.o

它们大概表示:

  • CC:C 编译器,一般是 gcc
  • CXX:C++ 编译器,一般是 g++
  • CFLAGS:C 编译选项
  • CXXFLAGS:C++ 编译选项
  • TARGET:最终生成的目标程序
  • SRCS:源文件列表
  • OBJS:目标文件列表,也就是 .o 文件列表

例如:

复制代码
CC=gcc
CFLAGS=-Wall -g
TARGET=myproc
SRC=myproc.c

$(TARGET): $(SRC)
	$(CC) $(CFLAGS) $(SRC) -o $(TARGET)

这里:

复制代码
CFLAGS=-Wall -g

表示编译时加上两个选项:

复制代码
-Wall

表示打开常见警告。

复制代码
-g

表示生成调试信息,方便使用 gdb 调试。

展开后就是:

复制代码
gcc -Wall -g myproc.c -o myproc

4-6-3 自动化变量 @、^、$<

Makefile 中还有一些特殊变量,它们不需要我们自己定义,而是 make 自动生成的,所以叫自动化变量。

常用的有三个:

复制代码
$@  表示目标文件
$^  表示所有依赖文件
$<  表示第一个依赖文件
1. $@ 表示目标文件

例如:

复制代码
myproc: myproc.c
	gcc myproc.c -o myproc

可以写成:

复制代码
myproc: myproc.c
	gcc myproc.c -o $@

这里 $@ 就表示当前规则的目标,也就是:

复制代码
myproc

所以命令展开后就是:

复制代码
gcc myproc.c -o myproc
2. $^ 表示所有依赖文件

例如:

复制代码
myproc: main.o add.o sub.o
	gcc main.o add.o sub.o -o myproc

可以写成:

复制代码
myproc: main.o add.o sub.o
	gcc $^ -o $@

这里:

复制代码
$^

代表所有依赖文件:

复制代码
main.o add.o sub.o

而:

复制代码
$@

代表目标文件:

复制代码
myproc

所以命令展开后就是:

复制代码
gcc main.o add.o sub.o -o myproc
3. $< 表示第一个依赖文件

例如:

复制代码
main.o: main.c
	gcc -c main.c -o main.o

可以写成:

复制代码
main.o: main.c
	gcc -c $< -o $@

这里:

  • $< 表示第一个依赖文件,也就是 main.c
  • $@ 表示目标文件,也就是 main.o

所以命令展开后就是:

复制代码
gcc -c main.c -o main.o

注意:

$< 只表示第一个依赖文件。

如果规则是:

复制代码
myproc: main.o add.o sub.o
	gcc $< -o $@

那么 $< 只表示:

复制代码
main.o

不会表示后面的 add.o sub.o。

所以链接多个 .o 文件时,一般用 \^,而不是 <。

4-7 多文件项目的 Makefile 编写

实际项目中,一般不会只有一个 .c 文件。

如果项目中有多个源文件,我们就需要写稍微完整一点的 Makefile。

4-7-1 直接编译多个源文件

假设有三个文件:

复制代码
main.c
add.c
sub.c

如果手动编译,可以这样写:

复制代码
gcc main.c add.c sub.c -o myproc

对应的 Makefile 可以写成:

复制代码
myproc: main.c add.c sub.c
	gcc main.c add.c sub.c -o myproc

这个写法能用,但不够好。

因为每次只要有一个 .c 文件修改,都会把所有 .c 文件重新编译一遍。

例如我们只修改了 add.c,然后执行:

复制代码
make

make 会重新执行:

复制代码
gcc main.c add.c sub.c -o myproc

这样 main.c 和 sub.c 也会跟着重新编译。

在小项目中影响不大,但是如果项目很大,编译速度就会变慢。

所以更推荐的方式是:

先把每个 .c 文件编译成 .o 文件,再把所有 .o 文件链接成最终程序。

4-7-2 使用 .o 文件进行分步编译

C 程序的构建一般可以分为两步:

第一步:编译源文件,生成目标文件 .o

复制代码
gcc -c main.c -o main.o
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o

这里的 -c 表示只编译,不链接。

第二步:链接所有 .o 文件,生成可执行程序

复制代码
gcc main.o add.o sub.o -o myproc

对应 Makefile:

复制代码
myproc: main.o add.o sub.o
	gcc main.o add.o sub.o -o myproc

main.o: main.c
	gcc -c main.c -o main.o

add.o: add.c
	gcc -c add.c -o add.o

sub.o: sub.c
	gcc -c sub.c -o sub.o

.PHONY: clean
clean:
	rm -f myproc main.o add.o sub.o

这个 Makefile 的构建过程是:

复制代码
1. make 想生成 myproc
2. 发现 myproc 依赖 main.o add.o sub.o
3. 如果这些 .o 文件不存在,就先生成它们
4. main.o 由 main.c 编译得到
5. add.o 由 add.c 编译得到
6. sub.o 由 sub.c 编译得到
7. 三个 .o 文件都准备好后,再链接成 myproc

这样做的好处是:

如果只修改了 add.c,那么下次执行:

复制代码
make

make 只会重新生成:

复制代码
add.o

然后再重新链接生成:

复制代码
myproc

不会重新编译 main.c 和 sub.c。

这就是 make 提高编译效率的关键。

4-7-3 使用变量优化多文件 Makefile

前面的 Makefile:

复制代码
myproc: main.o add.o sub.o
	gcc main.o add.o sub.o -o myproc

main.o: main.c
	gcc -c main.c -o main.o

add.o: add.c
	gcc -c add.c -o add.o

sub.o: sub.c
	gcc -c sub.c -o sub.o

.PHONY: clean
clean:
	rm -f myproc main.o add.o sub.o

可以用变量优化:

复制代码
CC=gcc
TARGET=myproc
OBJS=main.o add.o sub.o

$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

main.o: main.c
	$(CC) -c main.c -o main.o

add.o: add.c
	$(CC) -c add.c -o add.o

sub.o: sub.c
	$(CC) -c sub.c -o sub.o

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

这样一来,如果目标文件名改了,只需要修改:

复制代码
TARGET=myproc

如果 .o 文件列表变了,只需要修改:

复制代码
OBJS=main.o add.o sub.o

4-7-4 使用自动化变量继续优化

上面的写法还可以继续优化。

原来:

复制代码
$(TARGET): $(OBJS)
	$(CC) $(OBJS) -o $(TARGET)

可以写成:

复制代码
$(TARGET): $(OBJS)
	$(CC) $^ -o $@

因为:

  • \^ 表示所有依赖文件,也就是 (OBJS)
  • @ 表示目标文件,也就是 (TARGET)

原来:

复制代码
main.o: main.c
	$(CC) -c main.c -o main.o

可以写成:

复制代码
main.o: main.c
	$(CC) -c $< -o $@

因为:

  • $< 表示第一个依赖文件,也就是 main.c
  • $@ 表示目标文件,也就是 main.o

完整 Makefile:

复制代码
CC=gcc
TARGET=myproc
OBJS=main.o add.o sub.o

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

main.o: main.c
	$(CC) -c $< -o $@

add.o: add.c
	$(CC) -c $< -o $@

sub.o: sub.c
	$(CC) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

这样已经比最初的写法简洁很多了。

4-7-5 模式规则

虽然上面的写法已经不错,但还有重复:

复制代码
main.o: main.c
	$(CC) -c $< -o $@

add.o: add.c
	$(CC) -c $< -o $@

sub.o: sub.c
	$(CC) -c $< -o $@

这些规则的结构完全一样:

.c 文件生成对应的 .o 文件。

所以可以使用模式规则。

写法如下:

复制代码
%.o: %.c
	$(CC) -c $< -o $@

这里的 % 可以理解成通配符。

它表示相同的文件名前缀。

例如:

复制代码
%.o: %.c

可以匹配:

复制代码
main.o: main.c
add.o: add.c
sub.o: sub.c

也就是说:

  • main.o 由 main.c 生成
  • add.o 由 add.c 生成
  • sub.o 由 sub.c 生成

于是 Makefile 可以写成:

复制代码
CC=gcc
TARGET=myproc
OBJS=main.o add.o sub.o

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

这就是一个比较常见的 Makefile 写法。

4-8 Makefile 的最终推荐模板

前面我们一步一步从最简单的 Makefile,讲到了变量、自动化变量、多文件编译和模式规则。

最后我们整理出一个比较实用的模板。

4-8-1 C 语言项目 Makefile 模板

复制代码
CC=gcc
CFLAGS=-Wall -g
TARGET=myproc

SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

这个模板中:

复制代码
CC=gcc

表示使用 gcc 编译器。

复制代码
CFLAGS=-Wall -g

表示编译选项。

其中:

  • -Wall 表示开启常见警告

  • -g 表示生成调试信息,方便 gdb 调试

    TARGET=myproc

表示最终生成的可执行程序叫 myproc。

复制代码
SRCS=$(wildcard *.c)

表示获取当前目录下所有 .c 文件。

例如当前目录下有:

复制代码
main.c add.c sub.c

那么:

复制代码
$(wildcard *.c)

展开后就是:

复制代码
main.c add.c sub.c

OBJS=$(SRCS:.c=.o)

表示把 SRCS 中所有 .c 后缀替换成 .o 后缀。

也就是说:

复制代码
main.c add.c sub.c

会变成:

复制代码
main.o add.o sub.o

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

表示最终目标 myproc 依赖所有 .o 文件。

链接时:

  • $^ 表示所有依赖文件,也就是所有 .o

  • $@ 表示目标文件,也就是 myproc

    %.o: %.c
    (CC) (CFLAGS) -c < -o @

表示所有 .o 文件都可以由同名 .c 文件编译得到。

其中:

  • $< 表示第一个依赖文件,也就是对应的 .c

  • $@ 表示目标文件,也就是对应的 .o

    .PHONY: clean
    clean:
    rm -f (TARGET) (OBJS)

表示清理生成的可执行程序和 .o 文件。

使用时,只需要执行:

复制代码
make

即可编译项目。

执行:

复制代码
make clean

即可清理项目。

以后新增一个 .c 文件时,不需要修改 Makefile。

比如新增:

复制代码
mul.c

再次执行:

复制代码
make

它会自动被编译进去。

4-8-2 C++ 项目 Makefile 模板

如果是 C++ 项目,可以改成:

复制代码
CXX=g++
CXXFLAGS=-Wall -g
TARGET=myproc

SRCS=$(wildcard *.cc)
OBJS=$(SRCS:.cc=.o)

$(TARGET): $(OBJS)
	$(CXX) $^ -o $@

%.o: %.cc
	$(CXX) $(CXXFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

如果你的 C++ 源文件后缀是 .cpp,就写成:

复制代码
CXX=g++
CXXFLAGS=-Wall -g
TARGET=myproc

SRCS=$(wildcard *.cpp)
OBJS=$(SRCS:.cpp=.o)

$(TARGET): $(OBJS)
	$(CXX) $^ -o $@

%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

这里和 C 语言模板的区别主要是:

  • C 语言一般使用 gcc
  • C++ 一般使用 g++
  • C 编译选项习惯写成 CFLAGS
  • C++ 编译选项习惯写成 CXXFLAGS

4-8-3 一个完整的使用过程

假设当前目录下有这些文件:

复制代码
main.c
add.c
sub.c
Makefile

Makefile 内容如下:

复制代码
CC=gcc
CFLAGS=-Wall -g
TARGET=myproc

SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)

$(TARGET): $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(TARGET) $(OBJS)

第一次执行:

复制代码
make

make 会做这些事情:

复制代码
1. 找到默认目标 myproc
2. 发现 myproc 依赖 main.o add.o sub.o
3. 发现 main.o add.o sub.o 不存在
4. 根据 %.o: %.c 规则,分别编译 main.c add.c sub.c
5. 生成 main.o add.o sub.o
6. 最后链接这几个 .o 文件,生成 myproc

然后可以运行:

复制代码
./myproc

如果只修改了 add.c,再次执行:

复制代码
make

make 会做这些事情:

复制代码
1. 发现 add.c 比 add.o 新
2. 所以重新编译 add.c,生成新的 add.o
3. 发现 add.o 比 myproc 新
4. 所以重新链接,生成新的 myproc
5. main.c 和 sub.c 没有修改,所以 main.o 和 sub.o 不会重新生成

如果想清理:

复制代码
make clean

它会执行:

复制代码
rm -f myproc main.o add.o sub.o

4-8-4 总结

Makefile 的核心并不复杂,重点记住三件事。

第一,基本格式:

复制代码
目标: 依赖
	命令

第二,make 会根据 依赖关系 + 文件时间戳 判断是否需要重新执行命令。

第三,常用自动化变量:

$@ 表示目标文件 $^ 表示所有依赖文件 $< 表示第一个依赖文件

初学时可以先掌握这个模板:

CC=gcc CFLAGS=-Wall -g TARGET=myproc SRCS=$(wildcard *.c) OBJS=$(SRCS:.c=.o) $(TARGET): $(OBJS) $(CC) $^ -o $@ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ .PHONY: clean clean: rm -f $(TARGET) $(OBJS)

以后写小型 C 项目时,基本就可以直接套用。

只需要修改:

TARGET=myproc

就能生成自己想要的可执行程序名

cpp 复制代码
BIN=proc.exe # 定义变量
CC=gcc
#SRC=$(shell ls *.c) # 采用shell命令行方式,获取当前所有.c文件名
SRC=$(wildcard *.c) # 或者使用 wildcard 函数,获取当前所有.c文件名
OBJ=$(SRC:.c=.o) # 将SRC的所有同名.c 替换 成为.o 形成目标文件列表
LFLAGS=-o # 链接选项
FLAGS=-c # 编译选项
RM=rm -f # 引入命令

$(BIN):$(OBJ)
    @$(CC) $(LFLAGS) $@ $^ # $@:代表目标文件名。 $^: 代表依赖文件列表
    @echo "linking ... $^ to $@"
%.o:%.c # %.c 展开当前目录下所有的.c。 %.o: 同时展开同名.o
    @$(CC) $(FLAGS) $< # %<: 对展开的依赖.c文件,一个一个的交给gcc。
    @echo "compling ... $< to $@" # @:不回显命令
.PHONY:clean
clean:
    $(RM) $(OBJ) $(BIN) # $(RM): 替换,用变量内容替换它

.PHONY:test
test:
    @echo $(SRC)
    @echo $(OBJ)
相关推荐
tianrun12345 小时前
Ubuntu 24.04 安装 Fcitx5 + Rime + 搜狗词库(替代 IBus 与 Linux 搜狗输入法)
linux·运维·ubuntu
wxytxdy6 小时前
通过猜数字游戏学习Shell脚本的分支、循环编写
linux·学习
YIN_尹6 小时前
【Linux 系统编程】手撕一个简易版的shell命令行解释器
android·linux·运维
小猫咪016 小时前
Linux 软链接和硬链接详解:ln 命令背后的 inode 原理
linux
小脑斧1236 小时前
从入门到精通:Linux 进程间通信 IPC 全解析|管道、共享内存、信号量、消息队列实战
linux·运维·服务器
ABCDEEE76 小时前
3.RAG
java·linux·服务器
剑神一笑6 小时前
Linux zip 与 unzip 命令详解:压缩算法原理与实战技巧
linux·前端·chrome
为思念酝酿的痛7 小时前
Linux线程
linux·服务器·后端
用户2367829801687 小时前
Linux cp 命令深度解析:文件复制的底层原理与高级技巧
linux
沐风_ZTL7 小时前
Ubuntu 22.04中OpenCode 安装与配置完整指南,及常问题解决办法
linux·ai·opencode