以 完全问题驱动的方式 推导 C++ 编译链接机制的演化路径。每一步都基于前一阶段无法解决的问题,提出新的设计方案,不依赖当前 GCC 或 MSVC 的实现细节,而是像一个架构师一样,从零开始设计一个现代 C++ 系统。
✅ 初始方案:所有函数、变量、代码都写在 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;
}
新的需求:希望将不同功能模块拆分到不同的 .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(),都要重新编译 所有文件,因为编译器仍然把它们当作一个整体处理。
存在问题:
新的需求:希望修改某个模块后,只重新编译该模块,而不影响其他模块。
✅ 解决方案:引入 单独编译 和 链接阶段
// 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;
}
gcc -c math.cpp -o math.o
gcc -c main.cpp -o main.o
gcc math.o main.o -o program
在编译 main.cpp 时,它调用了 add() 函数,但 add() 的实现在 math.cpp 中。编译器怎么知道这个函数存在?怎么知道怎么调用?
存在问题:
新的需求:确保每个函数调用都能找到正确的定义,并进行类型检查。
✅ 解决方案:引入 函数声明 和 符号表机制:
在编译阶段,只需要函数的 声明(Declaration),也就是函数的接口。编译阶段只看接口,编译 main.cpp 时,编译器看到 add() 的声明,就知道这个函数存在,参数是两个 int,返回一个 int。
链接器看到 main.o 中调用了 add(),而在 math.o 中找到了 add() 的实现,就把调用指令和函数实现连接起来
// 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);
例如
// 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,它们的符号表如下:
符号地址 | 符号类型 | 符号名称 |
---|---|---|
0000000000000000 | T | add(int, int) |
0000000000000060 | T | div(int, int) |
0000000000000020 | T | sub(int, int) |
0000000000000040 | T | multi(int, int) |
符号地址 | 符号类型 | 符号名称 |
---|---|---|
0000000000000000 | T | main |
0000000000000000 | D | x |
0000000000000008 | D | xx |
0000000000000000 | B | y |
U | add(int, int) | |
0000000000000004 | d | u |
0000000000000004 | b | v |
这里符号类型有很多,T表示这个符号时函数,因为函数放在TEXT段。D表示这个符号时变量,因为变量放在DATA段,B表示未初始化的变量,放在BSS段。U表示未定义,这个符号没有在当前这个cpp中实现,需要到其他文件中查找。小写的t,d,b
表示这个符号时内部链接,只有本cpp的函数和变量可以访问,不对外开放。符号的地址都是相对地址,在链接的时候统一分配。
符号地址 | 符号类型 | 符号名称 |
---|---|---|
0000000000000714 | T | main |
0000000000020030 | D | x |
0000000000020038 | D | xx |
0000000000020040 | B | y |
0000000000000738 | T | add(int, int) |
0000000000000798 | T | div(int, int) |
0000000000000758 | T | sub(int, int) |
0000000000000778 | T | multi(int, int) |
0000000000020034 | d | u |
0000000000020044 | b | v |
存在问题:
void task(int)
和 void task(float)
新的需求:支持函数重载,即允许相同函数名、不同参数类型的函数存在。
✅ 解决方案:引入 符号修饰(Name Mangling):
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