Linux下的编译器gcc/g++
文章目录
- 1.gcc/g++概念
- 2.程序的翻译
- 2.1 预处理
- 条件编译
- 代码裁减应用--防止头文件的重复定义
- 2.2 编译
- 2.3 汇编
- 2.4 链接
- 3.库
- 3.1 静态库
- 3.2 动态库
- 3.3静态库的安装、使用和与动态库的对比
- 求三连!
1.gcc/g++概念
gcc:C编译器,只能用来进行编译C语言
g++:是一个C++/C语言编译器
2.程序的翻译
以一下程序为例:

gcc编译选项:
通过gcc 要编译的文件 -o 编译后的名字
也可以直接gcc 要编译的文件默认生成的是a.out的可执行程序。

命令执行
输入./test.exe 除了系统指令都要声明程序所在路线.就表示在当前目录下

但实际上,程序的翻译一共分4步:
- 预处理
- 编译
- 汇编
- 链接
2.1 预处理
预处理有4个步骤:
- 头文件展开(“头文件展开” 指将代码中 #include 的头文件内容直接嵌入到源文件中,生成预处理后的文件)
- 去注释(字面意思)
- 宏替换(将define定义的x 替换成123)
- 条件编译(后面有单独讲解)
预处理:
gcc -E test.c -o test.i
表示test.c处理为test.i文件 -E表示执行程序的翻译,做完对源文件的预处理,就停下来!
此时我们打开test.c和test.i进行对比

我们发现 宏被替换 注释消失 头文件被展开
条件编译
我们新创建了一个文件 写入了以下代码

直接编译运行后的结果

因为我们没有定义VERSION1的宏 我查看 与编译的详细情况

我们发现VERSION1部分的代码直接裁剪没了
当我们定义宏时

然后我保存并对文件重新编译运行 我们发现此时执行的是version1的代码

我们预编译下 发现version2的代码被裁减了

为什么要进行代码裁减?
假如一个软件 有收费版和免费版 为了避免维护一个项目两次 可以通过代码裁减进行功能区分 同时也可以防止头文件被重复包含 增加内存开销
代码裁减应用–防止头文件的重复定义
前提知识:
保护宏能生效,关键在于 #define XXX 定义的宏是 全局有效的(在预处理阶段,宏的作用域是 “整个编译单元”,即单个 .cpp 文件及其所有包含的头文件 宏定义只会生效一次)。
举个例子:
//头文件
#ifndef MYMATH_H // 步骤1:判断宏是否未定义
#define MYMATH_H // 步骤2:若未定义,定义该宏// 核心内容(声明)
int add(int a, int b);
struct Point { int x; int y; };#endif // 步骤3:结束条件判断//源文件
#include "mymath.h" // 第一次包含
#include "mymath.h" // 第二次包含int main() {add(1, 2);return 0;
}
预处理阶段的执行过程(关键!)
预处理器会逐行扫描 源文件,遇到 #include 就替换,遇到 #ifdef 就判断:
第一步:处理第一次 #include "mymath.h"
预处理器把#include "mymath.h"替换为 mymath.h 的全部内容,此时开始解析保护宏:
执行 #ifndef MYMATH_H:检查 “宏 MYMATH_H 是否存在”—— 此时全局未定义,条件为 真;
执行 #define MYMATH_H:定义宏 MYMATH_H(相当于在 “全局宏表” 中记录:MYMATH_H 已存在);
保留 #ifndef 和#endif之间的核心内容(add 声明、Point 结构体);
执行 #endif:结束条件判断。
第一次替换后,源文件 的预处理中间代码为:
// 从第一次 #include 展开的内容
int add(int a, int b);
struct Point { int x; int y; };
第二次 #include “mymath.h” 待处理
第二步:处理第二次 #include "mymath.h"
再次替换 #include "mymath.h" 为头文件内容,解析保护宏:
执行 #ifndef MYMATH_H:检查 “宏 MYMATH_H 是否存在”—— 第一步已定义,条件为 假;
预处理器直接 删除 #ifndef 和 #endif 之间的所有代码(包括 #define MYMATH_H 和核心内容);
执行 #endif:结束条件判断。
第二次替换后,源文件 的最终预处理代码为:
// 仅保留第一次展开的内容
int add(int a, int b);
struct Point { int x; int y; };int main()
{add(1, 2);return 0;
}
无论嵌套多少层,只要宏被定义过,后续所有包含该头文件的操作都会触发 “跳过逻辑”—— 这就是 “嵌套包含安全” 的原理。
当然还有方案2 在源文件开头加入
#pragma once
核心原理:
直接告诉编译器:“该头文件只包含一次,无论 #include 多少次,都只编译一次”。
不过该方法存在局限性:
特殊场景失效:若同一头文件被放在不同路径(如
./include/mymath.h和./src/mymath.h内容相同但路径不同),#pragma once会认为是两个文件,导致重复包含(保护宏无此问题)。
2.2 编译
编译:把C语言翻译成汇编语言
编译:
gcc -S test.i -o test.s
表示test.i处理为test.s文件 -S 表示执行程序的翻译,做完对做完预处理文件中的C语言翻译为汇编语言,就停下来!

为什么要把C语言翻译成汇编语言?
因为计算机是二进制计算机 通过二进制指令集进行运行程序 而翻译语言的本质:转成CPU能够识别的指令集

而二进制效率太低 于是就产生了汇编语言 汇编语言又太麻烦 就诞生了编译器 而二者之间的翻译 就依靠于编译器了。
出于历史问题 汇编翻译成二进制已经很成熟 将接近自然语言的C、Java直接翻译成二进制难度太大 不如直接站在巨人肩膀上 先翻译成汇编 汇编再翻译成二进制。
2.3 汇编
汇编:把汇编语言翻译为二进制文件
汇编:
gcc -c test.s -o test.o
表示test.s处理为test.o文件 -c 表示执行程序的翻译,做完对做完编译文件中的汇编语言翻译为二进制文件,就停下来!

二进制文件是没法直接运行的 必须经过链接生成可执行程序后才行。
2.4 链接
汇编:把汇编语言翻译为二进制文件
汇编:
gcc test.o -o test
表示test.o处理为test文件 将test.o链接生成可执行程序

为什么需要链接?
因为我们所包含的二进制头文件展开 展开的是申明 而实现是存在于库中的,这就衍生出我们库的概念了。
3.库
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c(源文件)文件会形成一个*.o(二进制)文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。静态链接的缺点很明显:
- 浪费空间:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;
- 更新比较困难:因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
动态链接的出现解决了静态链接中提到问题。动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
动态链接其实远比静态链接要常用得多。比如我们查看下 test 这个可执行程序依赖的动态库,会发现它就用到了一个动态链接库:

# ldd命令用于打印程序或者库文件所依赖的共享库列表。1
在这里涉及到一个重要的概念:库
- 我们的C程序中,并没有定义
“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实“printf”函数的呢? - 最后的答案是:系统把这些函数实现都被做到名为
libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用.
Linux下,动态库XXX.so, 静态库XXX.a
Windows下,动态库XXX.dll, 静态库XXX.lib
3.1 静态库
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为 “.a”
说人话就是把里面库函数中声明的实现全部拷贝到.o文件里面生成了可执行程序。
3.2 动态库
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为 “.so”,如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件。
说人话就是运行时候通过链接文件把.o的函数声明与库里面函数实现进行匹配链接,这样程序就可以执行了。
注意:gcc默认生成的二进制程序,是动态链接的。(通过file命令查询)

3.3静态库的安装、使用和与动态库的对比
在云服务器中 我们的静态库是默认没有安装的:
//centos
yum install glibc-static libstdc++-static -y
//ubuntu
apt install libstdc++-11-dev
我们可以通过
gcc test.c -o test.static -static的方式进行静态编译 而-static表示强制gcc采用静态链接的方式

我们对比test和test.static文件大小发现差了接近十倍 这就是动静态链接的显著差异。
