【Linux庖丁解牛】—gcc/g++&make/Makefile!

目录

1、Linux编译器-gcc/g++使用

1.1 预处理

1.2 编译

1.3 汇编

1.4 链接

> 动态链接和静态链接 

> 静态库和动态库

​编辑 2、⾃动化构建-make/Makefile

2.1 背景

2.2 基本使用及工作原理

2.3 适度扩展语法


1、Linux编译器-gcc/g++使用

gcc和g++的使用基本相同,这里我就全部以gcc的使用为例了。

下面是gcc的最基础的用法:

下面是我在code.c中编写的一段简单的C代码。

 如果我们用gcc时后面直接跟上源文件(不带任何选项)的话,gcc编译器会在当前目录默认生成一个a.out的可执行文件(这也是gcc最简单基础的用法)

 如果我们想要指定生成的可执行文件名字,我们可以进行如下操作:

 当然我们也可以这样写:

 调换一下顺序是没有影响的,不过-o选项后面一定是跟着我们要生成的可执行文件的名字!

我们在学习C语言时就知道程序的翻译分为4个阶段:预处理、编译、汇编、链接!而下面我就会通过这四个过程讲一下gcc的其他选项和用法!

1.1 预处理

在学习C语言时,我们知道,预处理过程会把程序中的头文件展开,宏替换,去注释和条件编译

但是我们并没有真切的感受到这一过程,现在,我们在Linux中使用工具gcc便可以让我们真切的感受到这一过程了:

-E选项的作用是:开始进行程序翻译,在预处理做完的时候就停下来!

这句命令我们可以这样理解:对code1.c 源文件进行预处理操作并把结果写入临时文件code1.i文件当中(临时文件会自动创建),如果我们没有指定临时文件,结果会在我们的显示器(屏幕)当中直接打印出来!

这里有一个小细节:我们命名预处理过后的临时文件时,习惯性的以.i为后缀(这是一种编程惯例)。

下面是预处理前和预处理后代码的比较:

 果然,通过对比,我们可以很直观的感受到头文件的展开,宏替换,去注释和条件编译。

1.2 编译

编译的过程就是把我们预处理后的代码翻译成汇编。

1.3 汇编

汇编过程就是把编译后的汇编代码生成机器可识别的代码(二进制)

此时我们用vim打开.o文件,会发现里面是一堆乱码(这也在意料之中)。 

1.4 链接

连接过程就是把我们自己写的代码(被翻译成二进制后)其所依赖的库连接起来,最终形成可执行文件或库文件。(其本质就是将所有的.o文件合并在一起)

为什么会有链接这一过程呢?

就如我们这个程序当中使用的printf函数,我们包含的头文件中只有其声明,并没有他的定义。printf函数的定义其实就是在C标准库当中,如果我们想要printf函数运行起来,就必须链接他所依赖的C标准库。

那我们这么知道该可执行程序依赖的库呢?

指令ldd就可以完成;ldd指令是Linux系统下的一个动态链接库查看工具,用于查看一个可执行文件或者共享库所依赖的动态链接库。

> 动态链接和静态链接 

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

• 浪费空间:因为每个可执⾏程序中对所有需要的⽬标⽂件都要有⼀份副本,所以如果多个程序对
同⼀个⽬标⽂件都有依赖,如多个程序中都调⽤了printf()函数,则这多个程序中都含有
printf.o,所以同⼀个⽬标⽂件都在内存存在多个副本;
• 更新⽐较困难:因为每当库函数的代码修改了,这个时候就需要重新进⾏编译链接形成可执⾏程
序。但是静态链接的优点就是,在可执⾏程序中已经具备了所有执⾏程序所需要的任何东西,在
执⾏的时候运⾏速度快

动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对 独⽴部分,在程序运⾏时才将它们链接在⼀起形成⼀个完整的程序,⽽不是像静态链接⼀样把所有程序模块都链接成⼀个单独的可执⾏⽂件。动态链接其实远⽐静态链接要常⽤得多。

在这⾥涉及到⼀个重要的概念:库

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

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

> 静态库和动态库

• 静态库是指编译链接时,把库⽂件的代码全部加⼊到可执⾏⽂件中,因此⽣成的⽂件⽐较⼤,但在运
⾏时也就不再需要库⽂件了。其后缀名⼀般为“.a”


• 动态库与之相反,在编译链接时并没有把库⽂件的代码加⼊到可执⾏⽂件中,⽽是在程序执⾏时由
运⾏时链接⽂件加载库,这样可以节省系统的开销。动态库⼀般后缀名为“.so”,如前⾯所述libc.so.6就是动态库。gcc在编译时默认使⽤动态库。完成了链接之后,gcc就可以⽣成可执⾏⽂件


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

file 指令是一个在类 Unix 操作系统(如 Linux 和 macOS)中广泛使用的命令行工具。它的主要功能是确定文件的类型。当你对一个文件运行 file 命令时,它会检查文件的内容或元数据,并尝试返回一个描述文件类型的字符串。)

当然,我们也可以静态链接:

 ⼀般我们的云服务器,C/C++的静态库并没有安装,可以采⽤如下⽅法安装:

# Centos
yum install glibc-static libstdc++-static -y
#ubuntu
apt install glibc-static libstdc++-static -y

注意:
• Linux下,动态库XXX.so,静态库XXX.a
• Windows下,动态库XXX.dll,静态库XXX.lib

下面是静态链接和动态链接分别形成的可执行程序的体积比较: 

 2、⾃动化构建-make/Makefile

2.1 背景

• 会不会写makefile,从⼀个侧⾯说明了⼀个⼈是否具备完成⼤型⼯程的能⼒。
• ⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义了⼀
系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄
于进⾏更复杂的功能操作。

• makefile带来的好处就是⸺⸺“⾃动化编译”,⼀旦写好,只需要⼀个make命令,整个⼯程完全⾃动编译,极⼤的提⾼了软件开发的效率。
• make是⼀个命令⼯具,是⼀个解释makefile中指令的命令⼯具,⼀般来说,⼤多数的IDE都有这
个命令,⽐如:Delphi的make,VisualC++的nmake,Linux下GNU的make。可⻅,makefile
都成为了⼀种在⼯程⽅⾯的编译⽅法。
• make是⼀条命令,makefile是⼀个⽂件,两个搭配使⽤,完成项⽬⾃动化构建。

2.2 基本使用及工作原理

我们先在code.c里面编写我们要测试的代码:

 然后再当前目录下创建一个Makefile的文件:

 最后,我们只要在当前目录下使用make命令,就会自动帮我们执行语句gcc………然后生成可执行程序myproc。

依赖关系
• 上⾯的⽂件code.c,它依赖myproc.c
依赖⽅法
• gcc -o myproc code.c ,就是与之对应的依赖关系

 以上就是make/Makefile 的最基本用法。

项⽬清理
• ⼯程是需要被清理的(所以我们再完善一下我们的makefile)

• 像clean这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动
执⾏,不过,我们可以显⽰要make执⾏。即命令⸺⸺“make clean”,以此来清除所有的⽬标
⽂件,以便重编译。


• 但是⼀般我们这种clean的⽬标⽂件,我们将它设置为伪⽬标,⽤ .PHONY 修饰,伪⽬标的特性
是,总是被执⾏的。
什么叫做总是被执⾏?

我们先做一个小实验:

当一个源文件被第一次编译后,如果我们不对源文件的内容进行更改,则我们使用make指令时,不会再命中编译。

但是,如果我们用.PHONY修饰myproc,make就会总是被执行:

但我们一般不建议编译程序用.PHONY修饰,原因就在于如果我们有几百个源文件,这几百个源文件没有更新过或者只有一个几个修改过,如果全部都重新编译的话,那效率就会很低,成本也会很高。所以,默认情况下(不被.PHONY修饰的依赖方法),我们对应的依赖方法默认老代码不做重新编译!

 那make又是怎么知道源文件的新旧呢?

这时候就不得不提到文件的三个时间了:

⽂件 = 内容 + 属性

Modify: 内容变更,时间更新

Change:属性变更,时间更新

Access:常指的是⽂件最近⼀次被访问的时间。在Linux的早期版本中,每当⽂件被访问时,其atime 都会更新。但这种机制会导致⼤量的IO操作。具体更新原则,不做过多解释。  

每个文件和可执行程序都会被记录三个时间,而make则会根据源文件和可执行文件的modify时间的新旧来判断要不要重新编译该文件。若一个源文件的modify时间比可执行程序的时间更新,则我们make时,该源文件就会被重新编译。

• make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make命令。那么:
1. make会在当前⽬录下找名字叫“Makefile”或“makefile”的⽂件。
2. 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 myproc 这
个⽂件,并把这个⽂件作为最终的⽬标⽂件。
3. 如果 myproc ⽂件不存在,或是 myproc 所依赖的后⾯的 myproc.o ⽂件的⽂件修改时间要
⽐ myproc 这个⽂件新(可以⽤ touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成
myproc 这个⽂件。
4. 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为
myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀
个堆栈的过程)
5. 当然,你的C⽂件和H⽂件是存在的啦,于是 make 会⽣成myproc.o ⽂件,然后再⽤
myproc.o ⽂件声明 make 的终极任务,也就是执⾏⽂件 hello 了。
6. 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个
⽬标⽂件。
7. 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并
报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。

8. make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不在,那么对
不起,我就不⼯作啦。

2.3 适度扩展语法

上面说到过make的好处就是让我们的工程编译更加的方便,但是到目前为止,我们并没有看到make命令让我们的编译更加方便,甚至还更加复杂了。

一个源文件的编译,如果我们使用make的方式来说雀氏让我们程序的编译更加复杂了。但是未来我们面对的绝大多数工程都会有很多不同的源文件,这时候我们如果说一个一个的编译的源文件,效率就太低了。

下面我们就来完善一下我们的make/Makefile工具:

  1 BIN=myproc #目标文件/可执行                                                                                                  
  2 SRC=$(shell ls *.c)#显示当前目录下的所有.c文件名/源文件
  3 #SRC=$(wildcard *.c)
  4 OBJ=$(SRC:.c=.o)#SRC内部的文件名:.c->.o/目标.o文件
  5 CC=gcc#编译工具gcc
  6 LFLAGS=-o#链接形成可执行选项
  7 FLAGS=-c#形成.o文件选项
  8 RM=rm -f
  9 
 10 $(BIN):$(OBJ)#将所有的.o文件链接形成可执行
 11     @$(CC) $(LFLAGS) $@ $^
 12     @echo "linking ... $^ to $@"
 13 %.o:%.c#将所有的源文件编译形成.o文件
 14     @$(CC) $(FLAGS) $<
 15     @echo "compling ... $< to $@"
 16 .PHONY:clean
 17 clean:
 18     $(RM) $(BIN) $(OBJ)#清楚所有的.o文件和形成的可执行文件

 使用一下也没有问题。

Makefile下一些特殊符号的解释: 

 补充一下:@符号的作用是在使用Make命令时不回显该依赖方法(也就是不回显该条字符串)(不加@符号也行,看个人习惯)。