我们在 Windows 中编写 C/C++ 程序时,常用的 VS2019 是一个集成开发环境,包含了很多工具包。而在 Linux 下开发,大部分的情况下都是使用一个个独立的工具。比如:编写代码用 vim,编译代码用 gcc,调试代码用 gdb。
一、编辑器 - vim
为什么选择使用 vim 呢?
因为 vim 是所有 Linux 环境下自带的。
vi/vim 的区别简单点来说,它们都是多模式编辑器。不同的是 vim 是 vi 的升级版本,它不仅兼容 vi 的所有指令,而且还有一些新的特性在里面。例如语法加亮,可视化操作不仅可以在终端运行,也可以运行于 x window、 mac os、windows。这里统一选择按照 vim 来进行讲解。
1、vim 的基本概念
- 正常/普通/命令模式(Normal mode)
- 插入模式(Insert mode)
- 底行模式(last line mode)
2、vim 的基本操作
进入 vim,在系统提示符号输入 vim 及文件名称后,就进入 vim 全屏幕编辑画面:
$ vim test.c
(1)正常模式切换至插入模式,有以下 3 种方式
- 按 i 进入插入模式后,从当前光标所在位置开始输入文字。
- 按 a 进入插入模式后,从当前光标所在位置的下一个位置开始输入文字。
- 按 o 进入插入模式后,插入新的一行,从行首开始输入文字。
(2)插入模式切换至正常模式
- 按 ESC 键。
(3)正常模式切换至末行模式
- 按下 shift + : ,其实就是输入 : 冒号。
(4)退出 vim 操作,在正常模式中输入 : 冒号进入底行模式,然后选择输入
- w(保存当前文件)
- wq(存盘并退出 vim)
- q!(不存盘且强制退出 vim)
3、vim 正常模式命令集
(1)移动光标
- vim 可以直接用键盘上的方向键来控制光标上下左右移动,但正规的 vim 是用小写英文字母 h、j、k、l 来分别控制光标向左、下、上、右移一格。
- 按 gg:进入到文本开始。
- 按 shift+g / G:移动到文章的最后。
- 按 shift+4 / $:移动到光标所在行的 “行尾”。
- 按 shift+6 / ^:移动到光标所在行的 “行首”。
- 按 w:光标跳到下个字的开头(以单词为单位)。
- 按 e:光标跳到下个字的字尾(以单词为单位)。
- 按 b:光标回到上个字的开头(以单词为单位)。
- 按 #l:光标移到该行的第 # 个位置,如:5l、56l。
- 按 # + shift+g / G:光标移动到第 # 行。
- 按 #j / k 光标向下 / 上移动 # 行。
- 按 ctrl+b :屏幕往 “后” 移动一页。
- 按 ctrl+f :屏幕往 “前” 移动一页。
- 按 ctrl+u :屏幕往 “后” 移动半页。
- 按 ctrl+d :屏幕往 “前” 移动半页。
(2)删除文字
- x:每按一次,删除光标所在位置的一个字符。(常用)
- #x:例如,6x 表示删除光标所在位置的 “后面(包含自己在内)” 的 6 个字符。
- shift+x / X:大写的 X,每按一次,删除光标所在位置的 “前面” 一个字符。
- #X:例如,20X 表示删除光标所在位置的 “前面” 20 个字符。
- dd:删除(剪切)光标所在行。(常用)
- #dd:从光标所在行开始删除 # 行。
(3)复制文字
- yw:将光标所在之处到字尾的字符复制到缓冲区中。
- #yw:复制 # 个字到缓冲区。
- yy:复制光标所在行到缓冲区。(常用)
- #yy:例如,6yy 表示拷贝从光标所在的行(包含自己在内) “往下数” 6 行文字。
- p:将缓冲区内的字符粘贴到光标所在位置。注意:所有与 “y” 有关的复制命令都必须与 “p” 配合才能完成复制与粘贴功能。(常用)
- #p:将缓冲区内的字符粘贴 # 份到光标所在位置。
- yy+p:复制粘贴。
- dd+p:剪切粘贴。
(4)替换操作
- r:替换光标所在处的字符(局部文本替换),支持 #r。(常用)
- R:进入替换模式,替换光标所到之处的字符,直到按下 ESC 键为止(整体文本替换)。
(5)字母大小写转换
- shift+~:先按下 shift 键,再按下波浪号 ~ 不要停,往后遇到的所有小写字母将被转成大写,所有大写字母将被转成小写。
(6)撤销上一次操作(常用)
u:如果您误执行一个命令,可以马上按下 u,回到上一个操作。按多次 “u” 可以执行多次恢复。
ctrl+r:撤销 u 操作,也就是撤销的恢复(反撤销)。
(7)更改操作
cw:更改光标所在处的字到字尾处。
c#w:例如,c3w 表示更改 3 个字。
(8)跳至指定的行
ctrl+g:列出光标所在行的行号。
# + shift+g / G:例如,15G 表示移动光标至文章的第 15 行行首。
4、vim 在末行模式中的命令集
(1)列出行号
- set nu: 输入 set nu 后,会在文件中的每一行前面列出行号。
(2)跳到文件中的某一行
- #:#号表示一个数字,在冒号后输入一个数字,再按回车键就会跳到该行了,如输入数字 15 后再回车,就会跳到文章的第 15 行。
(3)查找字符
- /关键字:先按 / 键,再输入您想寻找的字符,如果第一次找的关键字不是您想要的,可以一直按 n 会往后寻找到您要的关键字为止。(常用)
- ?关键字:先按 ? 键,再输入您想寻找的字符,如果第一次找的关键字不是您想要的,可以一直按 n 会往前寻找到您要的关键字为止。
(4)批量化替换字符
%s/printf/cout/g
(把文中所有 printf 替换成 cout,g --global 表示全局的意思)(常用)
(5)查看函数手册
!man [选项] [函数名]
(按 q 退出手册)。(常用)
(6)保存文件
(7)退出 vim
- q:按 q 就是退出,如果无法离开 vim,可以在 q 后跟一个 ! 强制退出 vim。
- wq:一般建议离开时,搭配 w 一起使用,这样在退出的时候还可以保存文件。(常用)
(8)多文件多屏操作
如果想把 test.c 文件中的 10 行代码复制 test1.c 文件中,该如何操作呢?
vs test1.c(在 vim 中打开 test1.c 文件,左右分屏)
再按 ctrl + ww 组合键可以切换文件(w 要按两下)。
(9)跑任何想跑的命令
格式:!命令
(! 表示底行执行 bash 命令),比如:
5、 vim 中批量添加和删除注释
方法一:块选择模式
批量添加注释:
- 进入 vim 编辑器,按 ctrl+v 进入块选择模式(visual block),然后移动光标选择要添加注释的行。
- 再按 shift+i / I 键(大写字母),进入 Insert 插入模式,输入你要插入的注释符(比如 //)。
- 最后按 ESC 键,你所选择的行就被注释上了。
批量删除注释:
- 同样按 ctrl+v 进入块选择模式,选中要删除的行首的注释符号,注意 // 要选中两个。
- 选好之后按 d 键即可删除注释,ESC 保存退出。
方法二:替换命令
在末行模式下,可以采用替换命令进行注释:
- 添加注释:起始行号, 结束行号 s/^/注释符/g(表示在 xx 到 xx 行加入注释符,^ 表示行首的意思),然后按下回车键,注释成功。
- 删除注释:起始行号, 结束行号 s/^注释符//g(表示取消 xx 到 xx 行行首的注释符),然后按下回车键,取消注释成功。
比如:
6、简单 vim 配置
(1)配置文件的位置
- 在目录 /etc/ 下面,有个名为 vimrc 的文件,这是系统中公共的 vim 配置文件,对所有用户都有效。
- 而在每个用户的主目录下,都可以自己建立私有的配置文件,命名为:.vimrc。例如,root 用户的 /root 目录下,通常已经存在一个 .vimrc 文件,如果不存在,则创建之。
- 切换用户成为自己执行 su ,进入自己的主工作目录,执行 cd ~。
- 打开自己目录下的 .vimrc 文件,执行 vim .vimrc。
(2)常用配置选项,用来测试
- 设置语法高亮:syntax on
- 显示行号:set nu
- 设置缩进的空格数为 4:set shiftwidth=4
(3)使用插件
要配置好看的 vim ,原生的配置可能功能不全,可以选择安装插件来完善配置,保证用户是你要配置的用户。
https://github.com/wsdjeg/vim-galore-zh_cn
二、编译器 - gcc/g++
1、gcc/g++ 命令 & 程序编译
C/C++ 程序要运行,一般要经历以下步骤:
预处理(进行宏替换)--> 编译(生成汇编)--> 汇编(生成机器可识别代码)--> 链接(生成可执行文件或库文件)
Linux 下通过 gcc 命令完成 C 程序编译的过程,通过 g++ 命令完成 C++ 程序编译的过程:
gcc 命令格式:
gcc [选项] 要编译的文件 [选项] [目标文件]
(g++ 与之类似)
(1)gcc 选项
- -E 只激活预处理,不生成文件,你需要把输出内容重定向到一个 .i 输出文件里面。
- -S 只进行预处理、编译阶段,并生成 .s 汇编文件,不进行汇编和链接。
- -c 只进行预处理、编译、汇编阶段,并生成 .o 目标文件,不进行链接。
- -o 指明要生成的文件,输出内容到一个输出文件中。
- -static 此选项对生成的文件采用静态链接。
- -g 生成调试信息。GNU 调试器可利用该信息。
- -shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统提供动态库。
- -O0、-O1、-O2、-O3 编译器的优化选项的 4 个级别,-O0 表示没有优化,-O1 为缺省值,-O3 优化级别最高。
- -w 不生成任何警告信息。
- -Wall 生成所有警告信息。
提示 :gcc 选项记忆:esc,iso
(2)预处理(进行宏替换)
预处理阶段会做的事:头文件展开、宏替换、条件编译、去掉注释等等。
预处理指令是以 # 号开头的代码行。
命令格式:gcc –E hello.c –o hello.i
- 选项 -E,该选项的作用是让 gcc 在预处理结束后停止编译过程。
- 选项 -o,是指目标文件,.i 文件为已经过预处理的 C 原始程序。
(3)编译(生成汇编)
编译阶段会做的事:语法检查(代码的规范性、是否有语法错误等),函数实例化,生成 .s 汇编文件。
命令格式:gcc –S hello.i –o hello.s
用户可以使用 -S 选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。
(4)汇编(生成机器可识别代码)
汇编阶段会做的事:把编译阶段生成的 .s 汇编文件转成 .o 目标文件(二进制机器码)。
命令格式:gcc –c hello.s –o hello.o
用户可使用选项 -c 即可看到汇编代码已转化为 .o 的二进制目标代码。
(5)连接(生成可执行文件或库文件)
在成功编译之后,就进入了链接阶段。
命令格式:gcc hello.o –o hello
2、函数库
在 C 程序中,并没有定义 printf 的函数实现,且在预编译中包含的 stdio.h 中也只有该函数的声明,而没有定义函数的实现,那么是在哪里实现 printf 函数的呢?
系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径 /usr/lib 下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数 printf 了,而这也就是链接的作用。
静态库(.a):指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。动态库(.so): 与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。
- 前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件:gcc hello.o –o hello
- gcc 默认生成的二进制程序,是动态链接的,这点可以通过 file 命令验证。
3、动态链接和静态链接
生成可执行程序的方式有两种:
(1)动态链接:链接动态库
优点:不需要把相关库中的代码拷贝到可执行程序中,编译效率高,程序运行起来后,需要用到哪个库,再把哪个库加载到内存中,边运行边加载。
缺点:万一有库丢失了,将直接导致程序无法正常运行。
(2)静态链接:链接静态库
优点:不依赖于任何的动态库,自己就可以独立运行。
缺点:占磁盘空间,占内存,把相关库中的代码完完全全拷贝到了可执行程序中。
Linux 下生成的可执行程序,默认是动态链接的,如何查看呢?
使用
ldd [filename]
命令可查看可执行文件的库依赖关系。使用
file [filename]
命令可以查看可执行文件的信息和类型。
想要生成的可执行程序是静态链接的,该如何做呢?
$ gcc test.c -o test_s -static
三、项目自动化构建工具 - make/Makefile
1、基本概念
(1)背景
对于一个多文件的项目,在 VS 集成开发环境中,可以自动帮我们维护好多文件,我们只需要一键就可以完成对所有文件的编译,生成可执行程序。
而在 Linux,项目的所有文件,都需要我们自己来维护,成本太高,所以要用到 make 和 Makefile 帮我们自动化维护。
(2)概念
- 会不会写 Makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。
- 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,Makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
- Makefile 带来的好处 —— “自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- make 是一个命令工具,是一个解释 Makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Delphi 的 make,Visual C++ 的 nmake,Linux 下 GNU 的 make。可见,Makefile 都成为了一种在工程方面的编译方法。
- make 是一条命令,Makefile 是一个文件(文件中保存的是目标文件和原始文件间的依赖关系和依赖方法),两个搭配使用,完成项目自动化构建。
2、基本使用
现在编写了一个 test.c 文件,需要编译文件生成可执行程序:
方式一:直接使用 gcc 命令
$ gcc test.c -o test
方式二:可以用 make 命令:想要使用 make 命令,需要创建一个 makefile 文件。
如何创建 makefile 文件呢?
(首要先了解依赖关系和依赖方法的相关知识)
⚪依赖关系和依赖方法
- 依赖关系表明我依赖于谁。
上述例子中的文件 test 依赖 test .otest.o 依赖 test.s test.s 依赖 test.i test.i 依赖 test.c
- 依赖方法指的是对应的那个方法如何生成我。
gcc test.* - option test.* 就是与之对应的依赖关系。
比如上述例子,单文件项目,只有 test.c 一个文件:
目标文件 test 依赖于原始文件 test.c,但仅仅只有依赖关系是不能生成目标文件的。还需要有依赖方法,而 gcc test.c -o test 就是与之对应的依赖方法,表明如何生成目标文件 test。
编写 makefile 文件:
test:test.c # 表明了一种依赖关系,目标文件 test 依赖于 test.c
gcc test.c -o test # 依赖方法,怎么用 test.c 生成目标文件 test(需要以tab键开头)
.PHONY:clean # .PHONY —— "定义"伪目标:clean总是可以被执行的
clean: # 依赖项为空
rm -rf test # 清理可执行程序
编写完 makefile 文件后,使用 make 命令:
解释:.PHONY 的作用:
一般不会把可执行程序 “定义” 成伪目标,因为每次编译都是有成本的,第一次编译好了,就不需要再编译了,除非文件有改动。一般把清理可执行程序 “定义” 成伪目标。
简化 makefile 文件:
test:test.c
gcc $^ -o $@ # $^: 可执行程序所依赖的文件列表 $@: 目标文件
.PHONY:clean
clean:
rm -rf test
多文件项目,有 test.h test.c main.c 三个文件:
编写 makefile 文件:
test:test.c main.c # 目标文件 test 依赖于 test.c 和 main.c
gcc $^ -o $@ # $^: 可执行程序所依赖的文件列表 $@: 目标文件
.PHONY:clean
clean:
rm -rf test
为什么没有把 .h 头文件加入进来呢?
编译代码时头文件会展开,把头文件中的代码拷贝到源文件中,所以找到头文件才是最重要的,找头文件通常有两种路径:当前路径、系统路径。
【总结】
以后当遇到的项目变复杂了,文件多了,不用直接写 gcc 命令了,而是用 make/makefile 自动化构建项目。
3、原理
make 的推导过程图:
- make 会在当前目录下找名字叫 Makefile 或 makefile 的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到 test 这个文件,并把这个文件作为最终的目标文件。
- 如果 test 文件不存在,或是 test 所依赖的后面的 test.o 文件的文件修改时间要比 test 这个文件新(可以用 touch 测试),那么就会执行后面所定义的命令来生成 test 这个文件。
- 如果 test 所依赖的 test.o 文件不存在,那么 make 会在当前文件中找目标为 test.o 文件的依赖性,如果找到则再根据那一个规则生成 test.o 文件。(有点像一个堆栈的过程)。
- 这就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
- 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出并报错,而对于所定义的命令的错误,或是编译不成功,make 根本不理。
clean 项目清理:
- 工程是需要被清理的。
- 比如 clean,如果没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要 make 执行。即命令 make clean,以此来清除所有的目标文件,以便重新编译。
- 一般会把 clean 设置为伪目标,用 .PHONY 修饰。(伪目标的特性是:总是被执行的)。
4、补充
- makefile 文件保存了编译器和链接器的参数选项,并且描述了所有源文件之间的关系。make 程序会读取 makefile 文件中的数据,然后根据规则调用编译器,汇编器,链接器产生最后的输出。
- Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
- 显式规则说明了,如何生成一个或多个目标文件。
- make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 makefile,比如源文件与目标文件之间的时间关系判断之类。
- 在 makefile 中可以定义变量,当 makefile 被执行时,其中的变量都会被扩展到相应的引用位置上,通常使用 $(var) 表示引用变量。
- 文件指示包含在一个 makefile 中引用另一个 makefile,类似 C 语言中的 include。
- 注释,makefile 中可以使用 # 在行首表示行注释。
四、小程序 —— 进度条
1、回车、换行和回车换行
- 回车:用 \r 表示。回到当前行的最开始,如果此时写入数据,会依次往后覆盖掉当前行的数据。
- 换行。
- 回车换行:光标移动到下一行的最开始。
注意:
- C 语言中的 \n 表示:回车并换行。
- 键盘上的 Enter 键表示:回车并换行。
2、行缓冲区概念
(1)这段代码在 Linux 中运行,会产生什么结果呢?
#include <stdio.h>
#include <unistd.h> //sleep()
int main()
{
printf("hello world!\n"); //有'\n'
sleep(3);
return 0;
}
运行结果:先打印出 hello world,然后休眠 5s,结束程序。
(2)这段代码在 Linux 中运行,会产生什么结果呢?
#include <stdio.h>
#include <unistd.h> //sleep()
int main()
{
printf("hello world"); //没有'\n'
sleep(5);
return 0;
}
运行结果:先休眠了 5s,当 5s 结束后,才打印出 hello world,结束程序。
当 sleep(5); 执行的时候,printf("hello world"); 已经执行完了,但却没有先打印字符串,这是为什么呢?
printf("hello world"); 已经执行完了,但并不代表字符串就得显示出来。
那在执行 sleep(5); 期间,字符串在哪里呢?
缓冲区(本质就是一段内存空间,可以暂存临时数据,在合适的时候刷新出去)。
3、补充
刷新是什么?
把数据真正的写入磁盘、文件、显示器、网络等设备或文件中。
刷新策略:
- 直接刷新,不缓冲。
- 缓冲区写满,再刷新(称为全缓冲)。
- 碰到 ‘\n’ 就刷新,称为行刷新。(注:行刷新一般对应的设备是显示器)
- 强制刷新。
任何一个 C 程序,启动的时候,都会默认打开三个流(文件):
- 标准输入 stdin、标准输出 stdout、错误 stderr(类型是 FILE* 文件指针类型)
- 如果想要让数据在显示器上显示出来,需要向输出流 stdout 中写入数据。
回到前面的问题,为什么在执行 sleep 的时候,没有显示字符串呢?
因为我们想要把字符串显示到显示器上,显示器默认是行刷新,遇到 ‘\n’ 才刷新,而我们前面写的代码中,并没有 ‘\n’,所以 printf 执行完了没有刷新。
为了在 printf 执行完的时候,让字符串立马显示出来,需要进行强制刷新,把字符串尽快的写入显示器中。
强制刷新需要用到一个函数:
#include <stdio.h>
int fflush(FILE *stream); //把当前缓冲区的数据写入到流中
因为是让字符串在显示器上显示,所以我们需要传文件指针 File* stdout,代码如下:
#include<stdio.h>
#include<unistd.h> //sleep()
int main()
{
printf("hello world"); //没有'\n',字符串写入到了缓冲区中,但不会被立即刷新出来
fflush(stdout); //强制刷新,把当前缓冲区中的数据写入到输出流文件中
sleep(5);
return 0;
}
运行结果:先打印出 hello world,然后休眠 5s,结束程序。
4、进度条代码
#include <stdio.h> //fflush
#include <string.h> //memset
#include <unistd.h> //usleep
#define NUM 102 //101个字符+'\0'
int main()
{
char bar[NUM];
memset(bar, 0, sizeof(bar)); //把进度条清零
//每次循环,让字符串内容多一个'#',这样进度条就跑起来了
const char* lable = "|/-\\"; //两个\\表示'\',共4个字符
int cnt = 0;
while (cnt <= 100)
{
printf("[%-101s][%d%%] %c\r", bar, cnt, lable[i%4]); //每次打印进度条不需要换行,覆盖掉当前行的内容就行
bar[cnt++] = '#';
fflush(stdout); //强制刷新,把用户缓冲区的数据刷新出来
usleep(30000); //为了能够看到进度条,休眠30000us
}
printf("\n");
return 0;
}
效果如下:
5、小程序 —— 倒计时
弄明白了回车的概念后,下面写一个倒计时的小程序。
/* countDown.c */
#include<stdio.h> //fflush
#include<unistd.h> //sleep()
int main()
{
int count = 9;
while (count >= 0)
{
printf("%-2d\r", count); //数据写入缓冲区中,\r表示回车,从当前行的最开始写入
fflush(stdout); //强制刷新,把用户缓冲区的数据刷新出来
count--;
sleep(1); //为了能够看到倒计时,休眠1s
}
return 0;
}
运行结果:
【扩展】
虽然这里 count 是整型,但实际上打印到显示器上,是一个个字符。比如 int count = 123456,占 4 字节,使用 printf 打印到显示器上,是 6 个字符,占 6 字节。
- printf 格式化输出,实际上就是把这个内存级的整型数据转换成显示器可以显示的字符型的数据。
- scanf 格式化输入,实际上就是把键盘敲下的一个个字符型的数据转换成了一个内存级的整型数据。
文件分为二进制文件和文本文件,二进位文件在内存中是什么样子,写到文件中也就是什么样子。而文本文件写入到设备(文件)中时,是需要做转换的,比如显示器设备(也是一种文件),显示器是给人看的,所以它一定不是二进制文件,而是文本文件,只要是文本文件,必须要将所要显示的数据转换成人所能识别的一个个的字符型数据。
所以键盘和显示器设备(文件),统称为字符设备,体现在输入时是字符,输出时是字符。