一、Linux编译器-gcc/g++使用
1.1 为什么我们可以用C/C++做开发呢?
**无论是在windows、还是Linux中,C++的开发环境不仅仅指的是vs、gcc、g++,更重要的是语言本身的头文件(函数的声明)和库文件(函数的实现)。**所以我们在安装这些软件的时候,同时也选择了相关的开发包,会同步下载对应的头文件和库文件。
所以任何一款编译型语言的使用,都必要需要安装相应的开发包(头文件和库文件)
查看头文件:ls /usr/include/
1.2 预处理(进行宏替换)
预处理阶段会涉及到很多操作
a、去注释
注释我们一般用于对我们的代码进行解释说明,但并不参与编译,所以是可以直接去掉的,节省文件的大小。
b、头文件展开
头文件里面包含了我们需要的一些函数的声明,由于在链接之前各个文件都是独立进行编译和转汇编的,所以头文件将函数声明展示出来其实就是为了在编译过程的时候告诉编译器,这个函数是存在的,一定要放行,而最后的函数定义一般得等到链接的时候才能找到 。
c、条件编译
条件编译其实就是有选择的编译,比较常见的一种情况比如说我们要通过打印来观察代码的运行情况(调试),但是仅仅只是为了起到一个调试的作用,所以我们调试后还要删掉其实有点可惜,所以我们可以通过条件编译来对他进行保留,在必要的时候启动这段代码或者是去掉这段代码。
还有一种技巧就是**-D(宏) 直接在外面定义宏**
一种使用条件编译的场景举例------vs的社区版(免费)和专业版(付费)
一般在下载VS的时候可能会存在这两种版本,而一般我们购买那一种是根据我们的需求,而专业版是付费的所以必然功能是更加健全的,但从开发人员的角度,社区版和专业版难道需要维护两份代码么??其实他们是一份代码,只不过对于社区版,他们通过条件编译去掉了一些功能!!
命令:gcc -E test.c -o test.i
告诉gcc,从现在开始进行程序编译,从预处理工作就停下来,不要往前走了
1.3 编译(生成汇编)
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等(有错误会直接崩掉),以确定代码的实际要做的工作,在检查 无误后,gcc 把代码翻译成汇编语言。其实头文件里包含的函数声明,其实就是告诉编译器,这个函数是存在的,你要放行!!
命令:gcc -S test.i -o test.s
告诉gcc,从现在开始进行程序编译,将编译过程做完就停下来
1.4 汇编(生成机器可识别代码)
汇编阶段是把编译阶段生成的".s"文件转成目标文件(可重定位目标二进制文件)
命令:gcc -c test.i -o test.o
告诉gcc,从现在开始进行程序编译,汇编结束就停下来
注意:有执行权限和具有可执行能力是两回事,就好比一个富二代,他有继承家产的权利,但是他未必就有管理家业的能力------>所以.o文件是无法运行的!!需要经过链接才行!!
1.5 链接(生成可执行文件或库文件)
将目标文件和库进行链接,就得到了可执行程序
命令:gcc test.o -o test
明明已经生成了机器可以读懂的文件,为什么还需要链接才能运行呢???原因就在于我们当前的文件并只有函数调用、函数声明,缺没有函数方法,所以必须要和库(C语言标准库,本质上就是一个文件)链接,函数的方法就存在于库中(其实就是把源文件.c经过一定的编译,然后打包,最后只给你提供一个文件)这样做有两个好处:
1、方便源文件的隐藏(未来我们想把程序给别人用,但不希望他看到源码,也可以这样)
2、不让我们做重复工作(帮我们造好了轮子),站在巨人的肩膀上 。
所以软件=头文件的方法+库文件提供的方法实现+你的代码
个人觉得在日常的学习中,我们要尽可能地去尝试自己造轮子,研究底层,扎实内功,这样才可能应对未来的一些更复杂的情况,而在以后工作的时候,尽可能用一些已经写好的高效的代码,避免重复工作。而如果需要自己造轮子,早期的学习就会给你提供很大的帮助。
1.6 .o和库是如何链接的(静态链接和动态链接)
1.6.1 动态库文件和静态库文件
在Linux中: .so(动态库) .a(静态库)
命名规则:libname.so.XXX
在windows中:.dll(动态库) .lib(静态库)
在Linux中,通过ls /usr/lib64/libc.so* 可以看到我们的动态库文件
通过指令ldd 可以查看该可执行程序所依赖的动态库
还有之前我们知道其实指令的本质就是可执行程序,所以我们也可以去查看指令所依赖的动态库,我们会发现大部分都是用C的库
静态库的文件默认是没有安装的,需要通过以下指令去手动安装
C静态库:sudo yum install -y glibc-static
C++静态库:sudo yum install -y libstdc++-static
1.6.2 动态库和静态库理解
1、假设我们考上了一所高中,这里不让上网,但是你是一个喜欢打游戏的人,你通常会将打游戏这件事列入到自己每天的计划表中,而你的学校附近恰好有一个网吧,你在将计划进行到规定时间的时候就会翻墙出去上网,然后再回来。
自己的计划表------可执行程序
学校------编译器
网吧------动态库
网吧里的电脑------库文件
也就是说当程序执行到某个地方时,他会跳出到动态库继续执行,然后再回来,这个过程就是动态链接。
2、假设有一天网吧老板突然被举报,并且没有营业执照而被迫查封,这个时候我的计划就无法如期进行了,并且影响的不只是你,还有学校其他喜欢打游戏的人!
所以动态库不能缺失!!一旦缺失影响的不仅仅是一个程序,而是多个程序都会崩溃!
3、突然有一天学校允许学生上网了,这个时候网吧老板捕捉到了商机,把网吧的电脑搬出来开了一家二手电脑专卖店,然后喜欢打游戏的同学都过来购买了电脑了!!
二手电脑专卖店------静态库
二手电脑------库文件
也就是说,静态库进行静态链接的时候,会将自己的方法拷贝到目标程序中,该程序以后不用再依赖静态库!!
4、有一天那个老板又被举报查封了,但是这个时候学校里的学校却没有人知道这件事,因为大家都有自己的电脑了,店查不查封对他们没什么影响。
所以静态链接的程序并不依赖库文件,即使静态库丢失了程序也可以正常运行!!
1.6.3 控制链接方式的选择
当我们不做限制时,会默认使用动态链接。如果我们想要静态链接 就需要加 -static选项
1、如果没有-static,默认编译可能会出现三种情况:动态链接->静态链接->报错。而有了static,就会去掉第一种情况,即静态链接->报错
2、不一定是纯的动态链接或者静态链接,也可以是混合的
1.6.4 动态链接和静态链接的优缺点
动态库:
优点:动态库是共享库,可以有效节省资源(磁盘空间、内存空间、网络空间)
缺点:动态库一旦缺失,所有的程序将无法运行
静态库:
优点:不依赖库,程序可以独立运行
缺点:体积大,消耗资源
一般来说,我们在实际应用中更倾向于使用动态链接,因为体积大所带来的影响是很大的,比方说你下个游戏要1G,但是用静态链接可能就需要上百G,所以无论是我们还是Linux默认,都是会尽量选择动态链接。
1.6.5 debug&&release
debug可以被追踪调试,是因为在形成可执行程序的时候被添加了debug信息,所以占用的空间会更大一点。而release一般作为发行版,重在用户体验,所以体积会较小一点,我们Linux下编译默认是release版本,-g选项可以帮助我们改成debug版本。
扩展:可执行程序形成的时候,不是无序的二进制构成,而是有自己的格式的------>可执行程序有自己的二进制格式!(ELF格式)
readelf -S可以查看可执行程序的二进制构成
二、Linux项目自动化构建工具-make/Makefile
为什么我们会需要自动化构建工具????
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的 规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
所以,makefile带来的好处就是------"自动化编译",一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
说明:
1、make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命 令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一 种在工程方面的编译方法。
2、make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
2.1 依赖关系和依赖方法
举个例子,到月底的时候,你会给你爸打电话要下个月的生活费,这是因为你们两个之间存在依赖关系,而讨要生活费就是你们之间的依赖方法,现实中人与人之间能建立联系其实就是因为有依赖关系和依赖方法。
而我们使用makefile文件也同样需要依赖关系和依赖方法
我们会发现这个过程其实有点像是函数调用,特别像递归,形成了makefile的自动推导
所以其实我们乱序也可以,因为他会自己去找,如果找不到就会报错。
2.2 make clean
还是上面这个情景,我们一make就生成了很多文件
但是我们如果想要清理的话,就需要用clean(不需要依赖关系)
为什么make执行前面的,而make clean执行后面的呢???
原因就是因为,如果make没有指定的话,默认就是从第一行开始扫描到第一个make就执行,所以建议将编译的工作放到最前面,然后把清理的工作写到后面!!
2.3 为什么不会允许多次make呢??(重点)
我们会发现我们make一次之后,就不然我们继续make了。这是为啥呢?
1、为了提高编译效率!如果我们的文件相较于之前没有发生过任何的变化,那么就不能进行make(因为系统觉得没必要,纯属浪费时间),如果相较于之前的内容有更新过,那么就会重新形成一次新的可执行程序覆盖掉。
2、那么我们的系统究竟是如何做到判断这个文件是否被更新过呢??难道是将文件扫描一遍??显然不现实。其原理如下:
首先,可执行程序一定是由源文件形成的,所以源文件的最近修改时间一定比可执行程序的最近时间要老------>推到得出,如果我们的源文件的最近修改文件比可执行程序时间新,那么就说明这个文件被修改过!!!
总结:只需要比较可执行程序的时间和源文件的最近修改时间即可!
.exe比.c老,说明被修改过,需要重新编译
.exe比.c新,说明没有被修改过,就不需要重新编译(提高了编译效率!)
2.4 stat指令
stat指令是查看文件或者文件系统的详细信息
Access表示访问时间,任何增删查改都算访问操作
Modify表示修改文件内容的时间
Change表示修改文件属性的时间
1/ 按道理来说,Access的修改频率应该是最高的,但有些时候是无意义的,并且由于磁盘是属于外部设备,如果频繁访问的话,效率是极低的,所以在设计的时候,Access的时间其实是取决于另外两个时间的!!
(1)当仅读取或者访问文件文件时,只有Access会改变
(2)当修改文件内容时,Modify(文件内容)和Change(文件大小)都会改变,Access不一定改变
(3)当修改文件权限属性时, 只有Change会改变
2/ .PHONY:**所以make会根据源文件和目标文件的新旧,判定是否需要重新执行依赖关系进行编译,**那么如果我们希望依赖关系总是被执行,就需要.PHONY:(伪目标)
**.**PHONY:其实就是告诉make,这个目标是我的朋友,只要他想编你就让他编,不要阻拦,所以这个关键词常用在clean的身上。
3、 在一个文件不存在的时候,touch的作用是将该文件新建出来,但是如果这个文件存在的话,touch就可以将三个时间都修改成当前的时候,或者是用-a -m -c 选项来强制修改其中一个。
2.5 特殊符号
1、@和^ 分别指代依赖关系的前一个和后一个
2、依赖方法最前面+@ ------>对应的依赖方法在make后不会回显到屏幕上
三、Linux小程序-进度条
3.1 回车换行
其实回车和换行是两个概念,换行仅仅表示从当前位置跳到下一行,而回车则是回到该行的起点
其实**\n 默认是换行+回车 而\r才是回车**(键盘上的enter图标)
以前的老式打印机,通过打字他会自动从左往右写,而当我们写到最右边的时候,先往下跳(换行),再把他拉到最左边接着写(回车)
3.2 缓冲区
首先认识一下sleep函数 参数表示休息多少秒 头文件是<unistd.h> 方便我们观察
他会输出在输出hello Linux的时候,然后休眠2秒。
如果我们去掉换行,则hello Linux会在两秒后才出现
思考:去掉了\n,为什么会是先休眠再输出呢??
首先我们要肯定的是,代码运行的顺序必然是从前往后的,所以sleep一定是在printf之后执行的,这说明在休眠的时候,hello Linux必然被保存在某个地方,其实就是缓冲区!!缓冲区是由C语言维护的一段内存,他默认是按行刷新回显到显示器上!!所以如果我们想要提前刷新的话,就需要用fflush
参数可以是:标准输出(stdin)、标准输入(stdout)、标准错误(stderr)
这样就可以将缓冲区的内容刷新出来了!!!
3.3 简单倒计时的设置
首先我们需要processbar.c processbar.h main.c 三个文件
接下里先实现一个简单倒计时
我们要考虑两个问题:
(1) 倒计时要覆盖之前的位置,所以需要回车/r
(2)缓冲区是按行刷新的,所以必须要提前刷新
但是我们一旦将cnt变成10以上,就会出现这样的情况
这是因为我们的显示器是字符显示设备,所以他都是一个字符一个字符去打印的,你以为10是一个整体,其实对于他来说,是字符1和字符0 所以为了解决这个问题,我们需要去设置格式。比如说%-2d表示两个字符,格式左对齐
3.4 进度条基础版本
改进的地方:
(1)需要一个[ ]括起来
(2)需要一个数字来展示进度
(3)最后希望能有一个类似转圈的东西
cpp
#include"processBar.h"
2 #define NUM 102
3 const char* lable="|/-\\";//\\转义为1个
4 void processbar()
5 {
6 char bar[NUM];
7 memset(bar,'\0',sizeof(bar));
8 int len=strlen(lable);
9 int cnt=0;
10 while(cnt<=100)
11 {
12 printf("[%-100s][%d%%][%c]\r",bar,cnt,lable[cnt%len]);
13 fflush(stdout); //需要刷新出来
14 bar[cnt++]=STYLE;
15 usleep(100000);
16 }
17 printf("\n");
18 }
3.5 进度条外部调用版本
以上进度条是我们去设置进度的,但是实际我们的这个进度条应该由外部去调用,根据软件的具体情况去调用进度条,而具体的进度应该是由外部软件决定的。
cpp
#pragma once | 1 #include"processBar.h" 1 #include"processBar.h"
2 #include<stdio.h> | 2 | 2 #define NUM 102
3 #include<unistd.h> | 3 int main() | 3 const char* lable="|/-\\";//\\转义为1个
4 #include<string.h> | 4 { | 4 char bar[NUM];
5 void processbar(int rate); | 5 //模拟当前正在下载一款软件 | 5 void processbar(int rate)
6 #define STYLE '#'//风格 | 6 int total=1000; | 6 {
7 | 7 int cur=0; | 7 if(rate<0||rate>100) return;//防止你非
8 | 8 while(cur<=total) | 法
~ | 9 { | 8 int len=strlen(lable);
~ | 10 processbar(cur*100/total); | 9 printf("[%-100s][%d%%][%c]\r",bar,ra
~ | 11 cur+=10; | te,lable[rate%len]);
~ | 12 usleep(50000); | 10 fflush(stdout); //需要刷新出来
~ | 13 } | 11 bar[rate++]=STYLE;
~ | 14 printf("\n"); | 12 }
~ | 15 return 0; | 13
~ | 16 }
3.6 进度条回调版本
有些时候我们希望能进行多任务下载,因此可以将该进度条设置成回调函数,这样不同的软件就可以传对应不同的进度条。
cpp
#pragma once 1 #include"processBar.h" | 1 #include"processBar.h"
2 #include<stdio.h> | 2 | 2 #define NUM 102
3 #include<unistd.h> | 3 int main() | 3 const char* lable="|/-\\";//\\转义为1个
4 #include<string.h> | 4 { | 4 char bar[NUM];
5 void processbar(int rate); |E> 5 download(processbar); | 5 void processbar(int rate)
6 #define STYLE '#'//风格 | 6 return 0; | 6 {
7 typedef void(*callback_t)(int);//回调函>| 7 } | 7 if(rate<0||rate>100) return;//防止你非
数 | 8 | 法
8 void downLoad(callback_t cb); | ~ | 8 int len=strlen(lable);
9 | ~ | 9 printf("[%-100s][%d%%][%c]\r",bar,ra
~ | ~ | te,lable[rate%len]);
~ | ~ | 10 fflush(stdout); //需要刷新出来
~ | ~ | 11 bar[rate++]=STYLE;
~ | ~ | 12 }
~ | ~ | 13
~ | ~ | 14 void download(callback_t cb)
~ | ~ | 15 {
~ | ~ | 16
~ | ~ | 17 int total=1000;
~ | ~ | 18 int cur=0;
~ | ~ | 19 while(cur<=total)
~ | ~ | 20 {
~ | ~ | 21 cb(cur*100/total);
~ | ~ 22 cur+=10;
~ | ~ | 23 usleep(50000);
~ | ~ | 24 }
~ | ~ | 25 printf("\n");