How the C++ Linker Works

How the C++ Linker Works

C++链接器的工作原理


介绍

什么是链接?C++链接器实际上做了什么?链接是我们从C++源文件转到实际的可执行二进制文件时要经历的过程。第一阶段实际上是编译我们的源文件,编译完文件后,我们需要经历一个称为链接的过程。现在链接的主要重点是找到每个标识符和函数的位置并将它们链接在一起,记住每个文件编译成一个单独的对象文件作为翻译单元,并且它们彼此之间没有关系,这些文件实际上无法交互。因此,如果我们决定将程序拆分到多个C ++文件中,这当然很常见,我们需要一种方法将这些文件实际链接到一个程序中,这是链接器的主要目的。

即使您在外部文件中没有其他需要链接的函数,例如您将整个程序编写在一个文件中,应用程序仍然需要知道入口点在哪里,换句话说,需要知道main函数在哪里。这样当你实际运行你的应用程序时,C 运行时库可以说这里是main函数,我将跳到那里并开始从那里开始执行代码,这实际上是从你的应用程序开始的,即使你没有其他C++文件,它仍然需要链接main函数和类似的东西。

Visual studio

编写以下代码

//Math.cpp
#include <iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}

int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

上述代码只是两个简单的函数,但是没有main 函数。你必须意识到的第一件事是,有两个编译阶段,有编译和链接,实际上有一种方法可以区分两个结算,如果你按 Ctrl F7 或者如果你按下编译按钮,只有编译会发生,不会发生任何链接。
Ctrl F7 编译上述代码,发现没有任何错误。它生成了obj文件
右击项目,Build项目,你会发现出现了链接错误,无法解析入口点。因为我没有写入门点main函数。
在这里插入图片描述
因此,由于我们的编译分为编译和链接这两个阶段,因此我们实际上在每个阶段都会获得不同类型的错误消息。

刻意把代码改错,return 最后少个分号。

int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b
}

然后编译这个文件。会发现除了编译错误。这种类型的错误的错误代码,你会注意到它实际上是以字母C开头的,这告诉我们这是编译阶段发生的错误
在这里插入图片描述
对比刚刚的链接错误,发现是以LNK开头的。这当然会标记一个链接,它甚至在这里告诉我们这发生在链接阶段。
所以当出现错误时,你得知道是编译错误还是链接错误,这真的很重要。因为当你知道这一点,以便您可以正确修复。

我们打开项目属性可以看到配置类型时exe应用程序,每一个exe程序都应该要有程序得入口点。
在这里插入图片描述
切换到链接器->高级里面可以看到入口点信息。入口点不必是main函数,只需要有一个入口点,现在通常它是main函数,但只是为了你知道入口点不一定必须是称为 main 的函数,它真的可以是任何东西。
在这里插入图片描述
我们把main函数补上。

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

然后build项目,生成成功,对应也生成了HelloWorld.exe程序,运行正确没有问题。

Log Files

现在我要在多个文件中进行编写,例如这个Log函数不需要再Math.cpp文件中,因为它只是记录消息,所以我为什么不把它放入一个单独得文件里。新增Log.cpp文件,把Log函数移到这个文件下。

//Log.cpp
#include <iostream>

void Log(const char* message)
{
	std::cout << message << std::endl;
}

//Math.cpp
#include <iostream>

int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

回到Math.cpp 文件,编译这个文件。收到一个错误,你会知道这是一个编译错误。因为这个错误是以C开头得。这个错误是找不到Log标识符。
在这里插入图片描述
我们把Log函数得第一行复制到Math.cpp 文件中,相当于我们再Math.cpp文件中声明这个函数

//Math.cpp
void Log(const char* message);

我们再次编译Math.cpp,发现已经编译成功了.

链接错误

让我们看一下一种类型的链接错误,我们可能会得到这种错误,称为未解析的外部符号,这就是当链接器找不到它需要的东西时发生的情况。

我们回到Log.cpp文件并将log函数改为Logr,Math.cpp仍然保留Log函数的声明

//Log.cpp
void Logr(const char* message)
{
	std::cout << message << std::endl;
}
//Math.cpp
void Log(const char* message);

编译成功。它仍然希望调用 Log函数,所以这个文件仍然会编译,当然因为这没有链接,所以它所做的只是检查以确保这里的所有内容都正确编译,它相信某处有一个Log函数,但实际找到该日志函数将是链接阶段的工作。如果我构建整个项目我们实际会获得一个错误,这是一个链接错误,因为你可以看到它是以LNK字母开头的。
在这里插入图片描述
内容是未解析的外部符号,现在他确切地告诉我们缺少什么符号,正是那个log声明它甚至告诉我们在哪里引用它,我们在一个名为 multiply 的函数中引用它,所以这里是在 multiply 中我们调用 log,它实际上找不到将其链接到哪个函数,所以当然它必须给我们一个错误,因为当我们在运行该代码时,当它尝试调用log函数时它应该做什么,它不知道log函数在哪里。

int Multiply(int a, int b)
{
	//Log("Multiply");
	return a * b;
}

如果我去这里并注释掉这个log函数,这样我们实际上永远不会调用它。如果我尝试构建它,我们不会出错。原因是因为我从没有调用这个log函数,所以链接器并没有去链接到实际的log函数定义。

int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	//std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

另一个有趣的注意事项是,如果我确实在这里调用 log 函数和mutiply,但是我注释掉了这一行,这样我就永远不会调用 multiply,而如果我构建我的项目,这反过来又永远不会调用 log,现在你会看到我仍然收到一个链接错误,你可能想为什么会这样,我没有在任何地方调用乘法,为什么报链接错误。因为虽然我们没有在这个文件中使用 multiply 函数,但我们实际上可以另一个文件中使用它,所以链接器实际上确实需要链接,如果我们能以某种方式告诉编译器我只会在这个文件中使用它,那么我们当然可以删除这种链接的必要性,因为这从不调用它永远不需要调用日志。

static int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	//std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

在这里插入图片描述
等等,有一种方法我们可以做到这一点,如果我们来到这里并在 Multiply 前面写下 STATIC 这个词,这基本上意味着这个 multiply 函数只为这个翻译单元声明,我们的这个math.cpp文件,由于 multiply 永远不会在这个文件中调用,如果我构建,我们不会得到任何链接错误。

注意:函数的声明和定义的返回值及参数必须完全一致,不然链接器会报错

符号链接

另一种非常常见的链接错误是当我们必须查看符号时,换句话说,我们有具有相同名称和相同签名的函数或变量。因此如果发生这种情况,两个具有相同返回值和相同参数的相同函数将遇到麻烦。我们麻烦的原因是因为链接器不知道链接到哪个。

如果在同一个文件中出现了两个相同的函数,编译器还可以告诉我们有问题,因为并没有发生链接。如果把Log函数的定义复制到Math.cpp中

#include <iostream>

void Log(const char* message);

void Log(const char* message)
{
	std::cout << message << std::endl;
}

static int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

编译没有问题,但是build项目会出现链接错误。Log函数已经在log.obj中定义了,找到一个或多个重定义的符号。
在这里插入图片描述
所以这个案例链接它不知道要链接到哪个日志函数,是否链接到math.cpp还是是链接到Log.cpp它不知道.现在你可能会认为这种类型的错误不是经常发生的事情,你比这更聪明。

我们展示几种可以尽量避免这个问题的方法
添加Log.h头文件,并且把Log函数的定义剪切到这个文件中,在原来的Log.cpp文件中创建一个新的InitLog的函数,需要引入Log.h的头文件,不然会编译器报错会找不到Log标识符

//Log.h
#pragma once

void Log(const char* message)
{
	std::cout << message << std::endl;
}

//Log.cpp
#include <iostream>
#include "Log.h"

void InitLog()
{
	Log("Initialized Log");
}

我们在Math.cpp中也引入Log.h头文件

#include <iostream>
#include "Log.h"

void Log(const char* message);

static int Multiply(int a, int b)
{
	Log("Multiply");
	return a * b;
}

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}

然后build项目,出现了链接错误。它告诉志已经在本地 OBJ 中定义,所以我们收到一个重复的符号错误消息,但是你可以看到我实际上只有一个日志定义它在这个 log.h 文件中,为什么会出现多个标识符的问题
在这里插入图片描述
回到 include 语句的工作原理,请记住,当我们包含一个头文件时,我们只是获取该头文件的内容并将其放在我们的 include 语句所在的位置,所以实际上,在log.cpp和math.cpp中都定义了这个log函数。我们有几种选择可以解决这个问题

1.将log函数前面加上static 。

发生在这个日志函数的链接应该只是内部的,这意味着 log.cpp 和 math.cpp 中时将只是他们文件的内部链接log.h中的log函数定义。所以基本上log和math将有这个函数的自己的log版本,它对任何其他目标文件都不可见。

2.在log函数定义前面加上inline,

当然只是意味着它只是用到我们的实际函数体并用它替换调用

//Log.h
#pragma once

inline void Log(const char* message)
{
	std::cout << message << std::endl;
}

//Log.cpp
#include <iostream>
#include "Log.h"

void InitLog()
{
	Log("Initialized Log");
}

//相当于可以看作
void InitLog()
{
	std::cout << "Initialized Log"<< std::endl;
}

build成功。

我们可以解决这个问题,这可能是我在这种情况下会做的,那就是将其定义移动到一个翻译单元中,因为现在发生的事情是这个log函数包含在两个翻译单元 log.cpp 和 math.cpp.。这就是导致错误的原因,首先,我们可以将其移动到第三个翻译单元中,或者我们可以将此log定义放入这些现有翻译单元之一。
具体方法:将log函数的定义放到log.cpp中,然后Log.h中只剩log函数的声明

//Log.h
#pragma once

void Log(const char* message);

//Log.cpp
#include <iostream>
#include "Log.h"

void InitLog()
{
	Log("Initialized Log");
}

void Log(const char* message)
{
	std::cout << message << std::endl;
}

同样build成功
所以现在这个头文件只有log的声明,要链接到的实际函数包含在log.cpp中,函数只定义在我们项目的一个翻译单元中。

请记住链接器需要获取我们在编译期间生成的所有对象文件并将它们全部链接在一起,它将拉入我们可能正在使用的任何其他库,例如 C 运行时库 ,C++ 标准库,我们的平台API是必要的,从许多不同的地方链接许多其他东西是很常见的。还有不同类型的链接,我们有静态链接,我们有动态链接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值