How C++ Linker works?
文章目录
Linking
Linking 是C++ 源码到可执行二进制时的一个过程,主要工作是找到每个符号和函数的位置,并将它们链接在一起。
每个文件被编译成1个独立的obj文件,作为 translation unit,它们之间没有关系,也就是说这些文件之间没法沟通。
因此如果1个程序包含多个C++文件,需要1种方法将这些文件链接到1个程序,这就是链接器的主要作用。
即便程序没有外部的C++文件,程序仍然需要知道一个入口点。就是程序得从哪里开始执行,一般就是 main
函数。
Example 1
设计1个简单的项目,只包含1个源文件。
这里有2个函数,Log 和 Multiply,Multiply 函数调用 Log 函数。
Main.cpp:
#include<iostream>
void Log(const char* message)
{
std::cout << message << std::endl;
}
int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
然而这并不是一个完整的程序,因为没有主函数。
实际上编译程序包含2个阶段,编译和链接。
通过设定g++/gcc 不同的编译参数,设定不同的编译阶段
g++ --help
...
-v 显示编译器调用的程序。
-E 仅预处理。不编译、汇编或链接
-S 仅编译。不汇编或链接
-c 编译和汇编,不链接。
-o <file> 输出编译程序到文件
如果对上述代码只进行编译,是没有任何错误发生的。
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -S Main.cpp
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -c Main.cpp
一旦进行链接,能够生成编译过程文件(Main.o),但是链接会发生错误 undefined reference to main
:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -o Randy Main.cpp
/usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
(.text+0x24): undefined reference to `main'
collect2: error: ld returned 1 exit status
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ ls
Main.cpp Main.o Main.s
每个程序必须有个入口点,但不一定非得是main,就像一栋房子,想要入住,必须得有个门。
结论
因为编译是存在2个阶段的,所以我们会得到每个阶段相关的不同类型的错误消息。
比如上述代码第11行,如果少了个分号;
,编译代码会发生错误:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -S Main.cpp
Main.cpp: In function ‘int Multiply(int, int)’:
Main.cpp:11:17: error: expected ‘;’ before ‘}’ token
11 | return a * b
| ^
| ;
12 | }
| ~
在编译阶段就会报错。
如果在windows 环境下使用Visual Studio编译,会报error C2143
的错误,2143前面的C,就是代表Compiler,告诉我们这是编译阶段发生的错误。
如果修复了分号继续编译,则汇报Lxxx
的错误,L代表Linking 链接阶段的错误。
所以,很多时候知道bug是什么阶段的错误,我们就能正确地解决它。
Example 2
现在将Log函数单独整理出来形成1个文件,可以供大多数程序调用
Log.cpp:
#include<iostream>
void Logg(const char* message)
{
std::cout << message << std::endl;
}
Main.cpp
#include<iostream>
void Log(const char* message);
int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
std::cout << Multiply(3,11) << std::endl;
}
上述代码里面,Log.cpp函数被我手抖写成了Logg
函数
对上述代码 compiler 和 linking:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -S Main.cpp Log.cpp
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -o randy Main.cpp Log.cpp
/usr/bin/ld: /tmp/ccdeerk4.o: in function `Multiply(int, int)':
Main.cpp:(.text+0x1a): undefined reference to `Log(char const*)'
collect2: error: ld returned 1 exit status
compiler不会报错,因为主函数中提前声明了Log函数,编译器相信将来会在某个地方找到Log的实现。
但是linking会报错,告诉我们Multiply 函数调用时未找到Log 函数的实现。
这就是我们常见的undefined reference to XXX
,因为主函数链接不到该函数的具体实现。
CASE 1
如果将 Multiply 函数的实现去掉Log的调用,程序可以正常编译:
int Multiply(int a, int b)
{
// Log("Multiply");
return a * b;
}
因为 链接器不需要通过链接这个函数来调用Log函数了,因为从来没有调用过。
事实上,如果我们隐掉主程序对Multiply的调用,而Multiply中加上对Log的引用:
#include<iostream>
void Log(const char* message);
int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
int main()
{
// std::cout << Multiply(3, 11) << std::endl;
std::cout << "Hello , Randy!" << std::endl;
return 0;
}
编译后仍发现程序报错:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -o randy Main.cpp Log.cpp
/usr/bin/ld: /tmp/ccsO6vrO.o: in function `Multiply(int, int)':
Main.cpp:(.text+0x1a): undefined reference to `Log(char const*)'
collect2: error: ld returned 1 exit status
是不是很有趣,我从不调用Multiply函数,为什么还会有对Log函数的链接?
这是因为虽然主函数中我们没有使用Multiply函数,但是技术上说,我们可能在其他文件中调用Multiply函数,就像这里声明Log函数一样,因此链接器需要链接它。
如何告诉编译器Multiply函数不在其他地方使用?
所以,如果我告诉编译器这个函数只在当前文件中使用,是不是就能解决问题了?
在 Multiply 函数前加上 static
关键字,告诉编译器,Multiply 只是为这个 translate unit 声明的。
static int Multiply(int a, int b)
{
Log("Multiply");
return a * b;
}
CASE 2
如果现在我们修改Log.cpp中的Log函数:
#include<iostream>
int Log(const char* message)
{
std::cout << message << std::endl;
return 0;
}
// or
void Log(const char* message, int randy)
{
std::cout << message << std::endl;
}
编译时还会报错,因为链接时,程序找不到和Main.cpp中声明的 Log 函数返回值及入参一致的函数。
CASE 3
如果Log.cpp函数中有2个一样的函数,返回值及入参相同。
这就意味着函数或变量有相同的名字和相同的签名时,链接器不知道应该链接哪个函数了,这是不明确的。
#include<iostream>
void Log(const char* message)
{
std::cout << message << std::endl;
}
void Log(const char* message)
{
std::cout << message << std::endl;
}
结果:
(base) qiancj@qiancj-HP-ZBook-G8:~/codes/test/cherno/linker$ g++ -o randy Main.cpp Log.cpp
Log.cpp:8:6: error: redefinition of ‘void Log(const char*)’
8 | void Log(const char* message)
| ^~~
Log.cpp:3:6: note: ‘void Log(const char*)’ previously defined here
3 | void Log(const char* message)
| ^~~
这就是C++ 允许函数重载,函数名称相同,入参不能相同;
但是不允许函数名称和入参都相同,因为它们在链接阶段的签名也相同,编译器不知道该选哪个。
CASE 4
这种情况很常见,比如我们同时多个cpp文件中调用了1个头文件,该头文件中包含某个函数的具体实现,这是在编译阶段展开的时候,多个cpp文件中会包含同样函数的相同实现,也会让编译器不知道选择哪个函数版本。
常用解法
- 将需要调用的函数声明为
static
或inline
,这样其他cpp文件调用时不会发生错误; - 声明1个head 头文件,将函数声明放在头文件中,函数实现放在 cpp中,这种做法的根本原因是,当我们调用头文件时,引用了函数声明,函数实现放在了单独的 translate unit中,保证多文件调用时,只有1个具体的实现,从而避免了函数重复。