文章目录
- 1. 编译器gcc和g++
- 1.1 gcc vs g++有什么区别
- 1.2 gcc编译选项
- 1.2.1 预处理(进行宏替换)
- 1.2.2 编译(生成汇编)
- 1.2.3 汇编(生成机器可识别代码)
- 1.2.4 链接(生成可执行文件或者库文件)
- 思考一些问题???
- 2. 动态链接和静态链接
- 2.1 静态连接
- 2.2 动态连接
- 3. 动态库和静态库
- 3.1 静态库 .a结尾(libxxx.a)
- 3.2 动态库 .so结尾(libxxx.so)
- 4. 自动化构建-make/Makefile
- 4.1 写一个最简单的make/makefile
- 4.2 介绍依赖关系和依赖方法
- 4.3 解析 .PHONY
- 4.3.1 .PHONY的本质
- 4.3.2 为什么需要.PHONY?
- 4.3.3 为什么普通编译目标不用.PHONY?
- 4.4.4 .PHONY的底层原理
- 4.5 总结推导过程
- 4.6 扩展makefile的语法
1. 编译器gcc和g++
1.1 gcc vs g++有什么区别
gcc:C 语言编译器,默认按 C 语言标准编译,链接时不自动链接 C++ 标准库
g++:C++ 语言编译器,默认按 C++ 语言标准编译,会自动链接 C++ 标准库(libstdc++),并处理 C++ 特有的名称修饰(name mangling)
建议还是:gcc编C语言,g++编C++代码各司其职
1.2 gcc编译选项
gcc和g++二者的选项完全一致
gcc[选项]要编译的文件[选项]目标文件接下来,我们用另一种视角重新理解翻译程序的过程:
1.2.1 预处理(进行宏替换)
- 预处理的基本功能:
头文件展开:编译器会把头文件拷贝到源文件内部–头文件展开(头文件拷到源文件里)头文件声明和定义放一起,为了制作成库必须把头文件和源文件分开,达到一个保护的效果。
去注释:去除注释
宏替换:替换
条件编译:条件编译的本质是在对代码做裁剪,编译器能去注释,也就说明编译器具有裁剪的功能。那为什么编译器会有裁剪这个功能?有利于代码维护特定条件下的特定功能。类似于一款软件有免费的vs收费的?公司内部是如何维护这种项目代码的?他们仅仅需要维护收费的,收费的接口留下来,剩下的就是把其余接口隐藏起来,这就可以利用到条件编译了。 - 预处理是以#开头的代码行
- 实例:
gcc-Ehello.c-ohello.i- 选项-E的作用是让gcc在预处理结束后停止编译的过程
- 选项-o说明后面的文件是目标文件,“.i”为已经处理过的C原始程序
1.2.2 编译(生成汇编)
- 在这个阶段中gcc首先要检查代码的规范性、是否有语法错误等、以确定代码实际要做的工作,检查无误后将代码翻译成汇编语言
- 将高级语言翻译成汇编语言,-S程序开始翻译,将高级语言翻译成汇编语言就停下来
- 实例:
gcc-Stext.i-otext.s1.2.3 汇编(生成机器可识别代码)
将汇编语言翻译为二进制文件(.o:可重定位目标二进制文件)
- 汇编阶段是把编译阶段生成的“.s”文件转成目标文件
- 使用选项-c就可以看到汇编代码已经转化为“.o”的二进制目标文件
- 示例:
gcc-ctext.s-otext.o.o可重定位二进制文件不能直接运行,因为这里没有声明,没有定义,没有链接,gcc只有库和头文件在哪里需要连接后才能运行。但实际更常用的写法是直接从 .c生成 .o这里只做演示。
1.2.4 链接(生成可执行文件或者库文件)
编译成功之后就可以链接了:
gcc text.o-otext总结:
| 选项 | 作用 | 输出文件 |
|---|---|---|
-E | 仅预处理(宏展开、头文件展开、去注释) | .i |
-S | 预处理 + 编译,生成汇编代码 | .s |
-c | 预处理 + 编译 + 汇编,生成目标文件 | .o |
| (无) | 完整编译链接,生成可执行文件 | 默认为a.out |
思考一些问题???
- 为什么要把C语言翻译成汇编语言?历史是由汇编发展过来的
- 你的计算机为什么叫二进制计算机?因为二进制可以被CPU所执行,被CPU运算。(指令集会被设计成二进制)把指令合起来,这个就是程序。然而翻译语言的本质:也就是将高级语言转化成CPU能识别的指令集。古早时候,程序员都是用二进制编程的,纸带编程:
后面就引入了汇编语言,汇编语言就是将所有指令用单词进行映射,形成一种更好理解的语言。一种语言的的发明就意味着对应的编译器被发明出来,这个时候编译器就诞生了。编译器的核心工作就是将汇编语言翻译为机器语言,随着语言的不断演进,每发明一门语言就需要有配套的编译器保证其能被翻译为机器码,所以我们说语法的本质是编译器的翻译规则。这个时候就又引进了一个问题:
3. 我们是先有的编译器还是先有对应的语言?
我们说语言的本质是编译器的规则,规则不明确是没有语言的,那应该是先有语言再有编译器的。我们知道汇编语言的编译器是由汇编语言写的,我们不知道翻译的规则怎么写出的汇编语言,我们知道了翻译规则没有汇编语言怎么写编译?这就陷入了鸡生蛋蛋生鸡的问题。所以说,最早的汇编语言编译器是用机器码(手工写二进制/十六进制)编写的,二进制语言写完编译器逐步发展出汇编语言,汇编语言不断发展,发展到后面汇编语言再用它自己逐步的重写编译器,这个过程就叫做–编译自举。
4. 开发一门语言最必要的条件有那些???
编译器、头文件、库文件
这三个条件缺一不可,平时我们安装开发包的过程就是安装头文件与库
2. 动态链接和静态链接
2.1 静态连接
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个.c文件会形成一个.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:
静态链接实际上就是将我们所需要的库中的方法直接拷贝到我的程序中,其优点是不需要库跳转,一旦链接成功,可执行程序不再需要静态库再次链接。但是其缺点也很明显:
- 浪费空间:在静态链接下,每个可执行程序都会将所依赖的库代码(如 printf.o)完整地拷贝并嵌入到自身的二进制文件中。如果多个程序依赖同一个库,会导致磁盘空间的严重冗余。同时,虽然现代操作系统通过“写时复制”机制在物理内存中共享了只读的代码段,但每个进程仍需在虚拟内存中独立映射这些代码,增加了系统的整体资源开销。
- 更新比较困难:因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2.2 动态连接
动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
[ldd]命令可以用于打印程序或者库文件所依赖的共享列表:
可以看到这里用到了一个C语言动态链接库。
所以,我们应该如何理解库??我们可以举一个简单的例子:
- 我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
- 最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用 。
库的本质其实就是方法的集合。
3. 动态库和静态库
3.1 静态库 .a结尾(libxxx.a)
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,其后缀名一般为“.a”。
3.2 动态库 .so结尾(libxxx.so)
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时再链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为“.so”,如前面所述的 libc.so.6就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件。
和动态库产生关联的过程叫做动态链接,一旦库缺失,所有相关程序都运行不了。动态链接的是关联关系是通过地址完成的。
编译程序,我们通常用动态库和动态链接是最佳实践,我们可以强制使用静态链接编译看看。
安装静态库(默认是没有安装的)-static 链接成静态的,静态链接大小远大于动态链接,会导致内存占用过大。
gcc默认生成的二进制程序,是动态链接的,这点可以通过[file] 命令验证。
4. 自动化构建-make/Makefile
上面描述的编译链接是针对某一个文件的,那么如果我们有后面的工程项目中我们引入头文件、还各种源文件需要一起编译链接,如果有100个,那么我们是不是要一个命令一个命令的编译100次呢?那是非常低效的一种方式。那么有没有什么方法能批量化的构建我们的项目呢?
那么我们就引入了自动化构建方式–make。
构建:编译链接运行
makefile带来的好处就是—“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
4.1 写一个最简单的make/makefile
make:是一条命令
makefile:是一个文件
make是一个命令工具,是一个解释makefile 中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
新建一个文件Makefile(makefile也是可以的),准备一个text.c打印一个HelloWord
vim打开Makefile写入:
[make]运行该Makefile
4.2 介绍依赖关系和依赖方法
依赖关系和依赖方法结合必须成对且具有合理性,我们人与人之间的来往也是构建在依赖关系和依赖方法上的。
结合实际的运用,我们可以加上一个清理的过程:
text.exe:text.c gcc text.c-otext.exe .PHONY:clean clean:rm-ftext.exe text.i text.s text.o我们可以总结出四条结论:
- 依赖关系必须存在,但是依赖文件列表可以为空
- 依赖方法可以是任何shell命令
- clean目标,只是利用make的自动推导能力,让他执行rm命令,在构建工程的视角,看起来就是清理项目,但是其本质是清理不需要的临时文件。
- make后面可以跟目标名字,后面跟着谁,就解析谁的依赖关系和依赖方法,make默认只会推导一条完整的推导链路,并且是推导第一个依赖关系对应的推导。
4.3 解析 .PHONY
工程是需要被清理的:
- 像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行。不过,我们可以显示要make执行。即命令—“make clean”,以此来清除所有的目标文件,以便重编译。
- 但是一般我们这种clean的目标文件,我们将它设置为伪目标,用.PHONY 修饰,伪目标的特性是,总是被执行的。
什么叫做总是被执行?
文件=内容+属性
Modify:内容变更,时间更新
Change:属性变更,时间更新
Access:文件最近被访问的时间,什么叫访问?cat&&stat都叫访问文件,一个文件内容被修改是要从内存刷新到磁盘的,更新的内容都要刷新到磁盘上,查看要比修改的次数多。输出结论:.PHONY让目标总是执行的根本原因在于,让make忽略源文件和可执行目标文件的时间对比
补充:touch可以修改文件所对应的事件
4.3.1 .PHONY的本质
.PHONY 是 Makefile 中的一个特殊目标声明,它的作用是告诉 make:这个目标不是真实文件,不要检查它是否存在,也不要比较它的时间戳,每次都直接执行它所对应的命令。
4.3.2 为什么需要.PHONY?
make 的默认逻辑是:根据“目标和依赖文件的修改时间”来决定是否执行命令。如果目标文件已存在且比所有依赖文件都新,就跳过执行。
对于 clean 这类目标,它的目的不是生成一个叫 clean的文件,而是执行删除操作。如果没有 .PHONY 声明,一旦当前目录下恰好有一个名为 clean的文件,make 就会认为这个目标“已经满足条件”,直接跳过,导致清理命令根本不执行。
4.3.3 为什么普通编译目标不用.PHONY?
像 text.exe这种目标需要生成真实文件,它的执行必须依赖“文件时间戳”来判断是否需要更新——这样才支持增量编译,即只重新编译改过的文件,没改过的不动,从而提高构建效率。
如果给 text.exe加上 .PHONY,每次都会强制重链,反而浪费性能。
4.4.4 .PHONY的底层原理
make 在解析 Makefile 时,会把 .PHONY 声明的目标标记为“非文件目标”。在执行阶段,make 会直接跳过文件存在性检查和时间戳比较,无条件执行命令。因此,这种目标总是被执行——它是被规则驱动,而不是被文件驱动。
4.5 总结推导过程
下面,我们逐步拆解上面text.c->text.exe的过程。
text.exe: text.o gcc-otext.exe text.o text.o: text.s gcc-ctext.s-otext.o text.s: text.i gcc-Stext.i-otext.s text.i: text.c gcc-Etext.c-otext.imake执行
make命令会逐一解析makefile,会形成推导栈,推导栈其实就是依赖关系的集合。
总结make是如何工作的:
- make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到text.exe这个文件,并把这个文件作为最终的目标文件。
- 如果该文件不存在,或者说是text.exe所依赖的后面的text.o文件的文件修改时间要比text.exe这个文件新(可以用touch测试),那么,他就会执行后面所定义的命令来生成text.exe这个文件。
- 如果text.exe所依赖的text .o文件不存在,那么make 会在当前文件中找目标为text .o 文件的依赖性,如果找到则再根据那一个规则生成text.o 文件。(这有点像一个堆栈的过程)
- 你的C文件和H文件是存在的,于是make 会生成text .o 文件,然后再用text.o 文件声明make 的终极任务,也就是执行文件hello 了
- 这就是整个make 的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
- 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make是不会理的。
- make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么make就不工作了。
4.6 扩展makefile的语法
- make会将命令回显回来,怎样不回显命令?每个命令前面写@
形成可执行,还要清理或重新生成。
2. 如果我有一天想要修改文件名,那么通篇都需要改,这样很不友好。那么我们可以
BIN=text.exe SRC=text.c $(BIN):$(SRC) @echo "开始编译代码" gcc -o $@ $^ @echo "编译完成!" .PHONY:clean clean: @echo "清理工程。。。" rm -f $(BIN) @echo "清理完毕。。。"从此往后改就只需要改最上面就可以了。
3. 要是我们的项目中出现了100个源文件,我们应该如何编译
首先建立100个源文件:
touchsrc{1..100}.c同时这批文件还可以批量删除
rmsrc{1..100}.c我们不可能将上述文件一个一个编译,修改makefile
小细节:下述二者都能生成.o文件,gcc -c src1.c会自动生成 src1.o,省略了 -o参数,更简洁。
gcc-csrc1.c-osrc1.o gcc-csrc1.cMakefile的最佳实践(可以解决绝大部分的自动化构建问题):
BIN=text.exe #SRC=$(shell ls *.c)#方法一 SRC=$(wildcard *.c)#wildcard函数,获取当前目录下的所有原文件 OBJ=$(SRC:.c=.o)#将.c换成.o CC=gcc #定义变量或者指令便于后期替换 Echo=echo Rm=rm -f $(BIN):$(OBJ) @$(CC) -o $@ $^ #交代一下各个.o文件应该怎么编.o文件当前并不存在 @$(Echo) "linking $^ to $@...done" %.o:%.c @$(CC) -c $< #表示第一个依赖文件 @$(Echo) "compling $< to $@...done" .PHONY:clean clean: $(Rm) $(OBJ) $(BIN) .PHONY:text text: @$(Echo) "Debug---" @$(Echo) $(SRC); @$(Echo) "Debug---" @$(Echo) $(OBJ); @$(Echo) "Debug---"欢迎大家批评指正!!!