前言
接着上一篇博客写完这些基础开发工具,最后用vim写一个进度条代码。尽快过渡到第一座大山---进程。
一、gcc/g++
基础用法和扩展知识
gcc/g++是一款编译器, 用来编译C/C++语言的,先看看怎么使用,比如我写了一个打印hello world。
格式 gcc [选项] 要编译的文件 [选项] [目标文件]
默认编译成a.out 可以带-o,后面提

这个图其实有很多信息,一方面体现出了隐藏目录.的作用,另一方面我们可以猜测指令也是可执行程序!事实上ls、pwd等命令就是可执行程序,被放在了规定的目录下面,不带路径执行时默认先去那个目录里面找!所以直接输入a.out不行,因为它不在默认的目录中,哪个目录呢? /usr/bin/ 换句话说,如果我将我的a.out扔在/usr/bin/ 下面,那我的a.out也可以成为命令!

回到gcc/g++上,之前提到过Linux中文件不区分后缀,不代表gcc/g++不需要区分!gcc只能编译C语言,.c文件,g++既可以编译C也可以编译C++,另外,.cpp .cxx, .cc的后缀均是C++。
生成可执行文件的过程
编译生成的可执行程序不是一步就完成的,具体分成了四步。
- 预处理
- 编译
- 汇编
- 连接
1.预处理
预处理阶段主要进行四个操作
a.宏替换 b.去掉注释 c.条件编译 d.展开头文件
上文讲了gcc是可以带选项的!gcc -E main.c -o main.i -E的作用就是预处理后停止, -o目标文件的名字, .i为规定的后缀,也就是预处理过后的C语言程序。我们来用个代码实验一下

条件编译也可以在gcc中带选项,-D宏名字或者-D宏名字=数字或者-D宏名字=字符串,不可以重复定义 
看一下生成的main.i文件:
和上文说的一致,另外还有一个小细节,它的头文件写的是/usr/include/stdio.h 但是我们写的时候没有带路径啊,这说明不带路径时系统默认会去/usr/include/下面去找。
2.编译
初学的时候遇到最多的情况应该就是编译错误,这个过程干什么了呢?
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。vs中可以查看汇编代码
用户可以使用"-S"选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码
gcc -S main.i -o main.o
3.汇编
汇编阶段是把编译阶段生成的".s"文件转成目标文件
读者在此可使用选项"-c"就可看到汇编代码已转化为".o"的二进制目标代码了
gcc --c main.s --o main
这里额外提一点知识,反汇编是什么? ? ? 作用是什么? ? ? 这四个过程中也没有反汇编的事啊,反汇编是为了方便人们调试的,将二进制转化成人类可读的汇编代码,反汇编看的也更加底层,可以看到CPU,寄存器、栈帧的一些细节,核心方便人们去改错。
4.连接
生成可执行文件,连接干了什么呢? 上文提到stdio.h中只包含函数的定义,并没有实现,所以通俗的讲连接就是去找这个函数的实现了,所以一定要有系统默认查找的路径!
系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到
系统默认的搜索路径"/usr/lib"下进行查找,也就是链接到 libc.so.6 库函数中去,这就是链接的作用,这里称之为函数库。
函数库一般分为静态库和动态库两种。
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为".a",连接的过程也称为静态连接
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为".so",如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库,可以使用静态库。可以带static选项让他静态连接,使用静态库。
我们可以用ldd或者readelf等命令查看可执行程序用的动态库。

这张图可以明显看出来静态连接编译出来的文本更大

当然我们可以自己制作动静态库,这个会在后面提到。
选项
-E 只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S 编译到汇编语言不进行汇编和链接
-c 编译到目标代码
-o 文件输出到 文件
-static 此选项对生成的文件采用静态链接
-g 生成调试信息。GNU 调试器可利用该信息。
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.
-O0
-O1
-O2
-O3 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高
-w 不生成任何警告信息。
-Wall 生成所有警告信息。
洛谷中可以看到O2优化,可能会进行一些常量折叠,寄存器优化等操作。
二、gdb
个人感觉,gdb用着很不舒服,vscode的调试也一般,真想调试舒服一点,就去vs,vs的调试还是很舒服的,gdb看看就行。
首先我们要知道:
程序的发布方式有两种,debug模式和release模式
Linux gcc/g++出来的二进制程序,默认是release模式
要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项
如何开始和退出呢?
gdb binFile 退出: ctrl + d 或 quit

list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
list/l 函数名:列出某个函数的源代码。
r或run:运行程序。
n 或 next:单条执行。
s或step:进入函数调用
break(b) 行号:在某一行设置断点
break 函数名:在某个函数开头设置断点
info break :查看断点信息。
finish:执行到当前函数返回,然后挺下来等待命令
print§:打印表达式的值,通过表达式可以修改变量的值或者调用函数
p 变量:打印变量值。
set var:修改变量的值
continue(或c):从当前位置开始连续而非单步执行程序
run(或r):从开始连续而非单步执行程序
delete breakpoints:删除所有断点
delete breakpoints n:删除序号为n的断点
disable breakpoints:禁用断点
enable breakpoints:启用断点
info(或i) breakpoints:参看当前设置了哪些断点
display 变量名:跟踪查看一个变量,每次停下来都显示它的值
undisplay:取消对先前设置的那些变量的跟踪
until X行号:跳至X行
breaktrace(或bt):查看各级函数调用及参数
info(i) locals:查看当前栈帧局部变量的值
quit:退出gdb
调试的思路差不多,整体都是打断点,进调试,然后根据逐语句或者逐过程调试

三、Makefile
这个是一个很有用的工具。
会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作
makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建
用法
makefile如何编写?? 核心就是依赖关系 + 依赖方法。文件名叫makefile或者Makefile都可以,先写一个编译cpp的,写法:

第一行叫做依赖关系,main这个可执行文件依赖main.cpp,同时,main也是你的命令,比如make main,就可以执行下面那句话,那你上面说的make指令是什么意思?在makefile中,第一个依赖关系,可以只用make执行依赖方法! 你的依赖方法可以有多行,但是必须先空两个格(按一下Table) ,再写依赖方法,另外:依赖方法可以为空! 依赖关系中可以只有前面,冒号后面可以不跟任何东西,因为不是所有命令都需要依赖的,比如后面说的清理!
那上面刚讲完生成可执行文件的过程,是不是可以使用makefile一步一步的编译出来呢?当然可以!核心还是控制好依赖关系和依赖方法。
Makefile
main:main.o
g++ main.o -o main
main.o:main.s
g++ -c main.s -o main.o
main.s:main.i
g++ -S main.i -o main.s
main.i:main.cpp
g++ -E main.cpp -o main.i
原理
任意几句调换顺序可以吗?? 也可以,这就涉及到了原理,比如输入make指令。
- make会在当前目录下找名字叫"Makefile"或"makefile"的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到"main"这个文件,并把这个文件作为最终的目标文件。
- 如果main文件不存在,或是hellomain依赖的后面的hello.o文件的文件修改时间要比main这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成hello这个文件。
- 如果main所依赖的main.o文件不存在,那么make会在当前文件中找目标为main.o文件的依赖性,如果找到则再根据那一个规则生成hello.o文件。(这有点像一个堆栈的过程)
- 当然,你的C文件和H文件是存在的啦,于是make会生成 main.o 文件,然后再用 main.o 文件声明make的终极任务,也就是执行文件hello了。
- 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
- 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。
- make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
所以,顺序不重要,能找到对应的依赖关系很重要。
make clean和伪目标
既然有项目生成,就有项目清理,清理不需要依赖关系,这么写就可以。
makefile
clean:
rm -rf main.o main.i main.s main
这里又引入一个新话题,伪目标,使用make的时候可能会看到这么一句话

已经被更新了,make指令失败了。原理和原因我们后面马上提,但是我不想让这个命令失败,我想让对应的依赖方法总是被执行,怎么办?伪目标
写法:
这样就能保证make clean总是被执行的了!可以尝试在编译中添加,这样保证每次编译都能成功

补充知识
1.一次依赖关系中可能依赖多个文件,比如后面我们制作动态库的时候,libmymath.so:add.o sub.o mul.o 那我就得把add.o xxxx一大串都写上??
makefile也想到了这一点,所以有@ 和 ^ 这两个特殊符号, @代表冒号左边的东西, ^代表冒号右边的一串东西,就可以这么写
gcc -shared add.o sub.o mul.o 1.o 2.o 3.o -o libmymath.so等价于
gcc -shared \^ -o @
如图所示,make的时候,把 \^ 和 @的内容自动替换了进去

当然,也可以这么写g++ -o @ ^ ,这是一样的,写法问题,就和rm -rf * 和 rm * -rf 是一个道理
2.如果想一次同时编译形成两个可执行程序,怎么搞?在后面学习网络书写客户端和服务端要用上的。利用依赖关系,比如:all:client server 依赖方法为空,因为也不需要依赖方法,client和server正常写依赖方法和依赖关系就行!
- 如果一个文件的名字太长,可以取别名,比如:
bash
m=main.cpp
main:$m
g++ -o $@ $^ -std=c++11 -lpthread
$就是取出里面的值,比如查看环境变量的值.
4.上面我们提到,make指令失败的情况,这里我们来提一下原因和原理
原因:提高编译效率,这次编译的结果和上次一模一样,那我为什么还要编译??没必要。
原理:源文件经过编译形成可执行文件,那么源文件的最近修改时间一定比可执行文件的最近修改时间要短!
如果我们修改了源文件,可执行还没有被重新编译,那么源文件的最近修改时间大于可执行文件的最近修改时间! 所以,只需要判断这两个时间的比较即可。这就说明,一定有东西记录了这个文件的最近修改时间!这个在文件系统里会提,本质上是存放在了Block块中。
stat指令查看

这里有很多信息,IO Block: 文件系统分配粒度,一次分配4KB。
Blocks:实际分配大小,size是99,不满4kb,分配了4kb,Blocks * 512是字节数,也就是4096字节,4kb。当然这两个不是重点,重点看接下来显示出的三个时间戳,Access,Modify,Change。
文件 = 文件内容 + 文件属性。
1.对文件内容的修改就是Modify,对文件属性的修改就是Change,Access称为访问时间,一般是指最近一次读取的时间,比如cat了一下。
2.一般来说,Modify改变,Access和Change跟着一起改变,因为你更改文件内容,一定要读取,文件的属性比如size这些也会跟着改变。
3.Change改变,Modify和Access一般不变,比如修改权限,只有Change时间改变了,文件内容没有改变,也没有读取文件。
4.一般来说,Change会跟着Access和Modify一起改变,因为Change是记录文件属性的修改情况,这两个时间戳包含在文件属性之内。
5.touch命令的默认行为就是更改Modify和Access时间,Change由于属性变化会跟着改变,如果只想改变Modify,touch -m, 改变Access,touch -a,无论是哪个Change都会跟着变化。
另外还有个问题,你可能在cat文件之后查看,发现access时间没有改变,这实际上是Linux系统采用了默认的access time优化策略,因为正常来说访问一个文件是很频繁的,如果每次都更新就会带来额外的开销。可以通过mount命令查看,其中查看到的relatime就是默认选项,它只会在这两个情况下更新:
1.文件的modify/change时间晚于当前access时间;
2.距离上次access时间更新已超过 24 小时。
这就是cat命令 access时间没有改变的原因。
四、Cmake
这里只简单介绍一下,核心还是方便使用,Cmake是一款工具,核心就是生成makefile文件,不需要自己写,其中有各种各样的方法去规定makefile的样子,文件名:CMakeLists.txt,比较简单的写法:
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Reactor_Server)
set(CMAKE_CXX_STANDARD 11)
add_executable(Reactor_Server Main.cc)
target_link_libraries(Reactor_Server jsoncpp)
add_executable(Client Cal_Client.cc)
第一行是需要的版本,第二行是文件名,第三行是采用C++11标准,第四行是生成可执行文件用Main.cc 编译,第五行是连接指定库,Reactor_Server连接jsoncpp这个库。
写完之后,cmake . 指令运行,或者新开一个目录使得Cmake生成的文件在这里,cmake ...即可。就会看到有一个Makefile了。
五、模拟进度条
前置知识
就是写一个类似下载一样的进度条,一方面可以加强vim的使用,另一方面可以先了解缓冲区的一些知识,在后面的IO中也会提到。
首先我们要知道\r和\n的区别,\r是回车,就是光标回到开始,\n是换行,光标切换到下一行的同一位置。C语言中,\n的概念是换行,但是实际实现中,\r一般被解释成回车 + 换行,\r仍然是单纯的回车。

为什么我输出了两个hello world只显示出来一个呢?就是因为\r 只是回车,然后第二个hello world,将原来的hello world进行了覆盖!再来看一个现象:
cpp
#include<bits/stdc++.h>
#include<unistd.h>
using namespace std;
int main()
{
cout << "hello world";
sleep(3);
return 0;
}
sleep是库函数,字面意思。
现象是先sleep(3)秒,再输出hello world,但是是先执行的hello world,再sleep啊,这说明hello world一定被保存起来了!实际上是保存在用户级别的缓冲区内了!这是C语言为我们维护的一段内存。
如果我想强制输出呢?刷新缓冲区即可,C语言中可以用fflush函数,这里先提一句,C标准库为我们提供了三个标准输出流,分别是标准输入,标准输出,标准错误,这里我们要刷新标准输出,fflush(stdout).
C++中,cout有一个成员函数flush,所以直接cout.flush()就可以.
代码
有这种前置知识下,就可以开始写进度条了。
进度条的原理很简单,就是一个字符串每次都变长,利用\r的特点让他每次都覆盖,看成进度条的效果。使用printf方便一点,cout不太方便打印。
代码比较简单,这里只提几个注意事项,因为\和%是转义字符,所以规定字符\是\,%是%%, 对于打印,因为进度条是打印固定长度,只是中间的#不断变长,所以要控制固定宽度,比如100,但是又因为默认是右对齐,我们这里要左对齐,所以要带-。
cpp
#define TOP 100
#define STYLE '#'
#define RIGHT ']'
const char s[] = {'-','\\','/', '|' };
void pbar(unsigned int time)
{
printf("进度条:\n");
char str[102];
memset(str,'\0',sizeof(str));
int cnt = 0;
int len = strlen(s);
while(cnt <= TOP)
{
printf("[%-100s][%d%%][%c]\r",str,cnt,s[cnt % len]);
fflush(stdout);
str[cnt++] = STYLE;
if(cnt < 100) str[cnt] = RIGHT;
sleep(time);
}
printf("\n");
}