本篇博客整理了Linux(centOS版本)中基础开发工具的用途和用法,旨在透过开发工具的使用,帮助读者更好地理解可执行程序的编写、编译、运行等。
目录
一、软件包管理器 yum
1.软件的下载与安装
无论Windows或Linux,下载软件的过程都大致相同,这是因为软件都放在远端的服务器,要在一个客户端(pc端/移动端)上安装软件,都得通过网络去到远端的服务器上下载。
Windows下要安装一个软件,基本是在图形化界面上来完成的。例如在浏览器上搜索和进入某个官方网站下载相应的安装包,下载完成后点击安装包进行安装,这个过程要做的操作基本是在图形化界面上勾勾选选,总得来说较为简单。
而在 Linux 下安装软件,基本是以命令行输入的方式。Linux的CentOS版本中,安装软件可以通过以下三种方式:
- 源码安装:软件的开发者直接将软件的源代码给用户,然后用户自行对这份源代码进行编译、安装。源码安装的成本很高,对用户有较高的要求,一般在安装一些组件、动静态库等才考虑此方案。
- rpm安装:rpm类似于windows下的安装包,但下载好 rpm 包后安装还得通过指令rpm,并且可能涉及到非常复杂的依赖关系,需要用户在安装前做好准备工作(依赖关系的示例:安装软件A的时候,系统可能会提示在安装之前,必须先安装软件B和软件C)。在windows的日常使用中,我们接触的许多软件之所以可以一键安装,是因为软件厂家已经将所有必要的东西替用户打包好了,但在linux下通过rpm安装,下载一个软件就真的只有一个软件,而它还依赖其他哪些软件就需要用户自己来搞清楚了。
- yum安装:yum(Yellow dog Updater, Modified)是Linux下的一个简单的集成化安装方案,一个常用的软件包管理器,主要应用在Fedora,RedHat, CentOS等发行版上前人把一些常用的软件提前编译好,打包成一个一个软件包寄存在远端的服务器,然后交由本地的软件包管理器yum来安装,以此解决安装源的问题(也就是说,用户不必考虑去哪里下载、安装什么版本、依赖关系有什么)。yum类似于ios上的应用商店,可以很方便的获取到这个编译好的软件包,直接进行安装。
要安装一个软件,理想的状况应该是,软件的源代码已经编译成可执行程序,且这个可执行程序能够轻易被用户下载。所以这三种安装方式中,最常用也最好用的是yum安装。由于本系列博客的Linux环境是通过云服务器来搭建的,用来登录云服务器的软件XShell其实就是一个客户端,yum就是客户端上一个可以安装其他软件的软件应用商店。而关于yum的所有操作首先必须保证主机网络通常。
(可以输入”ping 网址“来检测是否有网)
2.Linux应用商店:yum
指令yum可以访问远端的软件包,下载安装软件。yum的相关操作需要在root权限下进行,且在软件下载和安装时必须连网。
语法:yum + (选项)
【补】选项:
- list:显示所有已经安装和可以安装的软件(可以结合管道 | 和指令 grep 来选择软件)
- install + 软件名:安装指定软件,安装时询问(yum install -y 软件名,安装时不询问)
- remove + 软件名:删除指定软件,删除时询问(yum remove -y 软件名,删除时不询问)
1
2
3
4
3.yum源
yum之所以知道去哪里下载软件包,是因为yum中内置了软件包的下载链接,在/etc/yum.repos.d/路径下,就可以看到当前设备的yum源,其中centOS-Base.repo是官方认可的yum源,里面就存放了软件包下载链接(注意:云服务器在安装时,就配置好了国内的镜像网站。
而虚拟机需要自己配置yum源)。
1
2
补、数据传输软件
lrzsz是一个数据传输软件,使Windows机器和远端的Linux机器可以通过XShell传输文件。
其中,指令sz可以将远端Linux机器上的文件拿到本地Windows电脑中,指令rz是将本地Windows中的文件上传到远端的Linux机器上。
· 将Linux的文件传输到Windows平台上:sz
语法:sz + 文件名/路径
· 将Windows的文件传到Linux系统上:rz
语法:rz
二、多模式编辑器 vim
vim是一款功能强大的文本编辑器,由于有多种编辑模式,所以也叫多模式编辑器。它的前身是vi,但它兼容vi的所有指令,并且在vi的基础上添加了一些新的特性,例如:语法加亮、可视化操作等(注:vim需先安装,输入“yum install -y vim”即可)。
1.三种模式
vim有常用的三种模式:命令模式、插入模式、底行模式。
- 命令模式:此模式下可以控制屏幕光标移动,字符、字或行的删除,移动复制某区段,进入插入模式或者底行模式。
- 插入模式:只有在插入模式下,才可以做文字的输入。按Esc键可回到命令模式。
- 底行模式:在此模式下进行文件保存或退出,也可以进行文件替换、找字符串、列出行号等操作。在命令模式下按下“shift + : ”即可进入底行模式。
【补】进入底行模式(按下“shift + : ”)输入“help vim-modes”可以查看vim的所有模式
2.基本操作
2.1-模式的切换
· 命令模式—>插入模式:
从命令模式切换到插入模式,可以输入:a、o、i,而这三种方式的区别在于,进入插入模式后光标所在的位置不同。
- 输入a:进入插入模式后,是从光标的下一个位置开始输入
- 输入o:进入插入模式后,自动插入新的一行,从行首开始输入
- 输入i:进入插入模式后,是从光标的位置开始输入(最常用)
1
2
· 插入模式—>命令模式:
按下Esc键即可退回到命令模式(另外,其他模式都是以这种方式退回到命令模式的)。
· 命令模式—>底行模式:
按下“shift + :”(其实就是输入“:”)即可从命令模式进入底行模式。
2.2-vim的进入和退出
· 进入vim来编辑一个文件:
输入“vim + 文件名”即可进入vim的全屏编辑画面(如果vim后面的文件不存在,也会进入到vim全屏幕编辑画面;编辑了文件内容并退出后,会在当前工作目录下生成一个同名的新文件,但直接退出不会生产一个新文件)。
· 退出vim编辑器:
在底行模式下,输入w可以保存当前文件,输入q可以退出vim,常见的组合输入方式有:
- q:保存并退出(但更推荐用wq)
- q!:不保存,强制退出
- wq:保存并退出
- wq!:保存并强制退出
3.命令模式指令集
3.1-移动光标
- gg:移动光标到首行行首
- shift + g / G:移动光标到尾行行首
- n + G:移动光标到第n行行首
- $(shift+4):移动到行末
-
^(shift+6):移动到行首
-
w:以单词为间距,向右向下移动光标(n + w 以n个单词为间距)
-
b:以单词为间距,向左向上移动光标(n + b 以n个单词为间距)
-
e :移动光标至单词的结尾
-
h、j、k、l:以字符为单位进行左、下、上、右移动
3.2-复制/粘贴
- yy:复制光标当前行的内容到缓冲区(n + yy 从光标当前行开始,复制n行)
- p:将缓冲区中的内容粘贴到光标的下一行(n + p 从光标下一行开始,粘贴n行)
3.3-剪切/删除
- dd:剪切光标当前行的内容(严格来说,dd完不p,就是删除;p了,才是剪切)
- x(小写):删除光标位置的字符(n + x 删除从光标位置开始的n个字符)
- X(大写):删除光标前一个位置的字符(n + X 删除光标所在位置的前n个字符)
3.4-替换
- ~:切换光标位置的字符的大小写
- r:替换光标位置的字符,敲击r,然后敲击要替换的字符(n + r 替换从光标位置开始的n个字符)
- R:替换光标所到之处的字符,按下Esc键停止替换(其实这是进入了替换模式)
3.5-撤销
- u:撤销上一次操作(可按多次 u 执行多次撤销)
- ctrl + r:对撤销的恢复
3.6-翻页
- ctrl + d:向后移动半页
- ctrl + u:向前移动半页
- ctrl + f :向后移动一页
- ctrl + b:向前移动一页
4.底行模式指令集
4.1-多文本操作
- vs + 文件名:多文件分屏操作
- ctrl + ww:切换操作的文件(光标在哪个文件的窗口,就可以在文件上操作)
4.2-显示行号
set nu
:在每一行前显示行号set nonu
:取消显示行号
4.3-跳转到第n行
n
:跳转到第n行
4.4-查找关键字
/ + 关键字
:先输入/,然后输入一个关键字,可以从光标位置向下查找这个关键字,找到后,光标会跳转到关键字所在行,关键字也会被标记出来(如果第一次找到的不是想要找的,可以一直按n
,会继续往后查找)? + 关键字
:先输入?,然后输入一个关键字,可以从从光标位置向上查找这个关键字,找到后,光标会跳转到关键字所在行,关键字也会被标记出来(如果第一次找到的不是想要找的,可以一直按n
,会继续往前查找)
4.5-执行指令
!
:在底行模式下执行命令
4.6-保存编辑内容和退出vim
w
:保存w!
:强制保存q
:保存并退出q!
:强制退出(没有编辑任何内容,需强制退出)wq
:保存并退出wq!
:强制保存并退出
5.块模式
· 多行注释:
- 按ESC键进入命令模式
- 按Ctrl + V进入VISUAL BLOCK模式
- 通过 h、j、k、l 进行左、下、上、右的调整需要注释多少行和行的宽度
- 按 Shift + i 或 s 进入插入模式
- 输入注释符号
- 再次按ESC键,此时就完成了多行注释
· 取消注释:
- 按ESC键进入命令模式
- 按Ctrl + V进入VISUAL BLOCK模式
- 通过 h、j、k、l 进行左、下、上、右的调整取消注释多少行和行的宽度
- 此时按 d 即可完成去注释
6.vim的配置
默认配置的vim编辑器,没有提示、没有缩进、没有行号、没有语法检查,在编写代码的时候很不方便,于是就需要用户自行配置。常见的配置项例如:
set nu //设置行号
syntax on //语法高亮
set showmode //底部显示当前处于什么模式
set cursorline //显示行号下划线
set shiftwidth=4 //设置缩进的空格数为4
……
考虑到新手配置vim相当繁琐,这里提供一个能够直接输入指令一键配置的方法——前人在 gitee 中已经上传了一份只支持CentOS7版本的自动配置方案,直接在普通用户下输入以下指令即可(还会提示输入root用户的密码):
curl -sLf https://gitee.com/HGtz2222/VimForCpp/raw/master/install.sh -o ./install.sh && bash ./install.sh
上文中vim编辑器的演示图,就是配置完成的模样了。
三、编译器 gcc/g++
1.编译和链接程序:gcc/g++
gcc/g++是GNU的C/C++编译器,可以将一个后缀为.c/.cpp的源文件编译成一个可执行程序。
在将源文件编译和链接生成可执行程序的过程中会经历以下四个步骤:
- 预处理:头文件展开、去注释、宏替换、条件编译
- 编译:将C/C++代码翻译成汇编语言
- 汇编:汇编代码转为二进制代码
- 链接:将二进制代码和函数库进行链接,生成可执行程序
而这也是gcc/g++编译程序的大体过程。gcc/g++可以直接将源文件生成为可执行程序(后缀为.exe或.out的文件),也可以选择性地将源文件生成为,在编译链接过程中任一阶段的文件(如预处理阶段的.i文件,编译阶段的.s文件,汇编阶段的.o文件,和最后链接阶段的.exe文件或.out文件)。
【ps】gcc/g++的使用注意事项
首次使用gcc/g++编译器需提前下载:“yum install -y gcc”——gcc,“yum install -y gcc-c++ libstdc++-devel”——g++。
另外需注意!gcc 不能编译 cpp,只能编译 c,而 g++ 既可以编译cpp,也可以编译c,这是因为C++语言本身兼容C语言。
语法: gcc/g++ +(选项)+ 源文件名
【补】选项:
- (不加):直接将源文件生成一个可执行程序(也就是默认生成经过预处理、编译、汇编、链接全阶段处理后的文件)
- -E:只进行预处理。这个过程其实不生成文件(.i),若要看到预处理的结果,则需将结果重定向到一个输出文件中,否则结果只能直接打印到屏幕上
- -S:进行预处理和编译,将源代码转化为汇编语言(.s)
- -c:将汇编代码转化为二进制代码(.o)
- -o:将各阶段处理的结果输出到一个输出文件(-o后须紧跟文件名)
- -static:对生成的可执行程序采取静态链接的方式。
- -shared:对生成的可执行程序尽量采取动态链接的方式,生成的可执行程序相对较小。
- -g:生成调试信息(GNU 调试器可利用该信息;若不携带-g则默认生成release版本)
- -w:不生成任何警告信息
- Wall:生成所有警告信息
- -O0/-O1/-O2/-O3:它们是编译器优化的四个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
2.程序编译的过程
上文已经提过,将一个源文件生成一个可执行程序需经历预处理、编译、汇编、链接的四个阶段。在这里,小编提供一份用vim编辑器写好的c代码,以便下文用gcc来演示这四个阶段。
2-1.预处理
预处理阶段主要包括去注释、宏替换、头文件展开、条件编译等,经过预处理后的源代码直观上一般会比预处理前简洁干净一些。
- 去注释:删除代码中的注释
- 宏替换:将代码中的宏依照宏的定义替换成相应内容
- 头文件展开:把头文件中的内容拷贝到当前的源文件中。头文件属于开发环境的一部分,在Windows中,以往使用的vs、devC++等其实都叫作集成开发环境,也就是集代码编写、编译于一体,在下载vs、devC++时同时也会下载一个开发包,这个开发包就是的头文件和库文件,可以直接在写代码时“#include”包含它们。同样的,在Linux环境下一般也有许多与开发环境有关的,如代码编辑器、代码编译器、头文件和库文件等,在写代码也可以直接包含它们(/usr/include/目录是Linux下gcc/g++头文件的默认搜索路径,其中有许多与开发相关的头文件)
- 条件编译:条件编译例如#if / #else / #endif,尽管初学者写代码的时候很少出现,但在实际开发中作用极大,可以帮助调试代码,还可以减低代码维护的成本。例如在下载vs2019时会有社区版、专业版的下载选项,一般社区版会比专业版的少一些功能,而少的这些功能就是通过条件编译裁剪掉的,也就是说,社区版和专业版用的其实是同一份代码。因为条件编译的存在,厂商要维护社区版和专业版只需维护一份代码;如果没有条件编译,社区版和专业版就需要各自用一份代码,维护起来就非常麻烦,很可能维护了社区版的代码,但专业版的代码没修改。
要对上文中提供的test.c文件中的代码进行预处理并观察结果,可以输入以下指令:
gcc -E test.c -o test.i
2-2.编译
在编译阶段,首先要检查预处理后的代码(.i文件)的规范性(代码中是否有语法错误),以确定代码实际要做的工作,具体的过程主要有语法分析、词法分析、语义分析、符号汇总等;然后将代码翻译成汇编代码(.s文件)。
要对上文中预处理过的test.i文件进行编译并观察结果,可以输入以下指令:
gcc -S test.i -o test.s
2-3.汇编
汇编阶段会将汇编代码(.s文件)转换成机器可以识别的二进制代码(.o文件),这个二进制代码文件或称为可重定位目标二进制文件,简称目标文件。
要对上文中编译过的test.s文件进行汇编并观察结果,可以输入以下指令:
gcc -c test.s -o test.o
2-4.链接
链接阶段会将目标文件(.o)和库文件/函数库进行链接,生成一个可执行程序,而这个可执行程序也是一个二进制文件。
一个程序中可能引用/调用外部的其他子程序,以及源代码中本就使用到了库中的或他人提供的函数接口,所以编译器会将所有程序代码与函数库链接起来,以生成正确的可执行程序。
要对上文中汇编过的test.o文件进行链接并观察结果,可以输入以下指令:
gcc test.o -o test
3.动/静态库
3.1-库与链接
上文提到,链接阶段会将目标文件和库文件进行链接生成一个可执行程序,而函数库又是什么?
在刚刚接触C语言的时候,大家一定写过函数printf()来打印内容,而要正常使用printf(),就一定得在代码开头写上“#include <stdio.h>”,这是为什么呢?
众所周知,printf()是一个库函数,虽然它是前人写的一个函数,但它也跟写其他函数一样,要有函数的声明,还要有函数的定义。学习初期的新手写代码,一般习惯将函数的声明和定义并列在一块儿,声明的同时紧接上定义。而在库中,函数的声明和定义往往不在一块儿(或称声明与定义分离),函数的声明往往放在头文件中,而函数的定义往往放在库文件(也是放源代码的源文件),也就是函数库中。
“stdio.h”是一个头文件,里面放有printf()的声明,所以包含它后,就可以正常使用printf()了。而printf()的具体实现,其实是放在一个库文件中的。由库文件统一提供函数的定义(也就是函数的具体实现),其实是将原本含有多个函数定义的多个源文件整合起来,统一提供一个文件。这样,库就精简许多,不必为用户提供太多的源文件,还可以将源文件隐藏起来。
于是,要将一个源文件生成一个可执行程序,就有了编译和链接的具体阶段:编译的预处理阶段,让头文件在代码中展开;然后在汇编阶段,由头文件的库函数声明,初步确定库函数定义在哪里;最终在链接阶段,由源代码得到的二进制目标文件中的库函数声明,与库文件(函数库)的库函数定义终于相遇。
【Tips】函数库(或称库文件,简称库)本质上是一个存有库函数的函数定义的二进制源文件,放在系统的特定目录下(绝大多数的函数库都放在/usr/lib、/lib目录下)。库通过头文件向外导出接口,通过头文件找到库函数实现的代码,就可以把这段库函数代码链接到可执行程序中去。
3.2-动/静态库
计算机行业的早期是没有函数库的,每名程序员写每个程序的时候都得自己从零开始写,但时间一长,大家就慢慢积累下来一些功能常用又好用的函数。那时的程序员们经常参加行业聚会以促进彼此交流,而在这个过程中各自的函数库经常被共享,所以就有后来,程序员中的大能们提出将各自的函数库统一起来,经过校准和整理,最终形成了一份标准化的函数库,例如 glibc 等,极大地提升了代码编写的效率。
行业早期,各自的函数库都是以源代码的方式来共享的,虽然这种方式是最彻底的,后来也由此形成了开源社区,但传播起来较为麻烦,无法通过商业化形式来发布函数库。例如商业公司想要将自己研发的有用的函数库提供给客户,以此盈利,但又不能直接将源代码全部交给客户,否则后续就难以盈利了。于是,就有人提出以库的形式来共享。
其中,出现较早的是静态链接库。 静态库其实是公司将自己的函数库源代码经过“只编译不链接”的方式形成后缀为.o的二进制目标文件,然后用AR工具将后缀为.o的目标文件归档成后缀为.a的归档文件(或称为静态链接库文件),然后,通过发布.a 库文件和.h 头文件来提供静态库给客户。客户拿到.a库文件和.h头文件后,在编译阶段由.h头文件得知库函数的原型,然后在链接阶段,由链接器在.a库文件中取出库函数的代码,将其与由源文件而来的目标文件进行链接,从而得到一个可执行程序。
静态库在链接可执行程序的时候,会将库函数的代码段全部链接进可执行程序,这样做虽然可以确保程序能正常执行,但是代码段会占用很多空间,显得繁琐臃肿,尤其在多个程序都要用到同一个库函数的时候,每个程序中都得有一份这个库函数的代码段。
动态链接库的出现比静态链接库稍晚,但效率更高,相当于静态链接库的改进升级版,现今的库一般也都是用的动态库。动态链接库本身并不将库函数的代码段悉数链接入可执行程序,而只是做了个标记,然后放任程序在内存中执行,当运行时环境发现需要调用一个库函数的时候,再将这个库加载到内存中,不论有多少个程序会调用这个库函数都会跳转到第一次加载的地方执行。
【Tips】库分为两类:静态库和动态库。
- 静态库:链接时,把库文件的代码全部加入到可执行程序中,因此生成的文件比较大(然而在运行时也就不再需要库文件了,比较浪费空间资源),一般以.a为后缀。
- 动态库:在链接时,并不把库文件的代码加入到可执行程序中,而是在程序运行时再按需加载库(这样做可以节省许多空间的开销),一般以.so为后缀。
在 Windows 下,动态库的后缀是.dll,静态库的后缀是.lib;而在 Linux 下,动态库的后缀是.so,静态库的后缀是.a。所有的库文件都遵守相同的命名规则,即:libname.后缀.xxx。(题外话:gcc编译器在编译链接时默认找的是C语言标准库,且默认是动态库,而不会去找C++标准库,这也是gcc不能去编译cpp的原因) 。
3.3-动/静态链接
既然库分为静态库和动态库,那么在链接阶段,库与目标文件的链接自然就分为静态链接和动态链接。
将目标文件与静态库进行链接,就叫做静态链接。进行静态链接后,库函数的代码会悉数拷贝到可执行程序中,该程序以后不再依赖静态库。
【补】
- 首次使用c的静态链接前需安装c静态库,相关指令——“yum install -y glibc-static”。
- 安装c++的静态库——“yum install -y libstdc++-static”。
- gcc默认优先使用动态库,即使没有动态库而只有静态库。
- -static参数的本质是改变优先级,进行的链接不一定全是动态链接或静态链接,二者其实可以同时出现,但如果加了-static参数,就只进行静态链接。
- 输入“file + 可执行程序名”可以查看这个可执行程序采用的是什么链接。
将目标文件与动态库进行链接,就叫做动态链接。动态库是被所有可执行程序共享的(所以一般也被叫做共享库),也就是说,动态库一个足矣。但进行动态链接后,动态库不能丢失,一旦丢失,将可能导致多个程序无法正常运行。
【补】
- Linux中编译链接可执行程序,优先采用动态链接。
- 输入“ldd + 可执行程序名”可以查看这个可执行程序所依赖的是什么库。
【Tips】动/静态链接的对比:
【Tips】对于同一份源代码,想直接区分它的可执行程序采用哪种链接,可以输入ll直接查看可执行程序的大小,大的一般是静态链接,小的一般是动态链接
库 优点 缺点 静态库 不依赖第三方库,程序的可移植性良好 浪费空间资源 动态库 节省空间资源(磁盘空间、内存空间、网络空间等);可执行程序体积小,加载速度快 依赖第三方库,程序的可移植性较差
四、调试器 gdb
1.Debug 和 release
对于同一份代码,有两种不同的执行模式:Debug模式和Release模式。
Debug模式:将源代码切换至调试版本,在经过编译链接后的可执行程序中加入调试信息,使源代码可以被追踪、调试。
- Debug模式下得到的可执行程序,体积一定比Release模式下得到的要大,且这个可执行程序不会进行任何优化(优化会使源代码和生成的指令之间关系会更复杂,使调试信息更复杂),以便于程序员进行调试。
- Debug模式下会固定生成两个文件:.exe或者.dll文件(可执行程序),以及一个.pdb文件(该文件记录了代码中断点等调试信息)。
Release模式:将源代码切换至发布版本,不对源代码进行调试,在编译时对程序的运行速度进行优化,使程序在代码体积和运行速度上都是最优的。
- Release模式下会固定生成一个文件:.exe或.dll文件(可执行程序)。
gcc编译器默认在Release模式下生成可执行程序。要在Debug模式下,编译得到可执行程序,需要在指令 gcc 中加入 -g 参数。
【补】
- 输入“readelf -S 可执行程序名”可以把对应的可执行程序以段的形式读取出来。
- 输入“readelf -S 可执行程序名 | grep debug”可以筛选出与Debug有关的段。
2.调试Debug模式的程序:gdb
调试是编写代码时不可或缺的一环,在实际项目中,编写代码可能只占到工作总量的三成,而调试代码可能会占到七成或七成以上。
在Linux下是通过调试器 gdb 来调试代码的(首次使用需提前下载——“yum install -y gdb
”)。
为了方便下文演示指令操作,小编在这里放上一段代码:
语法: gdb + 可执行程序名 +(进入调试器后的选项)
【补】选项:
· 进入调试器:
- gdb + 可执行程序名:进入调试器开始调试一个Debug模式下生成的可执行程序
· 调试:
- r / run:运行可执行程序,开始调试(会从开始运行到第一个断点处)
- n / next:逐过程调试
- s / step:逐语句调试
- until + 行号:跳转至指定行
- finish:执行完当前正在调用的函数后停下来(不能是主函数)
- c / continue:运行到下一个断点处
- set var + 变量名=x:修改变量的值为x
· 显示:
- l / list + n:显示从第n行开始的源代码,每次显示10行(若n未给出则默认从上次的位置往下显示)
- l / list + 函数名:显示该函数的源代码
- p / print + 变量名:打印变量的值
- p / print + &变量名:打印变量的地址
- p / print + 表达式:打印表达式的值,通过表达式可以修改变量的值
- display + 变量名:将变量加入常显示(每次调试停下都会显示它的值)
- display + &变量:将变量的地址加入常显示
- undisplay + 变量编号:取消指定编号变量的常显示
- bt:查看各级函数调用及参数
- i / info + locals:查看当前栈帧当中局部变量的值
· 打断点:
- b / break + n:在第n行设置断点
- b / break 函数名:在某函数体内第一行设置断点
- info + b / breakpoint:查看已打断点信息
- d / delete + 断点编号:删除指定编号的断点
- d / delete + breakpoint:删除全部断点
- disable + 断点编号:禁用指定编号的断点
- enable + 断点编号:启用指定编号的断点
1
2
3
· 退出调试器
- quit/q:退出gdb
五、项目自动化构建工具 make/Makefile
make是一条指令,功能是找到一个名为Makefile的文本文件(存放的是构建项目的指令,即依赖关系和依赖方法),并执行相应的指令。要理解这一点,首先得明白项目是如何构建的,换句话说,由多个源文件生成的一个可执行程序,是如何编译链接而来的。
1.项目的构建
项目(project)可以看作是多个源文件和多个头文件的集合,用于生成或维护某个或某些可执行程序。一般入门计算机的新手所写的项目,代码功能相对简单,因而项目的体量较小,有一个包含了库中头文件的源文件就足矣。而在实际业务中,源文件可能有很多个,头文件不仅会用到库所提供的,还可能需要自行编写。
以windows环境下的VS2019为例,要构建一个有多个.c源文件和.h头文件的项目,十分方便,直接Ctrl + F5或点击“生成解决方案”即可,用户不需要考虑先编译谁、后编译谁、同时还要链接哪些库,这是因为集成开发环境——VS2019会自动帮助用户维护项目结构。
而在Linux环境下,要构建一个有多个.c源文件和.h头文件的项目,一般需要先让每个.c源文件生成对应的.o目标文件,再将这些目标文件与库链接,生成一个可执行程序。
示例:
上面的示例图中,程序的功能较为简单,源文件也只有2个,每次输入gcc指令进行编译和链接还较为方便,但如果程序的功能很复杂,源文件的数量非常多,每次输入gcc指令就会非常繁琐。
而make和Makefile可以帮助我们解决这个头疼的问题,类似于VS2019中的“生成解决方案”,大大减少工作量和提升工作效率。
2.make/Makefile
【补】make和Makefile的重要性
- 会不会写Makefile,反映了一个人是否具备完成大型工程的能力
- 一个工程的源文件不计其数,按照其类型、功能、模块分别放在若干个目录当中,Makefile定义了一系列的规则来指定:哪些文件需要先编译,哪些文件需要后编译,甚至于进行更复杂的功能操作
- Makefile带来的好处就是“自动化编译”,一旦写好,只需一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率
- make是一个命令工具,是一个解释Makefile当中指令的命令工具,一般来说,大多数的IDE都有这个命令,例如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,Makefile都成为了一种在工程方面的编译方法
- make是一条命令,Makefile是一个文件,两个搭配使用,完成项目自动化构建
2.1-依赖关系和依赖方法
上文中提到,Makefile是一个文本文件,其中存放的是构建项目的指令,而make是一个解释Makefile当中指令的命令工具,两者搭配使用就可以完成项目自动化构建。Makefile文件中的这些指令规定了,哪些文件需要先编译,哪些文件需要后编译,甚至于进行更复杂的功能操作,而这些指令就是依赖关系和依赖方法。
那么,依赖关系和依赖方法究竟是什么呢?
- 依赖关系(Dependency Relationship):文件A的变更会影响到文件B,那么就称文件B依赖于文件A。
- 依赖方法(Dependent Method):如果文件B依赖于文件A,那么通过文件A得到文件B的方法,就是文件B依赖于文件A的依赖方法。
例如上文中,main.o文件是由main.c文件经过编译后得来的,所以main.c文件的改变会影响main.o,也就是说,main.o文件依赖于main.c文件;而main.o文件是main.c文件经过指令“gcc -c test.c -o test.o”可以得到的,也就是说,main.o依赖于main.c的依赖方法就是指令“gcc -c test.c -o test.o”。
同理在上文中,可执行程序test是由main.o和add.o链接后得来的,也就是说,test依赖于main.o和add.o;而test是main.o和add.o经过指令“gcc -o test main.o add.o”可以得到的,也就是说,test依赖于main.o和add.o的依赖方法是指令“gcc -o test main.o add.o”。
2.2-编写Makefile文件
Makefile文件的编写需借助vim编辑器来完成,文件的内容一般是源文件和可执行程序的依赖关系和依赖方法。
test:main.o add.o
gcc main.o add.o -o test
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
编辑完Makefile文件后,输入指令 make 即可生成可执行程序,以及在编译链接过程中产生的中间产物。
【补】依赖关系和依赖方法的简写方式:
- $@:表示依赖关系中的目标文件(替换冒号左侧的文件)
- $^:表示依赖关系中的依赖文件列表(替换冒号右侧的全部文件)
- $<:表示依赖关系中的第一个依赖文件(替换冒号右侧的第一个文件)
【ps】 gcc/g++指令携带 -c 参数时,若不指定输出文件名,则默认其为xxx.o,故此处简写依赖关系和依赖方法可以不指定输出文件名。
2.3-make的原理
- make能够当前工作目录下寻找名为“Makefile”(或“makefile”)的文件;
- 如果找到了,make就会继续在文件中找到第一个依赖关系作为最终的任务目的(例如上文中,它会找到test,并将test作为任务目的);
- 如果这第一个依赖关系的可执行程序名本不存在,或,它已经存在且它所依赖的源文件/目标文件的修改时间比它的修改时间更新/更晚,那么make会执行相应的依赖方法来生成/重新生成这个可执行程序(例如上文中,test本不存在,make就根据它的依赖关系,执行相应的依赖方法来生成它);
- 如果这个可执行程序所依赖的源文件/目标文件本不存在,那么make会在Makefile文件中寻找这个不存在的源文件的依赖关系,找到了就会执行源文件的依赖方法生成它(这个过程类似于堆栈,例如上文中,test所依赖的main.o本不存在,make就去找main.o的依赖关系并执行依赖方法来生成它);
- 如果这个可执行程序依赖的源文件/目标文件是存在的,make就会生成它,以完成最终的任务目的(例如上文中,make会根据main.c和add.c生成main.o和add.o,然后根据main.o和add.o生成test);
- make生成一个可执行程序后,如果源代码没有任何修改,就无法再次make(再生成一次)。
【Tips】make指令会在Makefile文件中一层又一层地识别的依赖关系和依赖方法,直到完成最终的任务目的,即生成出第一个可执行程序;在寻找的过程中,如果出现错误(例如找到文件最后,被依赖的文件也找不到),那么make就会直接退出并报错。
2.4-项目的清理
在每次重新生成可执行程序前,都应该养成习惯,将上一次生成可执行程序时生成的一系列文件进行清理。对比windows下VS2019的一键 "清理解决方案",Linux下的make指令,也可以一次性清理make生成的一系列文件,只需在Makefile文件中编写清理一系列文件的依赖关系和依赖方法,然后输入“make + 依赖关系”即可(单独的make指令,会默认将第一个依赖方法作为最终的任务目的,而“make + 依赖关系”,能指定make执行Makefile中的一个依赖关系)。
clean:
rm -rf main.o add.o test
1
2
2.5-.PHONY定义伪目标
上文中,Makefile 有多个依赖关系,在make的时候,默认将生成test作为最终的任务目的。而 “make clean”是根据指定的依赖关系执行依赖方法,做清理的工作。
为什么make的时候,生成test总作为最终的任务目的呢?make根据Makefile文件的内容,在自顶而下处理依赖关系的时候,为什么最终生成的是test,而不是执行clean的依赖方法?答案其实特别简单——就凭test的依赖关系是写在最前面的。make指令在形成文件的过程中,会在Makefile文件里自顶向下地扫描,默认只会将第一个依赖关系作为最终的任务目的,而不执行clean。
而这个最终的任务目的实现之后,即make一次后生成了可执行程序test,就无法再次make了。
换句话说,“生成test”不是总会被执行的。
生成test一次后就不再生成test,它“不总会被执行”可以理解,但clean的清理操作就只能执行一次吗?按理来说,无论文件修改时间的新旧/早晚,清理操作应该是能够重复执行且不限次数的。如果想要让clean始终被执行,可以将clean设为一个伪目标,具体的方法是用关键字“.PHONY”修饰clean,被“.PHONY”修饰的依赖关系所对应的方法,总是会被执行。一般情况下,对编译链接操作是不加.PHONY的,只对清理操作加.PHONY。
test:main.o add.o
gcc main.o add.o -o test
main.o:main.c
gcc -c main.c -o main.o
add.o:add.c
gcc -c add.c -o add.o
.PHONY:clean
clean:
rm -rf main.o add.o test
1
2
补、文件的时间
· 指令 - 查看文件时间的指令:stat
语法:stat + 文件名
Access
:文件最近一次被访问的时间(查看文件内容、修改文件内容,都属于访问文件)Modify
:最近一次修改文件内容的时间Change
:最近一次修改文件属性的时间
【ps】这三个时间是相互关联的,有的操作可能会同时更新多个时间,例如修改文件的内容会使这三个时间都更新(因为修改文件内容,首先要访问该文件,其次修改后,文件的大小会发生变化)。上文提到,如果可执行程序已经存在,且它所依赖的源文件/目标文件的修改时间比它的修改时间更新/更晚,那么make会执行相应的依赖方法来生成/重新生成这个可执行程序;其中,文件修改时间之间的比较,是先将修改时间转换成时间戳再比较的。
【ps】修改文件属性,Access不会更新。这是因为对文件的各种操作,都会导致Access时间改变,早期的Linux系统,确实会随着对文件的操作,时刻更新Access时间,这些时间信息都存储在计算机的硬盘上。但硬盘属于外部设备,进行读写操作会比较慢,当整个系统在被多个用户使用的时候,如果过高频率地更新一个文件的Access时间,就会有大量的Access更新行为,这些行为都会往硬盘中写数据,会导致整个系统的运行速度下降。所以,在现在的Linux中,对Access的更新策略进行了修改,大致是系统自主维护了一个计数器,等Modify和Access的更新达到一定次数的时候,才会更新Access,以此来提高系统的运行效率(但不同版本的更新策略会有差异)。但这样一来,有时就需要手段更新文件的时间
· 指令 - 手动更新文件时间:touch
语法:touch +(选项)+ 文件名
【补】选项:
- (不加):将文件的所有时间更至最新
- -a:将文件的Access(访问)时间更至最新
- -m:将文件的Modify(修改内容)时间更至最新
- -c:将文件的Change(修改属性)时间更至最新
六、版本控制器 git
1.git简介
Git 是一款分布式的具有网络功能的版本控制软件,可以快速高效地处理从小型项目到大型项目的所有内容。它依赖于软件分布式开发的基础,其中不止一个开发人员可以访问特定应用程序的源代码并可以修改其他开发人员可能看到的更改。它最初由 Linus Torvalds(Linux之父)于 2005 年为 Linux 内核开发而设计和开发,每个 Git 工作目录都是一个成熟的存储库,具有完整的历史记录和完整的版本跟踪功能,独立于网络访问或中央服务器,且允许一群人一起工作,所有人都使用相同的文件,此外,可以帮助团队应对多人编辑同一文件时容易出现的混乱。它允许用户在开发过程中跟踪文件的更改,并在需要时回滚到之前的版本。这样可以在团队协作开发时避免冲突,并保证项目的完整性。
Linus在维护Linux时,需要做大量的版本维护工作,为了提高效率,使用管理版本的工具迫在眉睫,前期工作得到了一家运营版本管理工具的公司的支持,但维护Linux社区的工程师试图破解管理工具的行为,使这家公司撤销了工具的使用权。于是,Linus只好自己写了第一版开源git,并在Linux社区不断发展。如今我们在国内的gitee以及国外的github,都是基于git发型的商业化版本。
2.连接gitee
在Windows上使用git,可以查看此教程:gitee(码云)的注册和代码提交【手把手】。
要在Linux上使用git,完成以下步骤后即可:
step1 - 在Linux上检测是否安装git软件
- 在root用户下安装git:yum install -y git
- 查看已安装的git版本:git --version
step2 - 在gitee上注册账号并新建一个仓库
1
2
3
step3 - 把远端仓库拉到本地仓库
1
2 输入git clone + 刚刚复制的HTTPS链接,然后输入gitee的账号和密码
3 输入完之后,gitee创建的仓库会被克隆到本地
3.git三板斧
- 将文件添加到创建的本地仓库中:git add 文件名/ . (一个点,git会自动将在本地仓库中没有的文件全部放进远端仓库中;这个命令需在git本地仓库的目录下执行)
1
2
- 提交日志:git commit -m 日志信息(日志一定不要乱写,一般写对代码修改了什么;第一次使用可能需要输入个人邮箱和gitee账号)
- 同步本地仓库和gitee的远端仓库:git push(后续需要输入gitee账号和密码)
1
2 push成功后,就可以在gitee上找到添加的文件了
4.其他操作
- 查看日志:git log
- 上传时过滤文件
在本地仓库中有一个.gitignore的隐藏文件,通过vim编辑器在这个文件中加入文件后缀(例如*.i、*.o、*.exe等),可以指定带有这个后缀的文件,在输入“git add . ”上传文件时被过滤掉(不上传)。此外,一般上传的都是源文件,很少传.o、.exe文件(小编这么传是为了演示,请大家别学)。
1
2
- 移动/重命名文件:git mv
- 删除文件:git rm
- 将远端仓库的内容更新/同步到本地仓库:git pull
补、进度条小程序
1.缓冲区的概念
严格来说,回车换行是两个独立的操作,回车是将光标移动到当前行的行首(默认是最左侧),换行是将光标水平方向保持不变,沿竖直方向向下平移一行。C语言中,转义字符 \r 可以完成回车,而转义字符 \n 可以完成换行。
- \r: 回车,使光标回到本行行首
- \n: 换行,使光标下移一格
类比Windows系统,键盘上的一个ENTER按键就可以同时完成回车和换行,按下ENTER键之后,光标一般会默认去到下一行最开始的位置,效果等价于C语言的 \n + \r 。
对于以下这两份代码,内容大致相同,只是其中一份的printf()函数中带有转义字符 \n ,另一份则没有转义字符 \n ,但两者运行的结果却有很大区别。
1
2
(小编还没学会做动图TAT,请见谅)
可执行程序 test_1 运行后,先在屏幕上打印出“Hello Linux!”,再休眠了2秒才出现bash命令行。而可执行程序 test_2 运行后,先是休眠了2秒,才在屏幕上打印出“Hello Linux!”,并且由于没有换行符 \n,bash命令行最终紧跟在打印结果“Hello Linux!”之后。
任何一个C程序,都是严格按照代码语句的顺序去执行的,不论是 test_1.c 还是 test_2.c,它们生成可执行程序都是先执行printf(),再执行sleep()的。可执行程序 test_2 运行后,在休眠的2秒中,printf()要打印的“Hello Linux!”并没有出现,那“Hello Linux!”去了哪里呢?而“Hello Linux!”在休眠2秒后还是打印在了屏幕上,这说明“Hello Linux!”并没有丢失,实际上,“Hello Linux!”被保存在了行缓冲区中。
缓冲区是C语言维护的一段内存,缓冲区中的内容默认只有在程序结束时才会将刷新出来。显示器文件对应的是行刷新,也就是说,只有缓冲区当中遇到换行符 \n 或是缓冲区被写满,才会将缓冲区中的内容打印出来,所以 test_1.c 是先打印再休眠,而 test_2.c 则是先休眠再打印。
2.刷新缓冲区
换行符 \n 可以刷新缓冲区,将数据及时打印出来。那如果不能使用换行符 \n ,又要怎么将缓冲区中的数据及时打印出来呢?
这里可以用到库提供的 fflush() 函数,该函数可以刷新缓冲区,及时将缓冲区中的数据刷新到显示器文件中。
将fflush() 函数运用在上文的 test_2.c 中,就可以及时将“Hello Linux!”打印出来了。
1
2
【Tips】刷新缓冲区的方法:
- 程序结束时会自动刷新
- \n 刷新。
- fflush(stdout) 手动刷新
3.简单的倒计时
可以利用缓冲区的特点,实现一个简单的十秒倒计时。
#include <stdio.h>
#include <unistd.h>
int main()
{
int cnt = 10;
while(cnt >= 0)
{
printf("%-2d\r",cnt);
fflush(stdout);
sleep(1);
cnt--;
}
printf("\n");
return 0;
}
4.简单的进度条
- processBar.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define NUM 102
#define TOP 100
#define STYLE '='
#define BODY '>'
const char* lable = "|/-\\";//进度加载提示
//着色:
/* #define NONE "\033[m"
#define RED "\033[0;32;31M"
#define GREEN "\033[0;32;32m"
#define LIGHT_BLUE "\033[1;34m"
#define LIGHT_PURPLE "\033[1;35m" */
void processbar()
{
char bar[NUM];
memset(bar, '\0', sizeof(bar));
int len = strlen(lable);
int cnt = 0;
while(cnt <= TOP)
{
printf("[%-100s][%d%%][%c]\r", bar, cnt, lable[cnt%len]);
fflush(stdout);
bar[cnt++] = STYLE;
if(cnt < 100)
{
bar[cnt] = BODY;
}
usleep(100000);
//以微秒为单位进行休眠,想让进度条10秒跑完
//因为一共会循环101次,所以每次循环大概就是休眠0.1秒(/100毫秒/10000微秒)
}
printf("\n");
}
int main()
{
processbar();
return 0;
}
进度条向右走动的原理就是:当次比上次多打印一点内容。
- 由此可以定义一个字符数组bar,通过循环每次往字符数组里面追加字符,然后将这个字符数组逐个元素打印。由于每次循环都会往数组里追加字符,就会导致下一次打印出来的内容比这一次的多,视觉上就像是进度条在往右移动。因为进度条始终是在同一行往右走的,所以每打印完一次要用 \r,让光标回到当前行的最开始位置,使得下一次打印产生覆盖的效果。
- 因为进度条是从0~100%,中间有101个跨度,因此循环的次数就是101次。整个循环会执行101次打印动作和101次字符追加动作,所以总共会追加101个字符,并且加上末尾的\0,一共就是102个字符。
- 最初将数组中的内容全部初始化为\0,这样,第一次打印的就是一个空串什么也没有(对标0%)。
- 打印完后进行追加,在数组下标为cnt的位置追加了一个=,下标为cnt+1的位置追加一个>,于是第二次打印出来的就是=>(对标1%)。
- 当到进度到达100%的时候,我们希望打印出来的进度条右边没有>,因为100%对应的是最后一次打印,也就是当cnt == 100的时候,打印出100个=即可。这意味着,当执行这次打印时,数组下标为99的位置存储的是一个=,且下标为100的位置是\0。所以,当cnt == 99的时候,字符串追加的时候将其置成=,但还需要加一个判断条件:当cnt < 100的时候才能将bar[cnt]设置成>,否则不能修改bar[cnt]。