gcc/g++ 编译器
在此之前,我们了解了 linux 中编写代码的工具 —— vim。代码编写好之后,总得要运行吧?那么在 linux 中是如何运行编写好的代码文件呢?这就需要使用到 gcc/g++ 编译器了。
gcc 和 g++ 都是编译器,它们之间存在什么差异吗?
gcc 是一个 C 语言编译器,只能用来进行编译 C 语言
g++ 是一个 C++ 和 C 语言编译器
尽管 g++ 既可以编译 C 语言,也可以编译 C++,但是建议编译 C 语言就使用 gcc 编译器,编译 C++ 就使用 g++ 编译器,如果使用 g++ 去编译 C 语言,它是将 C 语言当作 C++ 去编译的
本文主要是以C语言程序作为讲解例子,所以编译器主要使用 gcc 。
gcc编译的指令格式:gcc [选项] 要编译的文件 [选项] [目标文件]
认识翻译四过程
之前曾多次提到程序的翻译过程:预处理,汇编,编译,链接。linux 中程序的翻译过程也分这四步,下面以 linux 的视角去认识程序翻译的四个过程。
预处理步骤主要的功能:1.头文件展开;2.去注释;3.宏替换;4.条件编译
创建一个 code.c 文件,文件中写入以下代码:

接下来使用 gcc 编译器编译 code.c 代码文件,若想要指定源代码编译之后的名称,可以使用选项 -o,指令为 gcc/g++ 要编译的文件 -o 目标文件。
演示:

以上可以发现,当使用编译器编译代码文件时,它是一步到位的,如果想要观察程序的翻译过程,一步到位显然是不方便我们观察的。我们可以使用特定的选项来观察指定的翻译的过程。
选项 -E 功能:让 gcc 在预处理结束后停止编译过程,指令格式为:gcc/g++ -E 代码文件,作用:将预处理的结果打印到显示器上。
一部分的显示结果如下图所示:

如果不想让预处理的结果打印到显示器上,可以使用选项 -o,指令格式为:gcc/g++ -E 待编译的文件 -o 目标文件.i (.i 文件为已经过预处理的 C 原始程序)。再输入 vim 目标文件.i 指令就可以观察源代码预处理的结果。
一部分的显示结果如下图所示:

接下来仔细的看看预处理的效果

对比源代码和预处理后的代码,我们可以观察到注释被裁掉了,宏被替换了,原先代码仅有16行,现在代码足足有800多行,800多行的内容都是头文件 stdio.h 展开后的结果。代码中包含的头文件在编译时,编译器会将头文件中的内容拷贝到对应的源文件内部,而这个拷贝的过程就叫做头文件展开。
接下来看看预处理对条件编译的效果,将 code.c 文件中的代码改成下图所示:

运行结果:

为什么结果是 hello linux 呢?因为没有定义 VERSION1 宏,在预处理时 #ifdef 后面的内容就被裁掉了。
对比源代码和预处理后的代码:

如果想要运行的结果为 hello world,可以定义 VERSION1 宏,代码如下所示:

运行结果:

对比源代码和预处理后的代码:

由以上观察的结果可以得出:条件编译的本质就是对代码做裁减。
我们可以使用条件编译防止头文件被重复包含(当然避免头文件重复包含最好使用 #pragam once)如下图所示:

#ifndef __CODE_H__ 表示:如果未定义 __CODE_H__ 这个宏,则编译下面的代码;第一次包含该头文件时,__CODE_H__ 未定义,于是定义它并继续编译头文件内容;后续再次包含时,因为 __CODE_H__ 已定义,所以跳过整个头文件内容。
编译:在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,在检查无误后,将编程语言(预处理后的结果)翻译成汇编语言。
使用 -S 选项来查看,功能为:程序开始翻译,将编程语言翻译成汇编语言之后就停下来,生成汇编代码。指令格式为:gcc/g++ -S 待编译的文件.i -o 目标文件.s(编译后的汇编文件规定以 .s 为后缀)。
演示:

问题:为什么要将C语言翻译成汇编语言?因为计算器只能识别二进制文件。
我们知道在计算器诞生之初,都是使用打孔纸带进行二进制编程,打孔纸带上都是指令和数据,使用打孔纸带进行编程容错率低,且可读性差,为此消耗大量时间发明了汇编语言。二进制不需要编译器,但是汇编语言需要编译器,因此编译汇编语言的编译器就诞生了,这个编译器的功能是将汇编语言转换成计算机可识别的二进制。之后在汇编语言上加以改进,C语言就诞生了。C语言发明之后,需要编译器编译C语言编写的代码。
那么这个编译器是直接将C语言翻译成汇编语言还是二进制?
答案是:将C语言翻译成汇编语言,再由汇编语言翻译成二进制。
每发明一门语言就得有对应的编译器,我们学习语言语法的本质是在学习编译器编译语言的规则。既然是一门语言就得有对应的编译器,那么是先有语言还是先有对应的编译器?在发明一门语言之前,会草拟一个版本,设计出配套的编译器后,再优化草拟的版本。在理论上,先有语言;在实践上,先有编译器。所以从不同的角度来看,先有语言或先有编译器都是正确的。
汇编:将汇编文件翻译成二进制文件(.o为后缀)【二进制文件全称可重定位目标可执行文件】
指令格式:gcc/g++ -c 文件名.s -o 文件名.o,-c 选项的功能:程序开始翻译,将汇编语言翻译成为二进制之后就停下来。
演示:

打开 code.o 文件,里面都是二进制。如下图所示:

问题:先有汇编语言还是先有用汇编语言编写的汇编编译器?
编译器是一款软件,汇编语言发明之后,用汇编语言编写汇编编译器(一串串代码),编写好之后需要编译这款这一串串代码,然而没有编译器。虽然没有用汇编语言编写的汇编编译器,但是还有二进制编程呀。汇编语言是在二进制编程的基础上诞生的,可以使用二进制编程编写好一款编译器,再用它编译汇编语言编写的汇编编译器(一串串代码),这样就可以使用由汇编语言编写的编译器编译汇编语言了,之后便可以抛弃由二进制编程编写的编译器,这种情况称之为编译器自举/语言自举。所以先有汇编语言,再有用汇编语言编写的汇编编译器。
我们知道 code.o是二进制文件,如果想要用编译器编译 code.o(没有可执行权限需要自己加),结果是什么?

结果显示不能编译二进制文件,为什么不能执行二进制文件?这就得要认识链接。
链接:生成可执行文件/库文件
指令格式:gcc 待链接的文件.o -o 目标文件(可执行文件),-o 选项的功能:文件输出到文件。
在我们编写代码时,我们可以调用任意函数,流插入流提取函数等等,函数的实现是在C标准库中实现的。用户编写的程序中使用的函数,与标准库中函数的实现相关联,这就是链接。链接的本质:对应的目标二进制文件中应用到的函数声明,在文件并没有实现,需要用到库中的函数声明,与库产生关联。
如果想要查看可执行文件依赖了哪些库,可以输入指令:ldd 可执行文件名。

可以找到 libc.so.6 文件,它在 lib64 目录下

上图中的 libc-2.17.so 就是C语言标准库。
使用C语言编写代码,必然要用到这三点东西:编译器,头文件,库文件
头文件提供声明,库文件提供实现
所以为什么 code.o 文件不能运行?因为在 code.o 文件中我们用到的库方法,只有声明没有定义,而 gcc 知道库和头文件在哪里。
初步认识库
库分为静态库和动态库
在linux系统中,静态库以 .a 为后缀,动态库以 .s o为后缀 。命名规则:libname.a/so
在 Windows 系统中,静态库以 .lib 为后缀,动态库以 .dll 为后缀
动态链接:代码与动态库产生的关联的过程,链接的是关联关系,更改的是地址库的地址
一旦库缺失,所有相关程序就无法运行了
静态链接:把我们程序所需要的库中的方法直接拷贝到当前的程序中
静态库和静态链接一旦链接成功,可执行程序就不再需要静态库
动态库的优缺点
优点:节省内存
缺点:慢,编译完成依旧依赖动态库
静态库的优缺点
优点:不需要库跳转,一旦编译成功不依赖静态库
缺点:可执行程序体积较大
gcc 编译默认采用动态链接的方式,编译程序,通常使用动态库和动态链接。
ldd 可执行文件名:查看可执行程序依赖的库
file 可执行文件名:查看可执行程序的链接方式
dynamically Linked:动态链接
如果想强制使用静态链接的方式,必须满足以下条件:
1. 安装了对应语言的静态库(默认没有安装)
安装指令:
CentOS:sudo yum install glibc-static(C语言) sudo yum install libstdc++-static(C++)
Ubuntu:sudo apt install libc6-dev(C语言) sudo apt install libstdc++-static(C++)
2. 输入指令的后面加上 -static 选项
演示:
可以发现静态链接和动态链接编译出来的文件的大小相差两个数量级。
查看 code-static 可执行程序调用了哪些库
statically linked:静态链接



1134

被折叠的 条评论
为什么被折叠?



