当前位置: 首页 > news >正文

C++ 编译链接机制的演化路径

以 完全问题驱动的方式 推导 C++ 编译链接机制的演化路径。每一步都基于前一阶段无法解决的问题,提出新的设计方案,不依赖当前 GCC 或 MSVC 的实现细节,而是像一个架构师一样,从零开始设计一个现代 C++ 系统。

🚧 第一版(V1):一切都在 main.cpp 中

✅ 初始方案:所有函数、变量、代码都写在 main.cpp 中。

// main.cpp
int add(int a, int b) {return a + b;
}int multiply(int a, int b) {return a * b;
}int main() {int result = add(2, 3) + multiply(4, 5);return 0;
}

🔧 编译命令:

gcc main.cpp -o program

📌 存在问题:

  • 随着功能变多,代码臃肿难以维护。
  • 所有内容耦合在一起,缺乏模块性。

例如我们增加不同的功能函数,还有一些公共的工具函数,全部代码都放在一起会显得非常臃肿切难易维护

// 工具函数
int add(int a, int b) {return a + b;
}
int sub(int a, int b) {return a - b;
}
int multiply(int a, int b) {return a * b;
}
int div(int a, int b) {return a / b;
}
// 主函数
int main(){int result = add(2, 3);return 0;
}

🚧 第二版(V2):拆分成多个 cpp 文件

💡 新的需求:希望将不同功能模块拆分到不同的 .cpp 文件中,提升可读性和可维护性。
✅ 解决方案:将函数拆分到多个 .cpp 文件中,并通过 #include “xx.cpp” 引入:

// math.cpp
int add(int a, int b) {return a + b;
}
int sub(int a, int b) {return a - b;
}
int multi(int a, int b) {return a * b;
}
int div(int a, int b) {return a / b;
}
// main.cpp
#include "math.cpp"int main() {int result = add(2, 3);return 0;
}

🔧 编译命令:

gcc main.cpp -o program

虽然代码拆开了,但每次修改一个函数,比如 add(),都要重新编译 所有文件,因为编译器仍然把它们当作一个整体处理。

📌 存在问题:

  • 修改任意一个函数都需要重新编译整个程序。
  • 实际上没有真正实现“单独编译”。

🚧 第三版(V3):支持单独编译与链接

💡 新的需求:希望修改某个模块后,只重新编译该模块,而不影响其他模块。
✅ 解决方案:引入 单独编译 和 链接阶段

1.拆分为 .cpp

// math.cpp
int add(int a, int b) {return a + b;
}
int sub(int a, int b) {return a - b;
}
int multi(int a, int b) {return a * b;
}
int div(int a, int b) {return a / b;
}
// main.cpp
int main() {int result = add(2, 3); // 链接时查找这个函数的实现return 0;
}

2.单独编译为 .o 目标文件:

gcc -c math.cpp -o math.o
gcc -c main.cpp -o main.o

3.链接成最终程序:

gcc math.o main.o -o program

在编译 main.cpp 时,它调用了 add() 函数,但 add() 的实现在 math.cpp 中。编译器怎么知道这个函数存在?怎么知道怎么调用?

📌 存在问题:

  • 编译器如何知道调用的add函数符合预期
  • 链接器如何知道 main.cpp 中调用的 add() 是定义在 math.cpp 中?

🚧 第四版(V4):引入符号声明和符号表机制

💡 新的需求:确保每个函数调用都能找到正确的定义,并进行类型检查。
✅ 解决方案:引入 函数声明 和 符号表机制:

在编译阶段,只需要函数的 声明(Declaration),也就是函数的接口。编译阶段只看接口,编译 main.cpp 时,编译器看到 add() 的声明,就知道这个函数存在,参数是两个 int,返回一个 int。
链接器看到 main.o 中调用了 add(),而在 math.o 中找到了 add() 的实现,就把调用指令和函数实现连接起来

1. 函数声明(.h 文件)

// math.h
int add(int a, int b);
int sub(int a, int b);
int multi(int a, int b);
int sub(int a, int b);

2. 编译阶段生成符号表:

  • 编译器在编译 main.cpp 时记录它引用了 add()。
  • 编译器在编译 math.cpp 时记录它定义了 add()。

例如

// main.cpp
#include "math.h"
int x = 1;
int y;
static int u = 1;
static int v;
char xx = 'a';
int main()
{int result = add(1, 2);return 0;
}

假设我们有两个目标文件 math.o 和 main.o,它们的符号表如下:

符号地址符号类型符号名称
0000000000000000Tadd(int, int)
0000000000000060Tdiv(int, int)
0000000000000020Tsub(int, int)
0000000000000040Tmulti(int, int)
符号地址符号类型符号名称
0000000000000000Tmain
0000000000000000Dx
0000000000000008Dxx
0000000000000000By
Uadd(int, int)
0000000000000004du
0000000000000004bv

这里符号类型有很多,T表示这个符号时函数,因为函数放在TEXT段。D表示这个符号时变量,因为变量放在DATA段,B表示未初始化的变量,放在BSS段。U表示未定义,这个符号没有在当前这个cpp中实现,需要到其他文件中查找。小写的t,d,b表示这个符号时内部链接,只有本cpp的函数和变量可以访问,不对外开放。符号的地址都是相对地址,在链接的时候统一分配。

3. 链接阶段解析符号:

  • 链接器合并所有 .o 文件的符号表。
  • 如果找不到某个函数定义,报错 undefined reference。
符号地址符号类型符号名称
0000000000000714Tmain
0000000000020030Dx
0000000000020038Dxx
0000000000020040By
0000000000000738Tadd(int, int)
0000000000000798Tdiv(int, int)
0000000000000758Tsub(int, int)
0000000000000778Tmulti(int, int)
0000000000020034du
0000000000020044bv

📌 存在问题:

  • 如何处理同名但参数不同的函数?比如 void task(int)void task(float)
  • 编译器如何区分这两个函数?

🚧 第五版(V5):支持函数重载

💡 新的需求:支持函数重载,即允许相同函数名、不同参数类型的函数存在。
✅ 解决方案:引入 符号修饰(Name Mangling)

1. 编译器根据以下信息生成唯一符号名:

  • 函数名
  • 参数类型
  • 所属命名空间或类
void func(int, int) {}
float func(int, float) {}
namespace Namespace
{void func(int, int) {}float func(int, float) {}class ClassName{public:void func(int, int);float func(int, float);};void ClassName::func(int, int){}float ClassName::func(int, float){}
}

编译器可能会将其转换为如下符号名:

0000000000000010 T __Z4funcif
0000000000000000 T __Z4funcii
0000000000000040 T __ZN9Namespace4funcEif
0000000000000030 T __ZN9Namespace4funcEii
0000000000000070 T __ZN9Namespace9ClassName4funcEif
0000000000000060 T __ZN9Namespace9ClassName4funcEii
  1. 链接器使用这些唯一符号名进行匹配。
http://www.dtcms.com/a/288706.html

相关文章:

  • 牛客NC14893 栈和排序(贪心 + 栈 + 后缀最大值维护)
  • 【机器学习|学习笔记】详解支持向量机(Support Vector Machine,SVM)为何要引入核函数?为何对缺失数据敏感?
  • 深入解析Hadoop中的EditLog与FsImage持久化设计及Checkpoint机制
  • Hadoop小文件合并技术深度解析:HAR文件归档、存储代价与索引结构
  • LeetCode|Day20|9. 回文数|Python刷题笔记
  • IP协议介绍
  • Linux: rsync+inotify实时同步及rsync+sersync实时同步
  • Leetcode 710. 黑名单中的随机数
  • 使用 Pyecharts 绘制精美饼状图:从基础到高级技巧
  • PDF 编辑器:多文件合并 拆分 旋转 顺序随便调 加水印 密码锁 页码背景
  • 微服务雪崩防护最佳实践之sentinel
  • Java 大视界 -- Java 大数据在智能安防门禁系统中的权限动态管理与安全审计(353)
  • 嵌入式硬件篇---舵机(示波器)
  • 测试学习之——Pytest Day4
  • python中读取 Excel 表格数据
  • 将EXCEL或者CSV转换为键值对形式的Markdown文件
  • 推荐一款基于.NET的进程间通信框架
  • 【橘子分布式】gRPC(编程篇-下)
  • 基于SHAP的特征重要性排序与分布式影响力可视化分析
  • ZooKeeper学习专栏(一):分布式协调的核心基石
  • 28.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--币种服务(二)
  • 智能驾驶整体技术架构详解
  • OPC UA, CAN, PROFINET, SOCKET, MODBUS, HTTP, S7七种物联网常用协议解释
  • Shell脚本-tee工具
  • 《计算机网络》实验报告三 UDP协议分析
  • DAY 20 奇异值分解(SVD)
  • 【Elasticsearch】冷热集群架构
  • 【数据结构】二维差分数组
  • 【milvus检索】milvus检索召回率
  • `TransportService` 是 **Elasticsearch 传输层的“中枢路由器”**