C++ | How C++ Linker works?

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文件中会包含同样函数的相同实现,也会让编译器不知道选择哪个函数版本。

常用解法
  1. 将需要调用的函数声明为staticinline,这样其他cpp文件调用时不会发生错误;
  2. 声明1个head 头文件,将函数声明放在头文件中,函数实现放在 cpp中,这种做法的根本原因是,当我们调用头文件时,引用了函数声明,函数实现放在了单独的 translate unit中,保证多文件调用时,只有1个具体的实现,从而避免了函数重复。

>>>>> 欢迎关注公众号【三戒纪元】 <<<<<

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值