开发工具gcc/g++
完成代码的编写完后,要形成可执行程序,需要编译工具进行对代码的编译。
C语言的编译工具是gcc,c++的编译工具是g++。
如果g++没有的话,可以切换到root执行命令yum install -y gcc-c++
C语言和C++的编译:
gcc只能编译C语言,而g++可以编译c++也可以编译C语言。
在C语言的最后一章,已经学习了源代码到可执行程序的的简单步骤。下面来详细一点的说明。
源代码,即程序,本质上是文本;可执行程序是机器语言,也就是二进制组成的。
文本翻译成二进制要经过以下几个步骤:
预处理,编译,汇编,链接。
预处理要进行:宏替换,头文件展开,去注释,条件编译。
gcc和g++编译的过程和选项是一样的。
下面写一个包含宏,头文件,注释,条件编译的代码:
对代码进行编译,gcc test.c
如果要指定输出的可执行文件文件名需要 -o选项:gcc test.c -o mytest,-o的含义是,将程序处理后的结果写入mytest中。
这里gcc的编译是一步到位,想要得到预处理的结果,需要用到-E选项:gcc -E test.c -o test.i
-E的意思是:进行程序翻译的过程中,预处理完成就停下来。这里不带-o会把处理的结果显示到界面上,不方便查看。
vim打开test.c,进入底行模式,输入vs test.i就能比较两种有什么不同。
图示框出的部分即头文件所在路径。头文件要被拷贝进test.i中,就必须能被系统找到,要找到就必须知道头文件所在路径。
由图可知宏替换,注释,条件编译,头文件展开的效果
Ctrl + w + w可以在两个窗口之间来回切换。
下面来看一下系统的头文件:
注意:是usr路径不是user路径
所以,写代码不是包含一下头文件就完了,首先要有头文件,其次要能找到头文件。之前安装vs2019时,除了安装编译器,还要安装编译器所需要的库,也就是说老的编译器不支持新语法是因为附带安装的库中,没有新语法的库。
任何编译器都必须通过一定方式,知道包含的头文件所在路径。
在预处理完后,test.i中还是C语言,也就是说,预处理只是进行文本层面的操作。目的是为了编译更顺畅。
编译的作用:将C语言翻译成汇编语言。
关于汇编语言,不需要了解太多,只需要知道,和C语言不一样就行了。
得到编译的结果要使用-S选项:gcc -S test.i -o test.s
-S的含义:对文件进行处理的过程中,编译完成后就停下来。汇编语言的后缀是.s。
汇编的作用:将汇编语言文件翻译成二进制文件,更准确的说法是,可重定位的二进制文件。以.o或.obj结尾。
这个重定位和重定向(>、<、>>)没有关系。
查看汇编的结果需要使用-c选项:gcc -c test.s -o test.o
-c的含义:对文件进程处理的过程中,会汇编完成后就停止。
这里已经变成二进制文件了
但是仍然无法执行,即使加上x权限后依然无法执行。
原因很简单,虽然转换成二进制后可以被系统识别了,但是其中的一些函数符号,还没有和库关联起来
包含头文件是为了代码能使用头文件中包含的方法(函数)。
而在进行-E -S -c操作的对象始终是test.c,也就是说,从头到尾,只进行了test.c代码的编译。比如printf,包含的头文件,传了参数,但printf是如何实现的?printf实现的操作没有写在test.c中。printf的实现实际上是由C语言的库完成的,但刚才的过程中只编译了test.c的代码。
所以代码中需要的printf实现在哪里?代码中使用的printf如何和printf的实现产生关联?
头文件中是不包含所有函数的实现的,函数的实现存放在头文件所对应的库中。包含头文件只是方便编译。
printf的实现在C语言的标准库中,标准库一般在/lib64/libc*路径下的第一个文件中
只要调用了函数,就还需要最后一步:将使用的printf函数和库中的printf函数通过链接操作关联起来。才能实现可执行程序。
gcc不用添加任何选项,系统就会自动到特定路径下搜索所要使用的库
gcc test.o -o mytest的隐含操作就是链接自己和程序和库,形成可执行程序。
使用ldd指令可以查看可执行程序所依赖的库。
libc.so.6是一个链接文件,指向C语言标准库
Linux平时使用的命令也可能是由C语言写的,可以查看一下。
还有其他的很多文件都会用到C语言标准库。如果删除C语言标准库,那么很多文件和命令都会无法实现。
如果需要记忆预处理,编译,汇编指令的选项,有一个简单的记忆技巧:-E-S-c和esc键只有大小写的区别。形成的对应文件后缀合在一起则是iso,国际标准化组织的简称,也是镜像文件的后缀。
在最开始的学习中,包含头文件被认为是函数声明;在编写代码的时候,比如输入一个函数,会有语法提示,本质上就是通过对头文件进行搜索来完成语法提示的。
而库文件中则存在函数的实现,以供链接,形成自己的可执行程序。
动态库和静态库
动态库和静态库的内容也很多,先基本的认识一下
Linux下动态库后缀:.so;Windows下动态库后缀:.dll
Linux下静态库后缀:.a;Windows下静态库后缀:.lib
下面用一个故事简单介绍一下动态链接,静态链接:
假设你是一个科研人员,你所在的科研机构有一台超级计算机,在进入科研机构的时候,你就被告知超级计算机的位置。一天的日程安排如下:起床,吃饭,做实验,收集数据,用超级计算机分析数据,写报告,和同事聊天……在分析数据之前的行为自己可以独立完成,要分析数据,必须借助于超级计算机,好在知道超级计算机的位置,找到超级计算机,找出对应实验的分析方法,完成数据分析后,分析数据的任务完成,再返回到日程安排中,进行下一项任务。
一天的日程安排就相当于自己写的代码,分析数据就相当于某个需要动态库的函数。在还没有分析数据前就已经知道动态库的位置,这叫做将函数和动态库关联起来,也就是链接的过程(链接不是执行,链接是产生关系)。当开始执行代码时,执行到需要动态库的函数时,就跳转库中(超级计算机),找到对应的函数实现(找出对应实验的分析方法) ,执行完毕后再返回,继续向后运行。
自己的日程行为就相当于自己写的代码,超级计算机中的实验分析方法就相当于库提供的函数实现,这种链接方式就叫做动态链接。动态链接的本质就是,在链接的时候,把需要的函数和库中的位置通过地址的方式关联起来。
科研机构中,需要使用超级计算机的不止一人,超级计算机是被所有科研人员所共享的。类比到系统当中,动态库就相当于超级计算机,被系统中所有文件和命令所共享。
静态链接本质上就是将库中的相关代码直接拷贝到相关程序中。比如有十个printf,每个printf都要把实现自己拷贝一份。静态链接后的程序,只要编译通过,就可以不要库,库只会使用一次。
动态链接的优缺点
优点:大家共享一个库,可以节省资源
缺点:一旦库缺失,会导致几乎所有程序失效。
静态链接的优缺点
优点:不依赖任何库,程序可以独立执行。
缺点:浪费资源。
可以通过file指令查看可执行程序的构成:
从框出来的内容来看,mytest是一个可执行程序,通过动态链接完成。
默认情况下,生成的可执行程序是通过动态链接完成的。
如果想要通过静态链接生成可执行程序,那么需要使用-static选项:gcc test.c -o mytest2 -static
-static选项需要安装静态库
安装指令:yum install -y glibc-static,这是C语言的静态库
安装指令:yum install -y libstdc++-static,这是C++的静态库
在仅仅使用printf函数的情况下,动态链接和静态链接的可执行文件大小差了近100倍。
再对mytest2使用ldd指令,就会弹出mytest2不是动态可执行的,file mytest2可以看到,静态链接。
正常情况下,动态链接的使用情况更多。默认情况下使用的是动态链接,甚至默认状态下是不带静态库的。但是如果软件要安装到别人的系统下,而别人的系统没有安装对应的库,这时候静态库就是必要的了。进行动态链接使用动态库,进行静态链接使用静态库。