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