画板里面只是整体思路,要和本节博客相结合着看,博客里面的知识点讲解的还是很详细的
三、编译器gcc/g++
3.1 背景知识
- 预处理(进⾏宏替换/去注释/条件编译/头⽂件展开等)
- 编译(⽣成汇编)
- 汇编(⽣成机器可识别代码)
- 连接(⽣成可执⾏⽂件或库⽂件)
3.2 gcc编译选项
格式: gcc [选项] 要编译的⽂件 [选项] [⽬标⽂件]
【预处理(进⾏宏替换)】
- 预处理功能主要包括宏定义,⽂件包含,条件编译,去注释等。
- 预处理指令是以#号开头的代码⾏。
- 实例: gcc –E hello.c –o hello.i
- 选项“-E”,该选项的作⽤是让 gcc 在预处理结束后停⽌编译过程。
- 选项“-o”是指⽬标⽂件,“.i”⽂件为已经过预处理的C原始程序。
【编译(⽣成汇编)】
- 在这个阶段中,gcc ⾸先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的⼯作, 在检查⽆误后,gcc 把代码翻译成汇编语⾔。
- ⽤⼾可以使⽤“-S”选项来进⾏查看,该选项只进⾏编译⽽不进⾏汇编,⽣成汇编代码。
- 实例: gcc –S hello.i –o hello.s
【汇编(⽣成机器可识别代码)】
- 汇编阶段是把编译阶段⽣成的“.s”⽂件转成⽬标⽂件
- 读者在此可使⽤选项“-c”就可看到汇编代码已转化为“.o”的⼆进制⽬标代码了
- 实例: gcc –c hello.s –o hello.o
【链接(⽣成可执⾏⽂件或库⽂件)】
- 在成功编译之后,就进⼊了链接阶段。
- 实例: gcc hello.o –o hello(将.o文件和库进行链接形成可执行程序)
3.3 动态链接和静态链接
在我们的实际开发中,不可能将所有代码放在⼀个源⽂件中,所以会出现多个源⽂件,⽽且多个源⽂件之间不是独⽴的,⽽会存在多种依赖关系,如⼀个源⽂件可能要调⽤另⼀个源⽂件中定义的函数, 但是每个源⽂件都是独⽴编译的,即每个*.c⽂件会形成⼀个*.o⽂件,为了满⾜前⾯说的依赖关系,则 需要将这些源⽂件产⽣的⽬标⽂件进⾏链接,从⽽形成⼀个可以执⾏的程序。这个链接的过程就是静 态链接。静态链接的缺点很明显:
- 浪费空间:因为每个可执⾏程序中对所有需要的⽬标⽂件都要有⼀份副本,所以如果多个程序对 同⼀个⽬标⽂件都有依赖,如多个程序中都调⽤了printf()函数,则这多个程序中都含有 printf.o,所以同⼀个⽬标⽂件都在内存存在多个副本;
- 更新⽐较困难:因为每当库函数的代码修改了,这个时候就需要重新进⾏编译链接形成可执⾏程 序。但是静态链接的优点就是,在可执⾏程序中已经具备了所有执⾏程序所需要的任何东西,在 执⾏的时候运⾏速度快。
动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对独⽴部分,在程序运⾏时才将它们链接在⼀起形成⼀个完整的程序,⽽不是像静态链接⼀样把所有程序模块都链接成⼀个单独的可执⾏⽂件。
动态链接其实远⽐静态链接要常⽤得多。⽐如我们查看下 hello 这个可执⾏程序依赖的动态库,会发现它就⽤到了⼀个c动态链接库:
$ ldd hello
linux-vdso.so.1 => (0x00007fffeb1ab000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff776af5000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff776ec3000)
# ldd命令⽤于打印程序或者库⽂件所依赖的共享库列表。
在这⾥涉及到⼀个重要的概念: 库
- 我们的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
- 这块需要举例说明,纯理论太⽣涩。 ⽹吧/⽹咖的例⼦,见画板
⼀般我们的云服务器,C/C++的静态库并没有安装,可以采⽤如下⽅法安装
# Centos
yum install glibc-static libstdc++-static -y
#ubuntu
略
其他问题,课堂画板补充:
- 条件编译应⽤场景
- 为什么⾮得把语⾔变成汇编
- 编译器⾃举
3.5 gcc其他常⽤选项 - 了解即可
- -E 只激活预处理,这个不⽣成⽂件,你需要把它重定向到⼀个输出⽂件⾥⾯
- -S 编译到汇编语⾔不进⾏汇编和链接
- -c 编译到⽬标代码
- -o ⽂件输出到 ⽂件
- -static 此选项对⽣成的⽂件采⽤静态链接
- -g ⽣成调试信息。GNU 调试器可利⽤该信息。
- -shared 此选项将尽量使⽤动态库,所以⽣成⽂件⽐较⼩,但是需要系统由动态库.
- -O0
- -O1
- -O2
- -O3 编译器的优化选项的4个级别,-O0表⽰没有优化,-O1为缺省值,-O3优化级别最⾼
- -w 不⽣成任何警告信息。
- -Wall ⽣成所有警告信息
四、⾃动化构建-make/Makefile
4.1 背景
- 会不会写makefile,从⼀个侧⾯说明了⼀个⼈是否具备完成⼤型⼯程的能⼒
- ⼀个⼯程中的源⽂件不计数,其按类型、功能、模块分别放在若⼲个⽬录中,makefile定义了⼀ 系列的规则来指定,哪些⽂件需要先编译,哪些⽂件需要后编译,哪些⽂件需要重新编译,甚⾄于进⾏更复杂的功能操作
- makefile带来的好处就是⸺“⾃动化编译”,⼀旦写好,只需要⼀个make命令,整个⼯程完全⾃动编译,极⼤的提⾼了软件开发的效率。
- make是⼀个命令⼯具,是⼀个解释makefile中指令的命令⼯具,⼀般来说,⼤多数的IDE都有这 个命令,⽐如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可⻅,makefile 都成为了⼀种在⼯程⽅⾯的编译⽅法。
- make是⼀条命令,makefile是⼀个⽂件,两个搭配使⽤,完成项⽬⾃动化构建。
4.2 理解
依赖例⼦ - ⽉底要钱的例⼦
4.3 基本使用
【实例代码】
#include <stdio.h>
int main()
{
printf("hello Makefile!\n");
return 0;
}
【Makefile⽂件】
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
【依赖关系】
【依赖⽅法】
- gcc -o myproc myproc.c ,就是与之对应的依赖关系
【项⽬清理】
- ⼯程是需要被清理的
- 像clean这种,没有被第⼀个⽬标⽂件直接或间接关联,那么它后⾯所定义的命令将不会被⾃动 执⾏,不过,我们可以显⽰要make执⾏。即命令⸺“make clean”,以此来清除所有的⽬标 ⽂件,以便重编译。
- 但是⼀般我们这种clean的⽬标⽂件,我们将它设置为伪⽬标,⽤ .PHONY 修饰,伪⽬标的特性 是,总是被执⾏的。
- 可以将我们的 hello ⽬标⽂件声明成伪⽬标,测试⼀下。
【什么叫做总是被执⾏?】
$ stat XXX
File: ‘XXX’
Size: 987 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 1321125 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ whb) Gid: ( 1000/ whb)
Access: 2024-10-25 17:05:30.430619002 +0800
Modify: 2024-10-25 17:05:25.940595116 +0800
Change: 2024-10-25 17:05:25.940595116 +0800
⽂件 = 内容 + 属性
Modify: 内容变更,时间更新
Change:属性变更,时间更新
Access:常指的是⽂件最近⼀次被访问的时间。在Linux的早期版本中,每当⽂件被访问时,其atime
都会更新。但这种机制会导致⼤量的IO操作。具体更新原则,不做过多解释。
结论: .PHONY:让make忽略源⽂件和可执⾏⽬标⽂件的M时间对⽐
4.4 推导过程
myproc:myproc.o
gcc myproc.o -o myproc
myproc.o:myproc.s
gcc -c myproc.s -o myproc.o
myproc.s:myproc.i
gcc -S myproc.i -o myproc.s
myproc.i:myproc.c
gcc -E myproc.c -o myproc.i
.PHONY:clean
clean:
rm -f *.i *.s *.o myproc
编译
$ make
gcc -E myproc.c -o myproc.i
gcc -S myproc.i -o myproc.s
gcc -c myproc.s -o myproc.o
gcc myproc.o -o myproc
make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make命令。那么:
- make会在当前⽬录下找名字叫“Makefile”或“makefile”的⽂件。
- 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 myproc 这 个⽂件,并把这个⽂件作为最终的⽬标⽂件。
- 如果 myproc ⽂件不存在,或是 myproc 所依赖的后⾯的 myproc.o ⽂件的⽂件修改时间要 ⽐ myproc 这个⽂件新(可以⽤ touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成 myproc 这个⽂件。
- 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为 myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀ 个堆栈的过程)
- 当然,你的C⽂件和H⽂件是存在的啦,于是 make 会⽣成 myproc.o ⽂件,然后再⽤ myproc.o ⽂件声明 make 的终极任务,也就是执⾏⽂件 hello 了。
- 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个 ⽬标⽂件。
- 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并 报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
- make只管⽂件的依赖性,即,如果在我找了依赖关系之后,冒号后⾯的⽂件还是不在,那么对 不起,我就不⼯作啦。
4.5 适度扩展语法
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)