文章目录
- 前言
- 一、gcc/g++编译器
- 1、gcc/g++安装
- 2、gcc介绍
- 3、gcc和g++区别
- 3.1 gcc不是只能编译.c源文件
- 3.2 gcc和g++编译文件
- 3.3 gcc 不会定义 __cplusplus 宏,而 g++ 会
- 3.5 演示
- 4、gcc/g++编译过程
- 二、动态库和静态库
- 1、动态库和静态库
- 2、动态链接和静态链接
- 2.1 动态链接
- 2.2 静态链接
- 2.3 总结
- 三、Linuc项目自动化构建工具-make/Makefile
- 1、引出make/Makefile
- 2、make/Makefile使用
- 3、make执行过程
- 4、make是怎么知道test目标为最新的呢
- 四、进度条小程序
- 1、缓冲区概念
- 2、\r&&\n
- 2.1 回车和换行介绍
- 2.2 回车和换行在c语言中区别。
- 3、进度条程序
前言
一、gcc/g++编译器
1、gcc/g++安装
查看版本: gcc/g++ -v/–version。
安装命令:sudo yum install -y gcc-c++。
2、gcc介绍
GCC(GNU Compiler Collection,GNU编译器套件)是由 GNU 开发的编程语言 译器。GNU 编译器套件包括C、C++、Objective-C、Java、Ada 和 Go 语言前 端,也包括了这些语言的库(如 libstdc++,libgcj等)。
3、gcc和g++区别
3.1 gcc不是只能编译.c源文件
可使用 gcc source.file -x language指定语言规则,来编译多种编程语言。但是默认情况下gcc只根据文件后缀名来判定源文件是什么语言,然后用对应的语法规则进行编译,用对应的链接规则生成可执行文件。
3.2 gcc和g++编译文件
gcc会将后缀为.c的文件当作c程序编译,将后缀为.cpp的文件当作c++程序编译。
g++会将后缀为.c的文件当作c++程序编译,将后缀为.cpp的文件也当作c++程序编译。
在编译阶段.cpp文件时,g++会调用gcc,因为对于.cpp文件,两者都将当作c++程序编译。但是因为使用gcc来编译.cpp文件时不会自动链接c++库,还需要手动来指定。但是g++在编译.cpp文件时,可以自动链接c++库,所以在平常使用时都会习惯使用g++来编译并链接,因为使用gcc虽然可以编译.cpp文件,但是需要手动指定连接c++库。其实g++编译.cpp文件还是调用的gcc,只不过在链接时,g++可以自动链接c++库。
3.3 gcc 不会定义 __cplusplus 宏,而 g++ 会
实际上,这个宏只是标志着编译器将会把代码按 C 还是 C++ 语法来解释,如上所述,如果后缀为 .c,并且采用 gcc 编译器,则该宏就是未定义的,否则,就是已定义。
3.5 演示
使用gcc a.cpp会编译失败,因为gcc不会自动连接c++库。
可以通过手动指定来使gcc a.cpp -lstdc++ 编译链接成功。
4、gcc/g++编译过程
我们知道一个程序的翻译需要经过以下的几个步骤。
我们可以使用如下的指令来生成一个可执行文件。
如下图所示的两种方法都可以将test.c编译成可执行文件test,即 -o 后面跟的就是生成的可执行文件的名字,剩下的都是要编译的文件。
上面是将test.c直接编译为可执行文件test,如果我们想要将test.c预处理后就停下来,我们可以使用如下的指令。需要注意的是在程序编译过程中,-o 后面跟的都是生成的文件。
-E:从现在开始进行程序的翻译,如果预处理完成,就停下来。
我们可以看到经过预处理之后的.i文件中还是c语言,预处理就是将.c文件中包含的头文件展开了,然后将#define定义的宏进行替换,并且将条件编译判断了,还将注释删除了。
接下来就是编译了,我们可以使用下面的指令来将test.i文件编译为test.s汇编语言文件。
-S:从现在开始进行程序的翻译,如果编译完成,就停下来。
我们可以看到经过编译之后将c语言代码都编译为了汇编语言代码。
下面就是汇编了,将test.s汇编文件汇编为机器可以识别的二进制文件。使用下面的指令可以将.s文件变为.o的可重定向目标文件。
-c:从现在开始进行程序的翻译,如果汇编完成,就停下来。
此时可以看到汇编文件已经变为了二进制文件。
然后再经过链接后就形成了可执行的test文件。
二、动态库和静态库
1、动态库和静态库
我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢?
答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib64”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。
lib64目录主要存放的是可被程序直接加载并使用的 64 位代码模块,包括动态库、静态库、内核模块等,这些文件对程序运行都至关重要。在 Linux 系统中,lib64目录通常用于存放 64位库文件的二进制文件。相比 lib 文件夹,lib64 专门用于存放 64 位库文件,而 lib 中是 32 位版本的库。这在支持多重系统时很有必要。
主要有以下几类文件:
(1). 动态链接库文件(.so):这些是可直接被程序加载并链接的共享库,以.so作为文件扩展名。如 libopencv.so。
(2). 静态链接库文件(.a):这些是静态存档文件,用于静态链接,以.a作为扩展名。如libxxx.a。
(3). Linux 内核模块文件(.ko):一些可以被内核动态加载的模块,以.ko为扩展名。
(4). 程序的执行文件:一些程序的执行文件也会放在 lib64 文件夹下,比如 /usr/lib64/firefox 等。
(5). 配置文件、说明文件等。
我们可以在 /usr/include/stdio.h 文件中查看到printf函数的声明,stdio.h就是我们经常包含的一个头文件。
在stdio.h文件中只有printf文件的声明,而printf函数的具体实现其实就在lib64目录下的某一个库中。
使用ldd指令可以查看test可执行文件的依赖库,我们可以看到test可执行文件依赖libc.so.6这个动态库。然后我们可以在lib64目录下找到这个库。
c语言程序是脱离不开c库文件的,因为stdio.h文件中提供了c语言的方法列表,即方法的声明。而动态库和静态库中就提供了c语言方法的具体实现,在连接过程就是通过方法的声明找到所对应库中的方法,然后生成可执行文件,这就是链接的过程。
2、动态链接和静态链接
一般链接的过程有两种方式:
动态链接 - 需要动态库(.so动态库文件)
静态链接 - 需要静态库(.a静态库文件)
2.1 动态链接
动态库在Linux上一般使用.so文件,在Windows上使用.dll文件。编写动态库使代码可以被广泛重用,是软件复用的重要方式。动态链接使程序升级和部署更加灵活。
动态链接库(Dynamic Link Library,DLL)是一种包含可以被多个程序共享的代码和数据的库文件,它可以在程序运行时动态加载,而不是在linking时静态加载。
动态链接库的主要特征包括:
(1). 共享性:多个程序可以同时使用一个动态库,减少重复代码。
(2). 动态加载:程序在运行时才加载动态库,而不是在编译时加载。
(3). 版本管理:动态库可以单独升级,而程序无需重新编译。
(4). 依赖管理:明确定义库的依赖关系,按需加载所需库。
(5).语言中立:支持多种语言调用,如C、C++、Rust等。
(6). 运行时链接:程序不需要包含动态库代码,只在运行时做符号解析并重定位。
(7). 空间节省:多个程序共享同一块内存,减少内存占用。
动态链接就是将库中的方法的地址填入到可执行程序中,建立可执行程序和动态库的关联,当执行到该方法时,就根据地址去动态库中找该方法的实现。这样可以节省资源,即使用哪个方法直接去动态库中对应的地址找该方法的实现即可,但是这就非常依赖动态库文件了,如果没有该动态库文件,就会链接失败。因为只有方法的声明,而没有方法的具体实现。
gcc和g++默认形成的可执行程序是动态链接的。
2.2 静态链接
静态库可以看作编译时直接插入目标程序的代码段。它的主要优点是移植方便,缺点是占用空间大、重复代码多。
静态链接库(Static Library)是一种在程序编译链接阶段就被整合到目标程序中的库文件,和动态链接库相对。
静态库的主要特征:
静态加入:静态库在编译时就被整合到目标程序中,而不是运行时动态加载。
独立存在:静态库是一个独立的文件,可以被重用。
无版本管理:程序每次都会使用静态库的一个快照,不方便做版本升级。
重复代码:如果多个程序都链接了相同的静态库,会有重复代码。
可移植性:静态库可以和程序一起打包部署,无需安装库的环境。
语言相关:静态库通常是特定语言编译而成,如C++。
编译时间增长:增加了编译链接时间。
增加可执行程序大小:会将库代码加入到可执行文件中。
静态链接就是将原本在动态库中链接的方法直接拷贝到了可执行程序中,这样程序在执行时就不需要根据地址去找函数的实现了,这样程序就不依赖库文件了,但是这样可执行文件中包含了方法的实现,这就使得文件变得大了。
-static表明使用静态链接的方式形成可执行程序。但是在使用前需要先下载c和c++的静态库。
下载c静态库。
下载c++静态库。
然后可以使用静态链接的方式生成可执行程序tests。
2.3 总结
我们可以看到test为动态链接形成的可执行文件,tests为静态链接形成的可执行文件,可以看到tests可执行程序的大小快要达到test可执行程序的100倍,所以说静态链接会将库代码加入到可执行文件中,这就增大了可执行文件的大小。
三、Linuc项目自动化构建工具-make/Makefile
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
1、引出make/Makefile
我们之前写的代码都是只有一个源文件需要编译,但是一个项目中有很多源文件都需要编译,此时如果我们再一个一个将源文件进行编译,这样启动一次项目的效率很慢。
例如我们为加、减、乘、除函数都写了一个源文件。
此时如果我们想要编译test.c源文件,只编译一个源文件是不行的。
需要将这些源文件都编译为一个test可执行程序。我们感觉到了这样一个小的程序都有了五个源文件,而一个项目中有那么多源文件,并且这些源文件之间的编译还需要有顺序。如果编译项目都是这样的话,那么一个项目的编译过程就太复杂了。所以就有了项目自动化构建工具make/Makefile。
2、make/Makefile使用
make是一个命令,而Makefile是一个文件,我们需要在Makefile文件中编写一个项目的编译,然后执行make命令就可以执行Makefile里面编写好的方法了。
我们可以使用touch在项目目录下先创建一个Makefile/makefile(大小写都可以)文件,然后使用vim进入该文件中进行makefile文件编写,我们先将makefile文件编写为这样,然后将文件保存并退出。
此时执行make命令,就会发现我们没有输入gcc编译命令,但是已经形成了一个test可执行文件。并且在make命令下面我们发现 gcc test.c add.c sub.c mul.c div.c -o test 这样的命令。到这里我们就应该可以明白了,make和makefile的一个作用,即make和makefile搭配使用可以将我们的源文件编译变为自动化源文件编译。
下面我们就来详细说一下makefile文件的编写。想要掌握makefile,首先需要了解两个概念,⼀个是⽬标(target),另⼀个就是依赖(dependency)。⽬标就是指要⼲什么,或说运⾏ make 后⽣成什么,⽽依赖是告诉 make 如何去做以实现⽬标。在 Makefile 中,⽬标和依赖是通过规则(rule)来表达的。所以下面的一个规则的意思就是test目标需要依赖test.c add.c sub.c mul.c div.c这几个源文件才可以执行gcc test.c add.c sub.c mul.c div.c -o test 这一个命令。
3、make执行过程
make命令后面不跟目标的话,就会自顶向下扫描makefile文件,执行遇到的第一个目标,伪目标也会执行。
此时输入make命令就会执行clean目标。
4、make是怎么知道test目标为最新的呢
Linux的文件有三个时间属性,使用stat命令可以查看文件的这三个时间。
1、Access:最近一次访问文件的时间(修改也算访问)
2、Modify:最近一次修改文件内容的时间(当修改文件内容时,因为文件的大小也会变化,所以change时间也会改变)
3、Change:最近一次修改文件属性(权限,大小等)的时间
make 在检查⼀个规则时,采⽤的⽅法是:如果目标的依赖文件的时间戳⼤于⽬标的时间戳,即该目标的依赖⽂件更新了,则知道有变化,那么需要运⾏规则当中的命令重新构建⽬标。而如果该目标的时间戳大于依赖文件的时间戳,说明该目标在构建后依赖文件没有发生变化,此时再构建该目标时就会提醒该目标已经是最新了。
如果还需要更详细的了解makefile文件的编写,可以看这个大佬写的文章。
四、进度条小程序
1、缓冲区概念
我们创建一个test.c文件,然后写入下面的代码。
然后再创建makefile文件并编写下面的两个目标。
此时执行make命令可以发现在屏幕上先显示了hello world这句话,然后才停顿1秒。而当我们将test.c中的printf()中的\n去掉后保存文件。
此时我们执行make命令会发现屏幕上是先停顿3秒,然后才打印出来hello world这句话。这就和c语言的缓冲区有关了。c语言会提供输出缓冲区,该缓冲区根据特定的刷新策略来进行刷新。而显示器设备一般的刷新策略是行刷新,即碰到\n就会把\n之前的所有的字符全部都显示出了。所以上面的代码中其实printf()刚开始就执行了,只不过屏幕没有刷新,所以信息没有被显示出了。而fflush()方法可以直接刷新缓冲区,所以在printf后面加上fflush(stdout),即代表立即刷新屏幕缓冲区,这样信息就会被显示出来了。
2、\r&&\n
2.1 回车和换行介绍
首先介绍一下“回车”(carriage return,’\r’)和“换行”(line feed,’\n’)这两个概念的来历和区别。在计算机还没有出现之前,有一种叫做电传打字机(Teletype Model 33)的玩意,每秒钟可以打10个字符。但是它有一个问题,就是打完一行换行的时候,要用去0.2秒,正好可以打两个字符。要是在这0.2秒里面,又有新的字符传过来,那么这个字符将丢失。于是,研制人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫做“回车”,告诉打字机把打印头定位在左边界;另一个叫做“换行”,告诉打字机把纸向下移一行。这就是“换行”和“回车”的来历,从它们的英语名字上也可以看出一二。
后来,计算机发明了,这两个概念也就被般到了计算机上。那时,存储器很贵,一些科学家认为在每行结尾加两个字符太浪费了,加一个就可以。于是,就出现了分歧:
Unix 系统里,每行结尾只有“<换行>”,即“\n”;
Windows系统里面,每行结尾是“<回车><换行>”,即“\r\n”;
Mac系统里,每行结尾是“<回车>”,即“\r”。
一个直接后果是,Unix/Mac系统下的文件在Windows里打开的话,所有文字会变成一行;而Windows里的文件在Unix/Mac下打开的话,在每行的结尾可能会多出一个^M符号。
Windows与Unix文件格式是不同的,问题一般就是出在\r\n问题上。当在不同的系统间传递文件,就要涉及格式的转换。
上面的内容来自LvSantorini作者的文章,详细了解可以去读这位大佬的原文章。
2.2 回车和换行在c语言中区别。
换行 (\n) 本义是光标往下一行(不一定到下一行行首),n的英文newline。
回车 (\r) 本义是光标重新回到本行开头,r的英文return。
下面的图为\n换行和\r回车在现实中的意思。即换行就是换到下一行,不会回到下一行的起始位置;而回车就是回到本行的起始位置。
但是在c语言中的"\n"的意思就是输入完一行内容后,光标转到下一行的起始位置 。
而"\r"的意思就是回到本行的起始位置。
我们可以写一个倒计时程序来感受\n和\r在c语言程序中的区别。
下面为使用\n来换行打印,即每打印一句都会换到下一行的起始位置打印。
而如果我们使用\r的话就会这样打印,即每次打印就不会换行了,而是回到本行的起始位置。
3、进度条程序
在知道了上面的\n和\r在c语言中的区别后,我们就可以使用\r来写进度条程序了。
#include<stdio.h>2 #include<unistd.h>3 #include<string.h>4 #define NUM 1025 6 int main()7 {8 char bar[NUM];9 memset(bar,0,sizeof(bar));10 const char *lable = "|/-\\";11 int n = 0;12 while(n <= 100)13 {14 //[%-100s]表示每次从左边开始打印bar字符串,每次打印都占100个字符位15 //[%d%%]表示按百分之打印整数n16 //%c 打印lable[n%4]表示动态打印lable数组里面的内容。17 printf("[%-100s][%d%%] %c\r",bar,n,lable[n%4]);18 bar[n]='#';19 n++; 20 fflush(stdout);21 usleep(20000);22 }23 printf("\n");24 return 0;25 }