目录
一.认识gcc/g++
二.编译的四个过程
2.1预处理(进行宏替换)
编辑
2.2编译(生成汇编)
2.3汇编(生成机器可识别的代码)
2.4链接(生成可执行文件和库文件)
2.5.动态链接和静态链接
三.库的理解
3.1动态库
3.2静态库
3.3故事理解动静态库
3.4动静态库的优缺
四.问题归纳
问题一:条件编译与if语句的区别
一.认识gcc/g++
GCC(GNU Compiler Collection,GNU 编译器套件),是 Linux 平台最主流的源代码编译器。 简单理解:把.c/.cpp文本代码翻译成电脑能执行的程序。
1. 两者区别
- gcc:主打编译C 语言(
.c),也可编译 C++,但不会默认链接 C++ 标准库 - g++:主打编译C++ 语言(
.cpp)g++ 本质内部还是调用 gcc,只是自动启用 C++ 相关规则、自动链接
libstdc++
文件后缀自动规则:
.c→ gcc 默认当作 C 代码.cpp/.hpp→ gcc/g++ 当作 C++ 代码
2. 基础编译命令(结合你 vim 写的 test.c)
C 语言示例 test.c
gcc test.c -o testtest.c:源代码文件-o test:指定输出可执行程序名字为test
运行程序:
./test如果是 C++ 代码 test.cpp
g++ test.cpp -o test3. 完整编译四大阶段(重点考点)
- 预处理:展开头文件
#include、宏定义#define - 编译:C/C++ 代码 → 汇编代码
- 汇编:汇编代码 → 机器目标文件
.o - 链接:目标文件 + 系统库 → 最终可执行程序
二.编译的四个过程
预处理 (.c→.i) → 【编译 (.i→.s)】 → 汇编 (.s→.o) → 链接 (.o→可执行程序)
2.1预处理(进行宏替换)
预处理是gcc/g++ 编译的第 1 步,在真正编译代码之前执行,由预处理器 cpp 完成。
所有以 #开头的指令,都是预处理指令。
问:
#include的原理是什么?答:预处理器将头文件内容原样嵌入到源文件中。
2.2编译(生成汇编)
输入:预处理完成后的文件.i(纯 C 文本,无#include、无注释、宏全部展开) 输出:汇编代码文件.s
核心工作:
- 词法分析:拆分代码,识别关键字、变量、运算符、字符串;
- 语法分析:检查 C 语法是否合法(少分号、括号不匹配这里报错);
- 语义分析:检查类型是否匹配(比如 int 赋值给指针、函数参数错误);
- 优化:对代码做简单优化;
- 生成汇编代码:把合法 C 代码,翻译成对应 CPU 架构的汇编指令。
✅ 关键区分: 编译:C 语言代码 →汇编代码(人类可读文本 .s)汇编阶段:汇编代码 →二进制目标文件(机器码 .o)
问:语法错误在哪个阶段检测?
答:编译阶段。预处理只是文本处理,不会检查语法。
2.3汇编(生成机器可识别的代码)
输入:汇编代码文件
.s(文本,汇编指令)工具:汇编器
as输出:目标文件
.o(二进制文件)工作内容: 将人类可读的汇编指令(
push、mov、call)翻译成CPU 识别的二进制机器码。⚠️ 重点:
.o是二进制,但无法直接运行! 内部缺少函数地址解析、标准库代码,只是零散的机器码片段。
2.4链接(生成可执行文件和库文件)
输入:一个或多个.o目标文件 + 系统库文件
输出:可执行程序(Linux 下没有后缀,例如 test)
两大核心工作
- 符号解析
.o文件里用到的外部函数(比如printf)只留下名字,没有真实内存地址。 链接器找到每个符号对应的实现,绑定地址。
你的代码调用
printf,printf的实现不在 test.o 中,在系统 C 标准库shturl.。
- 重定位调整代码、变量的地址偏移,把零散多个目标文件、库整合到统一地址空间。
2.5.动态链接和静态链接
发生在链接阶段,把你的
.o目标文件和库结合生成可执行程序。Linux:静态库
.a;动态库.so
三.库的理解
3.1动态库
把一堆函数编译成独立二进制库文件,不把库代码复制到你的程序里面。 程序运行的时候,才去磁盘加载这个.so文件,调用里面的函数。
对比静态库:静态链接会把库代码拷贝复制进可执行程序。
核心特点
- 编译链接时,只记录依赖关系,不拷贝库代码你的可执行程序体积很小。
- 运行时才加载库文件程序启动,操作系统找到
.so,把库加载进内存,程序调用库里面函数。 - 共享(共享库名字由来)多个程序同时使用同一个动态库,内存里只加载一份,节省内存。
前提:你的程序是动态链接生成的可执行文件。 编译链接阶段仅仅在程序内部写下记录:我需要
libc.so.6、libxxx.so,没有把库代码拷贝进来。 真正去找库发生在程序运行时刻,由 Linux 的「动态链接器」完成。
是谁负责找库?
ld‑linux‑x86‑64.so.2,这就是系统的动态链接器(加载器),它本身也是一个 so。 当你敲下./a.out运行程序时:
- 操作系统内核先把你的可执行程序加载进内存
- 内核发现:这个程序是动态链接程序,于是启动动态链接器 ld‑linux
- ld‑linux 专门负责:寻找、加载所有需要的动态库、解析函数符号地址
📋完整分步流程
第 1 步:读取程序内部的依赖清单
动态链接器打开你的可执行程序,读取里面保存的依赖列表。 比如里面记录:本程序依赖libc.so.6。
这个清单是编译链接阶段 gcc 写进去的。
第 2 步:按规则搜索磁盘,寻找对应的.so文件
ld‑linux 按照固定顺序搜索目录,顺序不能乱:
- 程序本身里面写的 rpath(编译时写入的路径,较少用)
- 环境变量
LD_LIBRARY_PATH(我们测试经常临时设置这个) - 缓存:读取系统缓存文件
/etc/ld.so.cache(最重要!)这是一个预生成的二进制缓存,保存系统所有动态库 → 对应文件路径映射,查找速度极快。
- 默认系统库目录:
/lib、/usr/lib
⚠️注意:默认不会搜索当前工作目录!这就是为什么你自己写的 libfunc.so 放在当前文件夹,直接
./main会报找不到库,必须设置export LD_LIBRARY_PATH=.。
第 3 步:找到 so 文件,把动态库加载进内存
磁盘上找到libxxx.so,把这个 so 文件映射加载到进程虚拟地址空间。
如果多个程序用同一个 so,操作系统在物理内存只放一份,多个进程共享。
第 4 步:符号重定位(填函数地址,最核心)
还记得编译链接阶段,你的.o文件中调用printf,当时不知道 printf 真实地址,只留了一个空坑。
动态链接器现在:
- 在已经加载进来的
libc.so.6里面找到printf函数真实内存地址 - 把这个地址回填到你程序预留的空坑位置。
这个动作就叫重定位。 回填完成之后,你的程序才真正知道
printf到底在哪,才可以调用 printf。
第 5 步:全部库加载、地址回填完毕,你的主程序正式开始运行
3.2静态库
Linux 静态库后缀:.a,Windows 静态库.lib
本质:把一堆
.o目标文件打包压缩到一个归档文件里面,不是可执行程序,也不是机器码直接运行的文件,只是.o的集合包。
静态库完整流程
- 编写库源码 → 汇编生成
.o - 使用
ar工具,把多个.o打包成为静态库libxxx.a - 编译链接阶段:链接器从
.a包里把需要的.o目标文件拷贝复制出来,合并到你的可执行程序。
⚠️重点:运行阶段不再需要
.a文件!代码已经复制进程序内部。
静态库libxxx.a本质是一堆.o目标文件的压缩包。 当链接器遇到-lfunc:
- 打开
libfunc.a归档包; - 在包里面找出主程序需要用到的那个
.o目标文件; - 把这个
.o完整拷贝出来,合并拼装到最终可执行程序内部; - 符号解析、重定位全部在链接阶段一次性做完。
⚠️注意:不是把整个
.a全部复制,只拷贝你程序用到的.o模块。
链接结束: ✅ 函数机器码已经实实在在复制进可执行文件。 ✅ 所有函数地址全部算好,填完毕。 ❌ 之后运行,完全不再访问 libfunc.a。
静态库.a | 动态库.so | |
|---|---|---|
| 本质 | ar归档:零散.o的压缩包,没有经过链接 | 经过gcc -shared链接生成的完整 ELF 共享二进制 |
| 内部 | 保留一份份原始独立.o | 已经合并,不存在原始.o |
| 生成工具 | ar rcs(只是打包,不链接) | gcc -shared(执行链接) |
| 使用时机 | 编译链接阶段抽取.o拷贝到程序 | 运行时操作系统加载整个 so 文件到进程内存 |
误区:“动态库就是另一种打包的.o 集合” 错误。
静态库 = 打包的原材料(.o);
动态库 = 已经加工装订完成的成品书。
3.3故事理解动静态库
背景设定
小明要写一份项目报告(你的 C 语言源代码),报告里面很多地方要用到「标准计算公式」(库函数:printf、加法工具等)。 计算公式写在一本工具书《公式大全》(库)。
工具书有两种版本:纸质复印本(静态库 .a)、图书馆公共藏书(动态库 .so)
📕静态库模式(静态链接)
链接 = 复印整本工具书,装订进自己的报告里
小明写报告的时候,为了方便,直接把《公式大全》整本书全部复印,把复印页全部钉进自己的报告文档里面。
✅好处:
- 这份报告本身什么都齐全。把报告拷贝给任何人,不需要再找原版工具书,拿过去直接看直接用。(程序不依赖外部库,移植方便)
- 使用的时候不用跑图书馆,直接翻自己报告里面的复印页面。
❌坏处:
- 报告变得很厚,体积巨大。(可执行文件体积大)
- 如果小红、小刚也做同样的报告,每个人都复印一份完整《公式大全》。每个人的报告里面都有一模一样的公式副本,大量纸张浪费。(内存冗余,每个程序都自带一份库代码)
- 如果出版社更新了《公式大全》出第二版。小明手里报告依旧是旧的复印版本,想要新公式,必须重新复印,重新装订报告。(库更新,程序必须重新编译链接)
对应命令:gcc test.c -o app_static -static
📚动态库模式(动态链接,Linux 默认)
链接:不在报告里抄公式,只在报告上写一行备注:「需要,请去图书馆借阅《公式大全》」
小明写报告,不复印任何公式,只在报告上标记:我需要用到《公式大全》这本书。
- 写报告(编译链接阶段):只登记我需要哪本书,不把书拿过来。
- 等到真正要阅读这份报告的时候(程序运行时),才派人去图书馆,把公共的《公式大全》借过来使用。
✅好处:
- 小明的报告薄薄的,体积很小。(可执行文件体积小)
- 小明、小红、小刚,所有人的报告都共用图书馆同一本原版《公式大全》。图书馆只存放一本,节省大量纸张。(内存中只加载一份动态库,多进程共享,节省内存)
- 出版社更新《公式大全》第二版,直接替换图书馆那本书就完事。所有人下次打开报告,自动使用新版公式,不需要每个人重新修改自己的报告。(替换 so 库,程序无需重编译)
❌坏处:
- 报告离开了图书馆就废掉!如果图书馆把《公式大全》弄丢了,这份报告直接没法看。(运行机器必须存在对应的
.so动态库,缺失库直接运行失败) - 每次看报告,要多跑一趟图书馆借书,多一点小小的时间开销。(程序启动有加载库的开销)
对应命令:gcc test.c -o app_dynamic默认动态链接。
故事对应考点小结
- 静态链接:链接阶段复制库代码到程序内部;自带全部内容,体积大,不依赖外部文件。
- 动态链接:链接只做记录,运行时才加载公共库;体积小,多程序共享库;依赖外部 so 文件。
一句话记忆
- 静态:把书抄进自己本子,走到哪带到哪,本子很厚。
- 动态:书放在公共图书馆,本子只写借书备注,用的时候去借。本子薄,但不能没有图书馆。
3.4动静态库的优缺
| 对比维度 | 静态库.a | 动态库.so |
|---|---|---|
| 文件本质 | .o目标文件归档包,不做链接 | 经过链接生成完整 ELF 共享二进制 |
| 代码处理 | 链接时拷贝用到的.o到程序内部 | 链接仅记录依赖;运行时加载完整 so |
| 运行依赖 | 不依赖.a,链接完可删除静态库 | 必须存在.so库文件 |
| 程序大小 | 大 | 小 |
| 内存表现 | 每个进程一份库代码,内存冗余 | 多进程共享内存一份库,节省内存 |
| 库升级 | 库更新,程序必须重新编译链接 | 替换 so 文件,程序无需重编译 |
| 制作工具 | ar rcs | gcc -fPIC -shared |
| 是否需要‑fPIC | 不需要 | 必须 |
| 启动性能 | 略快 | 略有加载开销 |
| 部署移植 | 简单,直接拷贝程序 | 复杂,要保证目标机器有对应 so |
Q:静态库程序运行的时候会读取 libxxx.a 吗?
A:不会。链接阶段已经把代码拷贝到可执行程序,运行和
.a无关。Q:我把静态库删掉,程序还能跑吗?
A:可以正常运行。
动静态库判断题(附答案解析)
判断对/错
- 静态库
.a是经过链接生成的ELF可执行文件。()- 使用静态库的程序运行时,仍然需要本机存在
.a文件才能正常运行。()- 静态链接时,链接器会从
.a归档包提取用到的.o,拷贝合并进可执行程序。()- 动态库
.so只是多个.o简单打包,没有做链接处理。()- 动态链接程序,编译链接阶段会把so库的机器码复制到可执行文件内。()
- 多个进程使用同一个动态库,物理内存中只会保存一份该动态库代码。()
- 更新动态库,替换新版
.so后,原有程序必须重新编译链接才能生效。()- 制作动态库必须加
-fPIC参数,制作静态库不需要。()ld‑linux‑x86‑64.so.2动态链接器只负责静态链接程序。()- 编译时
‑L.参数,可以让程序运行时自动在当前目录找so动态库。()答案+解析
- ❌错 解析:
.a是ar打包的.o归档包,没有做链接;.so才是链接完成的ELF共享文件。- ❌错 解析:静态库只参与链接阶段;链接完成,
.a可以直接删除,运行完全不需要。- ✅对 解析:静态库核心行为,抽取需要的目标文件拷贝到可执行程序。
- ❌错 解析:
gcc -shared会执行链接,把多个.o合并生成完整so,不是简单打包。- ❌错 解析:动态链接不拷贝库代码,仅记录依赖关系;运行时才加载so。
- ✅对 解析:动态库核心优势,内存共享,节约物理内存。
- ❌错 解析:动态库直接替换so即可,不需要重编译程序;静态库更新才需要重编译。
- ✅对 解析:‑fPIC生成位置无关代码,动态库必备;静态库无此要求。
- ❌错 解析:ld‑linux是运行时动态链接器,只管动态链接程序;静态链接程序不使用它。
- ❌错 解析:
‑L仅作用于编译链接阶段;运行时查找so看LD_LIBRARY_PATH、ld.so.cache等,和‑L无关。
四.问题归纳
问题一:条件编译与if语句的区别
#ifdef:预处理指令,前面带#,发生在预处理阶段(编译之前)
if:C 语言语句,发生在程序运行阶段
| 对比项 | 条件编译#ifdef / #endif | 普通if‑else |
|---|---|---|
| 执行时机 | 预处理阶段,编译之前 | 程序运行的时候 |
| 处理方式 | 不满足条件的代码,直接被删掉,不会进入最终程序 | 所有代码全部编译进可执行文件,运行做判断 |
| 生成程序内容 | 被裁剪的代码,程序里面根本不存在 | 全部代码都存在,只是不一定执行 |
| 占用程序体积 | 无效代码直接丢弃,不占体积 | 全部代码都占程序体积 |
| 判断依据 | 宏是否定义,预处理时就确定 | 运行时变量、表达式的值 |
判断:使用
#ifdef,条件不成立,里面的代码只是不会执行,仍然存在可执行文件中。(❌错)解析:预处理直接删掉,产物中不存在该代码。