g++/gcc编译器与自动化构建make/Makefile
一、gcc/g++
gcc是专门编译C语言的编译器,g++是专门编译C/C++的编译器。g++/gcc的选项是相通共用的。
gcc编译选项:gcc [选项] 要编译的文件 [选项] [目标文件]
编译过程:
先简单说明一下计算机语言发展史:最早的计算机语言是通过二进制机器码进行编写(三体中叶文洁就用过二进制穿孔纸带),后因为该方法太难受科学家们给各个二进制指令提供助记符--汇编,之后又在汇编的基础上出现高级语言--C语言。
C/C++的编译过程分为四步:预处理、编译、汇编、链接。
预处理:
预处理分为:1.头文件展开 2.去注释 3.宏替换(#) 4.条件编译
gcc编译实例:gcc –E hello.c –o hello.i
选项“-E”,该选项的作用是让gcc在预处理结束后停止编译过程。(生成文件后缀推荐使用.i)
   选项“-o”是指目标文件,“.i”文件为已经过预处理的C原始程序。 下面是cat  text.i的一部分,头文件已经被展开:
    下面是cat  text.i的一部分,头文件已经被展开:
条件编译:
   条件编译通过#ifdef、#ifndef、#if等指令,控制哪些代码块会被保留(参与后续编译),哪些会被删除(不参与编译),常用于跨平台开发、调试开关等场景。例如一个软件分为收费版和学生社区版,在代码的具体维护上并不会分为收费和免费两个代码分别维护,这样成本会过高。这时候只需要通过宏的注释使得免费版本的部分功能代码在编译期间被隐藏,从而达到免费版本中收费功能无法使用。

编译:
代码的语法正确性检测在此阶段进行(不是有个编译错误的说法嘛),在检查无误后会将代码转化为汇编语言。
使用实例:gcc –S hello.i –o hello.s
   选项“-S”,该选项的作用是让gcc在编译到汇编语言阶段结束后停止编译过程。(生成文件后缀推荐使用.s)

汇编:
汇编过程是将汇编语言翻译成机器可以识别的二进制文件。
使用实例:gcc –c hello.s –o hello.o
   选项“-c”,该选项的作用是让gcc在编译到二进制文件阶段结束后停止编译过程。(生成文件后缀推荐使用.o)
链接:
   链接核心作用是将多个 “半成品” 的目标文件(.o)、库文件(静态库.a或动态库.so)组合起来,最终生成一个可直接运行的可执行文件。它解决了目标文件之间的 “依赖关系” 和 “地址不完整” 问题,让分散的代码片段成为一个有机整体。
使用实例:gcc  hello.o  –o  hello
小技巧:gcc预处理、编译、汇编这三个选项可以巧记为ESc,对应键盘上Esc键;文件名记为iso,与苹果系统有些像(当然,Linux下不看后缀)。
二进制文件运行问题:
   欸,既然机器能读懂二进制,那么可以直接运行二进制(.o)文件吗?
好吧,就算给了执行权限也编不过去,汇编后产生的二进制文件不能直接用来运行的原因如下:
1、目标文件是 “局部二进制”,缺少全局符号关联(没有链接库)
其实就是调用了其他文件或库中的函数 / 变量,但未确定其实际地址。如调用C标准库中的printf函数,但由于并链接与C语言标准库形成链接而导致未定义报错。
2、目标文件缺少程序运行的 “全局内存布局”
操作系统加载并运行程序时,需要明确程序入口点和内存段分布这两个全局信息。而目标二进制文件仅记录 “当前文件内的局部地址”(相对于自身的偏移量),没有全局的内存布局规划。
3、目标文件可能只是程序的 “一部分”
一个程序可能是由多个源代吗编译链接形成的,单单只是拥有其中一个是无法正常使用的。
gcc其它常用选项:

二、动态链接和静态链接的初步了解
假设张三是一个高中生,每次到饭店的时候张三都会按照脑海里去学校食堂的路线到食堂吃饭。但是有一天学校食堂发生火灾不能准备饭菜,并且学校不允许零食和外面饭菜的存在。为了解决这个问题校长特意在每个学生位置上都装了一口锅,这时到吃饭点学生们就不再去食堂而是就地做饭菜。
虽然有些扯,不过张三和其它学生按照记忆中的路线去学校食堂吃饭就可以看成动态链接;张三和其它学生分别在自己座位上做饭吃可以看成静态链接。
动态链接:
动态链接就是编译链接时,仅在可执行文件中记录 “依赖的库名称和接口”,不复制库代码;程序运行时,由系统的 “动态链接器” 加载外部的动态库,完成库代码与程序的关联。动态链接不能独立运行。
动态链接中,当程序进行到需要调用库(张三上完课要去吃饭)时会由“动态连接器”(脑子)告诉其目标库(食堂)的具体地址(去食堂的路线)。程序中其它需要调用该库的地方也会从具体的地址找到库并进行库函数跳转,再通过库函数返回继续进行下面的程序(张三吃完饭回到教室学习)。一个库函数可以被多个地方调用,就是一旦库没了/发生异常就会无法调用导致错误(食堂炸了大家都吃不到饭)。
静态链接:
静态链接就是编译链接时,将程序依赖的库代码完整复制到可执行文件中,最终生成的可执行文件不依赖外部库,可独立运行。
静态链接中,库已经被拷贝到代码中(学校给每人配了一口锅),这时候就不需要提供额外的库地址(有了锅自己做饭就不依赖食堂)。并且由于没有寻找库的操作(去食堂)静态链接的程序启动阶段通常大于动态链接,但是由于需要拷贝库,代码的体积相比动态链接方式就更大(每人一口锅很占地方)
动态链接与静态链接的对比:
| 对比维度 | 静态链接 | 动态链接 | 
|---|---|---|
| 库代码整合时机 | 编译链接阶段(复制到可执行文件) | 程序运行阶段(加载外部动态库) | 
| 可执行文件体积 | 大(包含完整库代码) | 小(仅记录依赖信息) | 
| 外部库依赖 | 无,可独立运行 | 有,依赖外部动态库存在 | 
| 库更新维护 | 需重新编译程序 | 直接替换动态库即可(接口不变时) | 
| 资源占用(多程序) | 高(各程序单独包含库代码) | 低(多程序共用同一动态库) | 
一般的项目中外面都是以动态库动态链接为主,并且gcc的默认形式就是动态链接。
在 Linux 系统中,动态库是默认安装的,而静态库通常需要手动安装。
三、自动化构建 make/Makefile
Makefile/make基本概念:
   在 Linux 下,当我们开发一个包含多个源文件的程序时,手动输入gcc a.c b.c c.c -o app这样的编译命令会越来越麻烦, 一旦文件增多、依赖关系复杂(比如 A 文件依赖 B 文件的编译结果),手动编译不仅效率低,还容易出错。这时候,make 工具和它的 “剧本”Makefile就成了自动化构建的核心,帮我们自动处理编译流程。
Makefile:本质是一个文本文件,里面存放的是文件编译方式。一旦写好Makefile,调用make就能实现工程文件的自动编译,能极大提高效率。
make:是一个命令行工具,它会读取 Makefile 里的规则,自动检查文件的依赖关系和修改时间,只重新编译 “有变化的部分”,最终生成目标文件(比如可执行程序)。
总的来说,Makefile是一个文件,make是一个命令,两者搭配使用完成项目自动化构建。



Makefile中的依赖关系和依赖方法:
在日常生活中经常能看到依赖关系跟依赖方法:比如你给老爸打电话说“我是你儿子。”,你老爸当然知道你是他儿子,如果你只说这一句话他就会感到莫名其妙。但如果你在后面补充说“月底了,打钱。”你老爸就会产生具体行动。
这个例子里,你和你老爸这之间的关系就是依赖关系,基于这一层关系你老爸才会把生活费打给你这就是依赖方法。
Makefile中的依赖关系跟依赖方法是绑定的两者缺一不可。(你不要钱你爸怎么会打钱给你,你不是他儿子他又怎会打钱给你)。
在上面的Makefile文件中,text.exe是目标文件,:后面的text.c是依赖文件。下一行Tab键后面的指令就是依赖方法。
   
make的递归解析:
   先来看看下面的指令:
   现在运行一下:
虽然我们Makeflie中指令的顺序是按照源文件编译的反顺序,但make指令依旧是能够正确生成相关中间文件。
这是因为Makefile的执行逻辑是依靠递归,在表现形式上与栈类似。在上面指令中,gcc 选项由于无法执行会先被“压入栈中”,后面的gcc -c、gcc -S选项也会被逐渐“压入栈中”。但是到了-E选项的时候指令可以被执行,这时候就会触发递归回调,也就是“出栈”,逐渐执行之前的指令。
Makefile语法:
Makefile 是用于自动化构建(如编译、链接程序)的脚本文件,其核心是定义目标、依赖和命令之间的关系。
一、基本格式
目标: 依赖1 依赖2 ...命令1命令2...- 目标:要生成的文件(如可执行文件、中间文件 )或动作。
- 依赖:生成目标所需要的文件或其他目标(若依赖更新,目标会被重新构建)。
- 命令:生成目标的具体操作(如编译命令 gcc -c),必须以 Tab 键开头(不能用空格,否则报错)。
二、变量
   用于简化重复内容(如编译器、编译选项),格式:变量名=值,引用时用 $(变量名)
1、自定义变量
CC = gcc  # 编译器
CFLAGS = -Wall -O2  # 编译选项(-Wall显示警告,-O2优化)
TARGET = main  # 目标可执行文件名2. 自动变量(预定义,简化命令)
- $@:当前目标的名称(如编译- a.o时,- $@代表- a.o)。
- $^:当前目标的所有依赖(以空格分隔)。
- $<:当前目标的第一个依赖(常用于模式规则)。
- $?:所有比目标新的依赖(仅更新需要重新处理的文件)。  
三、模式规则
   用 % 通配符定义 “一类文件” 的通用构建规则(如批量将 .c 编译为 .o)。
格式:
%.目标后缀: %.依赖后缀命令四、伪目标
   不对应实际文件,用于执行动作(如清理、安装),需用 .PHONY 声明(避免与同名文件冲突)。可以理解为隐藏指令,make不明确调用时不会调用(上面有演示)。
五、条件判断
根据变量值执行不同规则,格式:
ifeq (值1, 值2)    # 值1等于值2时执行命令
else ifneq (值1, 值2)  # 值1不等于值2时执行命令
else命令
endif六、函数
Makefile 提供内置函数处理字符串或文件,格式为 $(函数名 参数1,参数2,...)。
常见函数:
| 函数 | 作用 | 示例 | 
|---|---|---|
| wildcard | 查找匹配模式的文件 | SRCS = $(wildcard *.c)# 获取所有.c文件 | 
| patsubst | 替换字符串中的模式 | OBJS = $(patsubst %.c,%.o,$(SRCS))# 替换为.o | 
七、默认目标
Makefile 中第一个目标会被 make 命令默认执行(无需指定目标),通常将最终可执行文件作为第一个目标。
总是被编译问题:



上面可以看到,明明都是gcc编译代码,为什么就.PHONY 就可以一直编译老代码呢?那是因为, .PHONY让gcc或者其它命令忽略了文件MOD时间对比新旧。
而 .PHONY可以让make忽略源文件和可执行目标文件的M时间对比

可是除了Mod时间发生改变以外,Acc跟Cha时间也发生改变了啊,这又怎么解释。
文件中的ACM时间解释:
Linux 下文件的 ACM 时间分别对应 Access(访问时间)、Modify(修改时间)、Change(状态变更时间),三者记录文件不同维度的时间戳,核心区别在于触发场景和记录的信息不同。
1、Access Time(访问时间)
核心含义:记录文件内容被读取的最近时间。
触发场景:只要文件内容被访问(不修改内容),就会更新
   触发更新常见操作:使用 cat、less、more 等命令读取文件内容。执行可执行文件(本质是读取文件指令)。通过 cp 命令复制文件(读取源文件内容时更新源文件 Acces Time)。
   注意:部分 Linux 系统默认开启 noatime 挂载选项(如 /etc/fstab 中配置),会禁用 atime 更新,以减少磁盘 I/O 开销,此时需通过 ls -lu 确认实际是否更新。
2、Modify Time(修改时间)
核心含义:记录文件内容被修改的最近时间。
触发场景:仅当文件的实际内容发生变化(如新增、删除、修改字符)时,才会更新 。
   触发更新常见操作:使用 vim、echo 等命令编辑并保存文件(如 echo "test" >> file.txt)。通过 dd 命令修改文件数据。
关联关系:mtime 更新时,会同步触发 Change Time(ctime)的更新(因为文件内容变化会导致文件大小等状态改变)。
3、Change Time(状态变更时间)
核心含义:记录文件元数据(inode 信息)被修改的最近时间。
触发场景:文件的元数据(非内容)变化时更新
触发更新常见操作:
1.修改文件权限(如 chmod 755 file.txt)、所有者 / 所属组(如 chown user:group file.txt)。
2.修改文件大小(如编辑内容导致 Modify Time变化时,Change Time 同步更新)。
3.移动文件(mv,若不跨文件系统,仅修改文件名 / 路径,内容不变,此时仅更新 Change Time)。
4.新建文件(文件创建时,Change Time 与Modify Time 初始值相同)。
关键区别:Change Time 不关注文件内容是否被读取,仅关注文件的 “属性” 或 “状态” 是否变更
为什么改变Modify时间(或忽略Modify时间新旧变化)后可以再次执行旧gcc指令?原因如下:
为了节约系统资源make默认不允许已经编译过的文件再进行二次重复编译,具体的实现就是通过Modify时间戳的前后变化来判断程序是否进行改动,如果Modify前后时间戳不一致说明源代码有较大的可能性进行改变,此时允许编译。
例外再补充说明一下:
ACM三种时间也属于文件属性
因为每次访问文件都会刷新Access时间,而在具体的操作过程中读取文件是十分普遍的现象,如果磁盘每次读取都因此修改文件属性就会导致磁盘访问次数增加降低系统整体效率,为了解决这个问题系统通常设置访问文件到达一定次数后才会更新Access时间。
