我们都知道代码形成可执行程序是需要进行预处理,编译,汇编,链接的。那么今天就来看看编译器吧。
一、gcc/g++编译器
gcc 和 g++编译器有什么区别呢?
gcc编译器是专门用来编译C语言的,而g++编译器既可以用来编译C也可以用来编译C++,主要用来编译C++。
既然都说到这里了,那么要想了解gcc、g++必然少不了围着预处理,编译,汇编,链接来进行说明。
1. 预处理
//filename为要编译的文件名
gcc -E filename 将预处理后的内容打印到显示屏上
gcc -E filename -o xxx 将预处理后的内容写到了xxx文件里,xxx文件自己指定
gcc -E filename -o xxx是在进行预处理阶段,在这个阶段中可以看到,它都做了以下的工作。
.
头文件展开
.
宏替换
.
去注释
.
条件编译
带大家理解一下,什么是头文件展开?
所谓的头文件展开其实就是把头文件中相关的内容拷贝到源文件里
,所以,预处理完毕,其实就可以不用头文件了。
条件编译本质是对代码进行裁剪
。
如何理解裁剪呢?
在预处理阶段,代码从原本的20行变成了800行,就是将头文件里面的内容拷贝到了源文件里,去掉注释就是在删除内容,条件编译就是根据条件可以修改代码。所以所谓的裁剪其实就是对代码进行增、删、改操作。
头文件都被放在了usr/include/目录下。
2. 编译
gcc -S filename -o xxx 对预处理阶段的文件,进行编译

可以看到,对比于预处理阶段的文件,预处理阶段的文件还是C语言 ,但是编译之后的文件我们就已经看不懂了。编译的目的就是为了生成汇编语言
。
3. 汇编
gcc -c filename -o xxx进行汇编
注意:此汇编非彼汇编。看到这里可能已经有人看懵了。编译生成的汇编是一种语言(汇编语言),而这里的汇编是生成可执行程序当中的一个阶段 。它是为了将汇编语言翻译成可重定位目标二进制文件
。那么,它既然已经是二进制文件了,也就是说它已经能够被计算机所识别,你说,这个文件是否能够被执行呢?

显然不行,这个时候可能就有细心的小伙伴发现了,程序能不能运行,不应该要看x权限吗,这个文件没有x权限,当然不能够被运行了 。那我们再来做一个实验。
可以看到,它是执行失败了的。那它为什么会执行失败呢?先看下一阶段。
4. 链接
gcc filename -o xxx生成可执行文件或库文件
可以看出来,可重定位二进制文件确实不是可执行程序,必须经过链接才能生成可执行程序 。那么,这是为什么呢 ?就由小编来为各位解惑吧。在写代码的时候,我们不可置否的使用了库函数,而库函数的实现在哪里呢?库函数又不是我们自己写的,是别人帮我们写好的,库函数的实现应该在库里面 。所以,我们在使用库函数时,相当于只是拿到了库函数的声明,它需要和库里面的函数进行链接才可以生成可执行程序文件(也就是说,可重定位二进制文件必须依赖于对应的库才可以)
。
虽然结论已经出来了,但是要怎么证明它确实是和库进行了链接呢?
ldd xxx查看可执行文件或共享库所依赖的动态链接库

看到这里,相信大家对链接已经有了一个初步的认识了,那么,什么是链接呢?
库函数的实现并不在我们自己写的代码里,那么它在编译的时候库函数的地址是未知的,库函数的实现在库里,它会有一个地址,链接的时候会把xxx.o文件和库进行合并,形成一个可执行文件。所谓的合并是在干什么呢 ?就是为了将库中函数的地址重新填写到.o文件里
。
既然,链接需要库,那也就是说,系统里必须提前给我们安装好了库,这样我们才能编译,链接?是的,没错。
函数声明
函数实现
如何理解库呢?首先为什么要有库 ?在开发当中,开发者会经常的使用到一些函数方法,这是一种很普遍的现象,但是,如果每个人都为了实现一个函数写一个方法,那么也太浪费时间了吧。所以,有人就把这些经常性使用的一些函数放在了库里,就是为了提高开发效率。
库的常见分类:静态库、动态库。

静态链接:与静态库链接。
动态链接:与动态库链接。
实验证明一下。
动态链接
静态链接
静态链接有可能会失败,这是因为系统中可能没有默认安装C/C++的静态库。使用大模型生成安装静态库的命令就可以了。
可以观察到,编译器默认采用动态链接的方式,形成可执行程序
。
举个简单的例子,来理解一下动静态库。
今年中考结束后,有许多学生都考上了自己梦寐以求的中学,其中不乏有许多尖子生,其中一名学生张三学习非常好,考上了中学,但是他有点自己的小心思,他想去一个附近有网吧的学校,当然了,他的父亲并不知道他的小心思,他不知道应该去哪所学校。于是就和他的父亲去找他的朋友李四,询问李四的意见。在询问过程中,不仅找到了一所非常不错的学校也达成了张三的心愿。这所学校管理非常严格,平时是出不去的,所以只有周末的时候才可以出去。张三为了出去玩,于是就给周末定了一个计划表,早上要完成语文,数学,英语作业,下午就要出去玩,然后洗衣服等,等到了玩的时间,张三立马就要出去玩,之后回到学校洗衣服。
张三就相当于是一个程序,他的父亲就是链接器,张三从李四这里得到链接信息(链接:写入方法的地址),这所中学就是内存,计划表就是具体的代码,网吧就是动态库(共享库)。
所以,什么是动态链接呢?动态链接就是程序还未加载到内存时,就已经和动态库建立了信息上的连接,执行到库函数的时候,跳转到动态库执行,然后继续执行自己的代码
。
后来,老师发现学生经常去网吧,就给派出所打电话,举报网吧无证经营,警察一看果然情况属实,就把这个网吧干掉了。这个时候想玩电脑就要花钱去买了,很浪费资源的,毕竟,上一次网吧才多少钱,这也导致许多同学都去不了网吧。
所以,动态链接(动态库)的优点是节省资源,缺点是一旦丢失,所有程序都无法运行
。
放假回家,你闷闷不乐,你父亲问你为什么,你说想玩电脑,你父亲看你学习成绩挺好的,索性就在学校附近的网吧里给你买了一台电脑。这就是静态链接。
所以,静态链接就是把你要的方法。拷贝到可执行程序里
。
静态链接的优点:不依赖任何库,自己独立就能运行
。
缺点:体积大,占据资源多(占据磁盘空间,内存空间),加载速度受影响
。
二、自动化构建make/Makefile
1. 基本概念
make是一个linux系统内置的命令
makefile/Makefile是一个需要自己建立的一个文件
。
make命令会在当前目录下寻找makefile文件,解释里面的内容。
2. make,makefile的操作
3. 理解makefile


举个简单的例子理解一下。
每到月底的时候,大学生经常会给自己的父亲打电话,如果你只是打电话,说我是你儿子,就把电话挂了。你爸能知道你要干什么吗?肯定不行,这就叫做表明依赖关系。你给你爸打电话说,没钱了,你爸就知道你要干什么了,一会钱就到你手机上了。这就叫做表明依赖方法。依赖关系+依赖方法才能达到你想要的目的。
看到这里,大家有没有疑问呢?为什么make的时候只执行了第一个目标呢 ?这是因为make,makefile默认只形成一个目标,就是从上往下遇到的第一个目标
。
那么什么是伪目标呢?
.PHONY:表示被修饰的目标是一个伪目标
。
伪目标有什么特点呢?
特点 :伪目标总是被执行的
。
要想理解什么是总是被执行的 ,就先来看看什么叫做总是不被执行。

可以看到,第一次没有编译形成可执行程序,make是成功的,第二次以后都是没有被执行。那么,这是为什么呢?我们给源代码添加一些内容,再看看结果呢?
可以看到,在将源代码内容改变之后,make是成功的。所以,make之所以会失败,是因为编译器发现没有必要再去对源代码进行编译,因为源代码已经是最新的了。如果重复的对最新的源代码进行编译,就会浪费CPU的资源。这样做的目的主要是为了提高编译的效率 。这叫做总是不被执行
。
那么,什么叫做总是被执行呢?来看一个例子。

我们给code加上.PHONY:code
,看一下运行结果。
这叫做总是被执行。但是有一个问题,make,makefile它怎么知道要不要重新编译呢?我们来看一个命令。

在前面的文章里,我们学过这个。任何一个文件,它都包含三种时间。
文件 = 文件内容 + 文件属性
。
access time文件最近被访问的时间
。
modify time文件内容最近被修改的时间
。
change time文件属性最近的修改时间
。
对内容做一些删除
可以看到,modify time发生了变化,那么为什么change time也会发生变化呢 ?首先,modify time也属于文件属性,其次文件的size也发生了变化,所以,change time发生了变化
。那么在删除内容的时候,肯定也访问了文件,为什么access time没有发生变化呢?这与系统的内核版本有关,老的内核版本是会发生变化的,新的内核版本可能得好多次才会发生变化 ,这是因为access time的参考价值不大
,所以才变成了这样。
我们的问题是,make,makefile它怎么知道要不要重新编译呢 ?答案是:通过对比源文件与可执行文件的modify time时间,源文件最新就重新编译
。
什么叫做总是被执行呢 ?忽略时间,不做对比,直接执行
。

刚开始,只有源文件,就编译,此时已经有了可执行文件,若下次还要编译,就对比源文件与可执行文件的modify time,源文件最新就重新编译。
三、版本控制器Git
1. git的核心功能
git的核心功能就是为了版本控制
。
举个简单的例子来理解什么是版本控制。
大学生每到期末就要写各种实验报告,张三率先完成了报告,拿去给老师检查,老师为张三指出了各种毛病,什么格式不对,字数不够各种问题,让张三继续去修改,张三改了几天又拿去给老师看,老师还是不满意,于是张三又继续去修改他的报告了,过了几天,又拿去给老师,还是有各种问题,这时候老师说,算了张三,你这报告越改越不行,把你的第一份报告拿来吧,这时候张三懵了,他根本就没有对以前的报告进行保存。张三每天都唉声叹气的,被舍友李四看到了,李四就比较聪明了,他把他的报告拿去给老师,老师依然指出了各种毛病,让李四去修改,但李四就对他的每一份报告进行了保存,每一次拿新的报告给老师看,几次之后,老师说,算了李四,你把你的第一份报告拿来吧。李四很高兴,他的作业终于完成了,因为他对他的报告都做了备份。
张三和李四就是程序员,报告就是代码,老师就相当于是产品经理。李四做备份的过程就相当于版本控制 。版本控制的目的就是为了应对各种变化。
2. 认识git
后来张三进入了一家公司工作,在他的目录下写代码,他为了做版本管理,在Linux上安装了git,在本地上建立了本地git仓库,后来,张三把自己写的代码 code1 都提交到了本地git仓库,好巧不巧,他的舍友李四也进入了这家公司工作,他们俩在同一个组里,张三开发功能一,李四开发功能二 code2,自己写自己的代码,那么他们两个要怎么协同开发呢?于是他们的组长在服务器上建立了一个git仓库,这个git仓库就叫做远端仓库。张三把自己的代码code1从本地仓库提交到远端仓库,李四也是一样,但是git有一个特点,就是自己本地的git仓库内容和远端仓库不一致就无法提交,对于李四而言也是如此。
规则1 :如果我们的本地仓库和远端仓库内容不一致,推送方就无法推送
。
比如张三提交了自己的代码code1,这时候李四去提交代码是无法推送的,因为李四的本地git仓库并没有张三的代码,这时候就要求李四必须和远端仓库进行同步才可以提交。下一次张三要提交代码,张三的本地git仓库和远端仓库又不一样了,就要求张三必须去同步。
总结 :多人开发的时候我们通过限制提交的方式,保证服务端,尽量都是最新的,如果不是最新的,允许client提交,其他client就必须同步的方式进行多人协同开发!每个人要提交,必须先和远端保持一致,增量式的提交。
细节1 :什么是仓库 ?仓库就是特定的目录
。
细节2 :git本身除了版本控制,也提供网络功能
。
细节3 :本地仓库和远端仓库没有本质区别。本地git服务和远端git服务没有本质区别
。
细节4 :git是一个去中心化的版本控制策略
。
什么是去中心化呢?
举个例子:推送方要推送自己的代码,前提是必须要和远端仓库保持一致,否则就必须要进行同步。如果有一天远端仓库不在了呢,那么你的数据依然存在于自己的本地仓库中,并不会受到远端仓库的影响,这就叫做去中心化。
3. git操作
//安装git
yum install -y git(centos)
apt install -y git(ubuntu)
在Github上创建项目

//克隆远端仓库
git clone pathname

什么是克隆 ?克隆就是将远端仓库里面的文件拷贝到本地仓库
。
但是我们会发现,克隆下来的仓库内容多了一个.git,这个.git是什么呢 ?.git就是本地仓库
。

这个就叫做当前工作区,我们写的代码就是在这里。
git add .将所有新增的内容提交到.git里
git commit -m将新增的内容提交到本地仓库
git push本地仓库与远端仓库进行同步

git add .
和 git commit -m
是将文件提交到本地仓库里,git push将本地仓库里的内容提交到远端仓库,同时需要输入用户名和密码。
在.git
里面有一个文件index
需要注意,它是干什么的呢 ?在本地仓库里还有一个暂存区,就是index 。git add .就是将文件提交到暂存区里的
。那么这个暂存区有什么用呢?它就是为了人们后悔用的。git commit -m才是将暂存区里的提交到本地仓库
。
那么,这个.gitignore这个文件是用来干什么的呢 ?git主要是为了做源文件,头文件,文档等的托管,它不需要临时文件 ,.gitignore就是为了过滤掉这些临时文件
。我们可以证明一下。


可以看到,并不是所有的文件都提交到了远端仓库。这些命令是将文件提交到仓库了吗 ?在当前工作区创建文件,添加内容,删除内容,修改内容,这些都是文件的修改记录,本质是当前工作区内文件的变化 。提交的本质是提交你的历史修改操作。
git log//查看提交历史
git pull//拉取远程更新
git status//查看工作区状态
四、调试器gdb/cgdb
1. gdb
gdb/cgdb是用来调试的工具 。就像vs也可以用来调试。
既然gdb/cgdb是用来调试的工具,那么这里调试为何会失败呢 ?vs编译有两个版本,Debug,Release。那么这两个有什么区别呢 ?Debug版本是给程序员使用的,它里面添加了调试信息,而Release是给用户,测试人员用的,不添加调试信息,是无法调试的。linux默认是以release版本发布的,没有调试信息
,所以会失败。
问题1 :你怎么知道linux是以release版本发布的?

问题2 :怎么才能以debug版本发布?
gcc filename -o xxx -g//以debug版本发布

通过对比,可以发现code1的大小比code大,这是为什么呢 ?因为code1里面添加了调试信息
。我们可以来证明一下。

接下来就可以进行调试了。

gdb会自动记录历史命令。用gdb调试,默认是看不到代码的,还需要手动去解决,体验很不好。所以接下来就看cgdb。
2. cgdb
调试的本质是在干什么呢 ?答案是:定位问题。
接下来,就来学习cgdb吧。
list/l + 行号------从指定行号查看代码
list/l 函数名
list/l 文件名:行号

.
b + 行号 / b + 文件名:函数名
用来打断点
.
info b
用来查看断点信息
可以看到,断点编号是线性递增的。
.
delete/d + 断点编号
删除断点
.
delete/d + breakpoints
删除所有断点
细节1 :gdb启动调试的时候只是开启了gdb,被调试的程序并没有运行起来
。
细节2 :r/run,表示的是在gdb的场景中,启动我们自己的程序
。
细节3 :在没有断点的情况下,r/run就是让我们的程序直接运行结束
。
细节4 :断点的本质功能是让我们的程序。运行到指定的行进行暂停
。
.
n/next
逐过程,不进入函数内部
可以看到,并没有进入Sum函数里面去。
.
s/step
单步执行,进入函数内部
在vs中,我们还可以在监视窗口查看变量值,地址。那在cgdb上是否也可以呢?
.
p + (&)变量名
可以看到,确实可以查看变量值及地址。但是它只会显示一次,这不符合我们的要求。所以有了接下来的一条命令。
.
display + 变量名
跟踪显示指定变量的值
.
undisplay + 编号
取消对指定编号的变量的跟踪显示
.
until + 行号
跳转到指定的行号
.
c/continue
从当前位置开始连续执行程序,直到运行结束或者运行到下一个断点处 。
.
finish
执行到当前函数返回,然后停止 。
.
bt/backtrance
查看堆栈调用信息
.
info local
查看当前栈帧的局部变量值
.
info i
查看当前正在debug的程序信息
.
disable + 断点编号
禁用断点
disable breakpoints
禁用所有断点
应用场景 :当你程序出现bug时,这时候你要去打断点进行调试,找到问题之后取消断点,修改问题,但是结果不对,这时候你就要重新打断点,有可能你就忘记了之前断点的位置。所以禁用断点的目的是为了保留调试痕迹 。
.
enable + 断点编号
启用断点
enable breakpoints
启用所有断点
3. 调试技巧
.
查找bug时可以采用二分查找,快速定位问题
。
.
watch 执行时监视一个表达式的值,如果监视的表达式在程序运行期间值发生变化,GDB会暂停程序的执行,并通知使用者
。
应用场景 :如果有一些变量不应该被修改,但是你怀疑它修改导致的问题,你可以watch它
。
.
set var 更改指定变量的值
。确定问题原因。
比如说:你怀疑是因为某个变量而导致程序出错,你可以在调试的时候直接更改它的值,进而确定是否是因为它的原因。
你求的是1到100的和,但是结果确实0。你就可以用set var来确定问题原因。
.
条件断点
添加条件断点
给已存在的断点添加条件