☰
Linux基本开发工具:编译器gcc/g++
2026/10/31 1:46:42 网站建设 项目流程

目录

一.认识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 test
  • test.c:源代码文件
  • -o test:指定输出可执行程序名字为test

运行程序:

./test

如果是 C++ 代码 test.cpp

g++ test.cpp -o test

3. 完整编译四大阶段(重点考点)

  1. 预处理:展开头文件#include、宏定义#define
  2. 编译:C/C++ 代码 → 汇编代码
  3. 汇编:汇编代码 → 机器目标文件.o
  4. 链接:目标文件 + 系统库 → 最终可执行程序

二.编译的四个过程

预处理 (.c→.i) → 【编译 (.i→.s)】 → 汇编 (.s→.o) → 链接 (.o→可执行程序)

2.1预处理(进行宏替换)

预处理是gcc/g++ 编译的第 1 步,在真正编译代码之前执行,由预处理器 cpp 完成。

所有以 #开头的指令,都是预处理指令。

问:#include的原理是什么?

答:预处理器将头文件内容原样嵌入到源文件中。

2.2编译(生成汇编)

输入:预处理完成后的文件.i(纯 C 文本,无#include、无注释、宏全部展开) 输出:汇编代码文件.s

核心工作:

  1. 词法分析:拆分代码,识别关键字、变量、运算符、字符串;
  2. 语法分析:检查 C 语法是否合法(少分号、括号不匹配这里报错);
  3. 语义分析:检查类型是否匹配(比如 int 赋值给指针、函数参数错误);
  4. 优化:对代码做简单优化;
  5. 生成汇编代码:把合法 C 代码,翻译成对应 CPU 架构的汇编指令。

✅ 关键区分: 编译:C 语言代码 →汇编代码(人类可读文本 .s)汇编阶段:汇编代码 →二进制目标文件(机器码 .o)

问:语法错误在哪个阶段检测?

答:编译阶段。预处理只是文本处理,不会检查语法。

2.3汇编(生成机器可识别的代码)

输入:汇编代码文件.s(文本,汇编指令)

工具:汇编器as

输出:目标文件.o(二进制文件)

工作内容: 将人类可读的汇编指令(push、mov、call)翻译成CPU 识别的二进制机器码。

⚠️ 重点:.o是二进制,但无法直接运行! 内部缺少函数地址解析、标准库代码,只是零散的机器码片段。

2.4链接(生成可执行文件和库文件)

输入:一个或多个.o目标文件 + 系统库文件

输出:可执行程序(Linux 下没有后缀,例如 test)

两大核心工作

  1. 符号解析.o文件里用到的外部函数(比如printf)只留下名字,没有真实内存地址。 链接器找到每个符号对应的实现,绑定地址。

你的代码调用printf,printf的实现不在 test.o 中,在系统 C 标准库shturl.。

  1. 重定位调整代码、变量的地址偏移,把零散多个目标文件、库整合到统一地址空间。

2.5.动态链接和静态链接

发生在链接阶段,把你的.o目标文件和库结合生成可执行程序。

Linux:静态库.a;动态库.so

三.库的理解

3.1动态库

把一堆函数编译成独立二进制库文件,不把库代码复制到你的程序里面。 程序运行的时候,才去磁盘加载这个.so文件,调用里面的函数。

对比静态库:静态链接会把库代码拷贝复制进可执行程序。

核心特点

  1. 编译链接时,只记录依赖关系,不拷贝库代码你的可执行程序体积很小。
  2. 运行时才加载库文件程序启动,操作系统找到.so,把库加载进内存,程序调用库里面函数。
  3. 共享(共享库名字由来)多个程序同时使用同一个动态库,内存里只加载一份,节省内存。

前提:你的程序是动态链接生成的可执行文件。 编译链接阶段仅仅在程序内部写下记录:我需要libc.so.6、libxxx.so,没有把库代码拷贝进来。 真正去找库发生在程序运行时刻,由 Linux 的「动态链接器」完成。


是谁负责找库?

ld‑linux‑x86‑64.so.2,这就是系统的动态链接器(加载器),它本身也是一个 so。 当你敲下./a.out运行程序时:

  1. 操作系统内核先把你的可执行程序加载进内存
  2. 内核发现:这个程序是动态链接程序,于是启动动态链接器 ld‑linux
  3. ld‑linux 专门负责:寻找、加载所有需要的动态库、解析函数符号地址

📋完整分步流程

第 1 步:读取程序内部的依赖清单

动态链接器打开你的可执行程序,读取里面保存的依赖列表。 比如里面记录:本程序依赖libc.so.6。

这个清单是编译链接阶段 gcc 写进去的。

第 2 步:按规则搜索磁盘,寻找对应的.so文件

ld‑linux 按照固定顺序搜索目录,顺序不能乱:

  1. 程序本身里面写的 rpath(编译时写入的路径,较少用)
  2. 环境变量LD_LIBRARY_PATH(我们测试经常临时设置这个)
  3. 缓存:读取系统缓存文件/etc/ld.so.cache(最重要!)

    这是一个预生成的二进制缓存,保存系统所有动态库 → 对应文件路径映射,查找速度极快。

  4. 默认系统库目录:/lib、/usr/lib

⚠️注意:默认不会搜索当前工作目录!这就是为什么你自己写的 libfunc.so 放在当前文件夹,直接./main会报找不到库,必须设置export LD_LIBRARY_PATH=.。

第 3 步:找到 so 文件,把动态库加载进内存

磁盘上找到libxxx.so,把这个 so 文件映射加载到进程虚拟地址空间。

如果多个程序用同一个 so,操作系统在物理内存只放一份,多个进程共享。

第 4 步:符号重定位(填函数地址,最核心)

还记得编译链接阶段,你的.o文件中调用printf,当时不知道 printf 真实地址,只留了一个空坑。

动态链接器现在:

  1. 在已经加载进来的libc.so.6里面找到printf函数真实内存地址
  2. 把这个地址回填到你程序预留的空坑位置。

这个动作就叫重定位。 回填完成之后,你的程序才真正知道printf到底在哪,才可以调用 printf。

第 5 步:全部库加载、地址回填完毕,你的主程序正式开始运行

3.2静态库

Linux 静态库后缀:.a,Windows 静态库.lib

本质:把一堆.o目标文件打包压缩到一个归档文件里面,不是可执行程序,也不是机器码直接运行的文件,只是.o的集合包。

静态库完整流程

  1. 编写库源码 → 汇编生成.o
  2. 使用ar工具,把多个.o打包成为静态库libxxx.a
  3. 编译链接阶段:链接器从.a包里把需要的.o目标文件拷贝复制出来,合并到你的可执行程序。

⚠️重点:运行阶段不再需要.a文件!代码已经复制进程序内部。


静态库libxxx.a本质是一堆.o目标文件的压缩包。 当链接器遇到-lfunc:

  1. 打开libfunc.a归档包;
  2. 在包里面找出主程序需要用到的那个.o目标文件;
  3. 把这个.o完整拷贝出来,合并拼装到最终可执行程序内部;
  4. 符号解析、重定位全部在链接阶段一次性做完。

⚠️注意:不是把整个.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)


📕静态库模式(静态链接)

链接 = 复印整本工具书,装订进自己的报告里

小明写报告的时候,为了方便,直接把《公式大全》整本书全部复印,把复印页全部钉进自己的报告文档里面。

✅好处:

  1. 这份报告本身什么都齐全。把报告拷贝给任何人,不需要再找原版工具书,拿过去直接看直接用。(程序不依赖外部库,移植方便)
  2. 使用的时候不用跑图书馆,直接翻自己报告里面的复印页面。

❌坏处:

  1. 报告变得很厚,体积巨大。(可执行文件体积大)
  2. 如果小红、小刚也做同样的报告,每个人都复印一份完整《公式大全》。每个人的报告里面都有一模一样的公式副本,大量纸张浪费。(内存冗余,每个程序都自带一份库代码)
  3. 如果出版社更新了《公式大全》出第二版。小明手里报告依旧是旧的复印版本,想要新公式,必须重新复印,重新装订报告。(库更新,程序必须重新编译链接)

对应命令:gcc test.c -o app_static -static


📚动态库模式(动态链接,Linux 默认)

链接:不在报告里抄公式,只在报告上写一行备注:「需要,请去图书馆借阅《公式大全》」

小明写报告,不复印任何公式,只在报告上标记:我需要用到《公式大全》这本书。

  • 写报告(编译链接阶段):只登记我需要哪本书,不把书拿过来。
  • 等到真正要阅读这份报告的时候(程序运行时),才派人去图书馆,把公共的《公式大全》借过来使用。

✅好处:

  1. 小明的报告薄薄的,体积很小。(可执行文件体积小)
  2. 小明、小红、小刚,所有人的报告都共用图书馆同一本原版《公式大全》。图书馆只存放一本,节省大量纸张。(内存中只加载一份动态库,多进程共享,节省内存)
  3. 出版社更新《公式大全》第二版,直接替换图书馆那本书就完事。所有人下次打开报告,自动使用新版公式,不需要每个人重新修改自己的报告。(替换 so 库,程序无需重编译)

❌坏处:

  1. 报告离开了图书馆就废掉!如果图书馆把《公式大全》弄丢了,这份报告直接没法看。(运行机器必须存在对应的.so动态库,缺失库直接运行失败)
  2. 每次看报告,要多跑一趟图书馆借书,多一点小小的时间开销。(程序启动有加载库的开销)

对应命令:gcc test.c -o app_dynamic默认动态链接。


故事对应考点小结

  1. 静态链接:链接阶段复制库代码到程序内部;自带全部内容,体积大,不依赖外部文件。
  2. 动态链接:链接只做记录,运行时才加载公共库;体积小,多程序共享库;依赖外部 so 文件。

一句话记忆

  • 静态:把书抄进自己本子,走到哪带到哪,本子很厚。
  • 动态:书放在公共图书馆,本子只写借书备注,用的时候去借。本子薄,但不能没有图书馆。

3.4动静态库的优缺

对比维度静态库.a动态库.so
文件本质.o目标文件归档包,不做链接经过链接生成完整 ELF 共享二进制
代码处理链接时拷贝用到的.o到程序内部链接仅记录依赖;运行时加载完整 so
运行依赖不依赖.a,链接完可删除静态库必须存在.so库文件
程序大小大小
内存表现每个进程一份库代码,内存冗余多进程共享内存一份库,节省内存
库升级库更新,程序必须重新编译链接替换 so 文件,程序无需重编译
制作工具ar rcsgcc -fPIC -shared
是否需要‑fPIC不需要必须
启动性能略快略有加载开销
部署移植简单,直接拷贝程序复杂,要保证目标机器有对应 so

Q:静态库程序运行的时候会读取 libxxx.a 吗?

A:不会。链接阶段已经把代码拷贝到可执行程序,运行和.a无关。

Q:我把静态库删掉,程序还能跑吗?

A:可以正常运行。

动静态库判断题(附答案解析)

判断对/错

  1. 静态库.a是经过链接生成的ELF可执行文件。()
  2. 使用静态库的程序运行时,仍然需要本机存在.a文件才能正常运行。()
  3. 静态链接时,链接器会从.a归档包提取用到的.o,拷贝合并进可执行程序。()
  4. 动态库.so只是多个.o简单打包,没有做链接处理。()
  5. 动态链接程序,编译链接阶段会把so库的机器码复制到可执行文件内。()
  6. 多个进程使用同一个动态库,物理内存中只会保存一份该动态库代码。()
  7. 更新动态库,替换新版.so后,原有程序必须重新编译链接才能生效。()
  8. 制作动态库必须加-fPIC参数,制作静态库不需要。()
  9. ld‑linux‑x86‑64.so.2动态链接器只负责静态链接程序。()
  10. 编译时‑L.参数,可以让程序运行时自动在当前目录找so动态库。()

答案+解析

  1. ❌错 解析:.a是ar打包的.o归档包,没有做链接;.so才是链接完成的ELF共享文件。
  2. ❌错 解析:静态库只参与链接阶段;链接完成,.a可以直接删除,运行完全不需要。
  3. ✅对 解析:静态库核心行为,抽取需要的目标文件拷贝到可执行程序。
  4. ❌错 解析:gcc -shared会执行链接,把多个.o合并生成完整so,不是简单打包。
  5. ❌错 解析:动态链接不拷贝库代码,仅记录依赖关系;运行时才加载so。
  6. ✅对 解析:动态库核心优势,内存共享,节约物理内存。
  7. ❌错 解析:动态库直接替换so即可,不需要重编译程序;静态库更新才需要重编译。
  8. ✅对 解析:‑fPIC生成位置无关代码,动态库必备;静态库无此要求。
  9. ❌错 解析:ld‑linux是运行时动态链接器,只管动态链接程序;静态链接程序不使用它。
  10. ❌错 解析:‑L仅作用于编译链接阶段;运行时查找so看LD_LIBRARY_PATH、ld.so.cache等,和‑L无关。

四.问题归纳

问题一:条件编译与if语句的区别

#ifdef:预处理指令,前面带#,发生在预处理阶段(编译之前)

if:C 语言语句,发生在程序运行阶段

对比项条件编译#ifdef / #endif普通if‑else
执行时机预处理阶段,编译之前程序运行的时候
处理方式不满足条件的代码,直接被删掉,不会进入最终程序所有代码全部编译进可执行文件,运行做判断
生成程序内容被裁剪的代码,程序里面根本不存在全部代码都存在,只是不一定执行
占用程序体积无效代码直接丢弃,不占体积全部代码都占程序体积
判断依据宏是否定义,预处理时就确定运行时变量、表达式的值

判断:使用#ifdef,条件不成立,里面的代码只是不会执行,仍然存在可执行文件中。(❌错)

解析:预处理直接删掉,产物中不存在该代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询