博客主页 😑
CPP学习DAY2
CPP学习DAY2

Author:

ChaunceyCHI

©

Wordage:

共计 4211 字

needs:

约 5 分钟

Popular:

371 ℃

Created:

目 录

链接器的工作方式

当我们将源代码编译成 obj 文件后,需要通过链接器将这些文件连接起来,使之生成我们所需要的可执行文件。那么在这个过程中,链接器究竟做了哪些工作呢?

链接的主要工作是找到每个符号和函数的位置,并将它们链接在一起。之前我们提到过,每一个 cpp 源代码文件实际上就是一个 translation unit,他们之间无法相互沟通,彼此之间还没有建立联系。

当我们将程序写在多个 cpp 文件中时,就必须使用链接器了。链接器除了找到每个符号和函数的位置外,还有一个很重要的功能,就是找到程序的入口点(Enter point),也就时我们所说的main函数。

当我们 build 一个项目时,实际上是执行了两个步骤:编译和链接。在VS中,假设我们有一份 cpp 代码如下:

#include <iostream>

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

当我们执行编译命令时(快捷键:CTRL + F7),我们会发现编译成功完成,没有报错;但当我们执行 build 或者运行(快捷键:F5)时,会产生一个 linking error,其大意时"没有找到程序入口点"。这是因为上面的代码中并没有包含 main 函数,在链接时链接器找不到程序的入口点,就无法知道程序应当从哪一步开始。

但是入口点并不一定必须是 main 函数,实际上我们可以自行定义程序的入口点,这可以在链接器中修改,但绝大多数情况我们无需改动,使用 main 作为程序的入口点即可。

下面我们用一个例子来小结一下:

首先我要声明一个用于打印的函数,将其写在 Log.cpp 文件中:

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

这里我们可以尝试去编译,会发现我们编译成功了!(即使里面没有main函数);

然后我们想写一个简单的两数相乘的程序,命名为 Math.cpp :

#include <iostream>

void Log(const char* message);

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

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

此处的 void Log(const char* message); 是告诉编译器我们有一个名为 Log 的函数存在(尽管我们并没有给出其详细定义),而编译器也会相信缺失有一个Log 函数存在,这样在下面的 Multiply 函数中才能使用 Log 函数。

将两份文件编译完成后,我们会得到两个 obj 文件,这两个文件在生成时会被 linker 所连接。

如果我们将上述 Log.cpp 文件中的 Log 改成 Logr 或别的什么名字,我们再编译时会发现这两个文件依旧会编译成功。这说明这两个文件之间没有联系的。但如果我们按下生成又会发生什么呢?我们会得到一个 Linking error,会告诉你无法解析的外部符号。

这就体现出 Linker 的功能了。linker 会在每个 obj 文件之间建立联系,从而生成最终的程序。

重新回到上面的例子。如果我们将写在 Multiply 函数中的 Log("Multiply"); 注释掉,那么链接器将不会报错,因为在编译过程中,编译器识别到我们并没有使用 Log 函数,那么在链接时将不会去寻找和 Log 有关的信息。然而如果我们不是注释掉Log("Multiply"); 而是将 main 函数中的输出语句注释掉,那么链接器依旧会报错。可是我们既然将输出语句注释了,说明我们并没有使用 Multiply 函数,自然也就没有用到 Log 函数,那么怎么会报与 Log 有关的链接错误呢?这是因为我们在此文件中声明的 Multiply 函数中使用到了 Log 函数,而编译器无法确定这个声明的函数是否会在其他地方被使用,所以一同进行了编译,那么链接器自然就会去找包含在 Multiply 函数中的信息。而在之前我们注释掉 Log 语句时,编译器发现并没有使用到 Log 函数,自然就不会将它放入这个 translation unit 的编译结果中。这里需要注意的两个关键词是“使用”与“定义”。

那么如何解决后者的问题呢?我们可以在 Multiply 函数的定义前加上 static ,即

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

这样我们这个函数的使用范围(作用域)就是当前文件了,也就是说其他的文件无法与之建立连接。这样我们在编译此文件时,如果发现此函数没有在当前 Translation unit 中被调用,即便是声明了此函数,也不会对其进行编译。这样我们的链接也就能顺利通过了。

在这里我们再强调一点:函数的返回类型,函数名、参数类型及参数个数都是十分重要的。如果我们将上例中的 Log.cpp 文件中的 void Log 改成 int Log 并添加代码 return 0;,一样会出现链接错误。因为在 Math.cpp 文件中,链接器试图去寻找一个返回类型为 void 的名为 Log 的函数,当然是找不到的,所以会出现错误。参数同理。

还有一种链接错误就是当我们有相同符号,也就是有两个名称相同的函数具有相同的返回值和相同的参数,此时我们的linker不知道要链接到哪个函数,此时就会出现链接错误。而当这两个相同函数出现在同一个代码文件中,即使没有发生链接,编译器也能够通过报错来告诉我们此处出现了两个相同函数。

也许你会说这么傻的错误怎么会发生,但是请考虑以下情况:

我们先写一个头文件 Log.h

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

再写文件1:

#include <iostream>
#include "Log.h"
int main()
{
    Log("hello");
}

再写文件2:

#include <iostream>
#include "Log.h"
void IntLog () 
{
    Log("world!");
}

我们可以看到,这三个文件中 Log 只被定义了一次,但是如果我们将它进行生成,那么我们仍会得到链接错误!这就要重新说回 #include 的作用了:#include 就是将后面文件中所有的代码复制到此处。所以实际上我们的文件1和文件2是这样:

文件1:

#include <iostream>
void Log(const char* message)
{
    std::cout << message << std::endl;
}
int main()
{
    Log("hello");
}

文件2:

#include <iostream>
void Log(const char* message)
{
    std::cout << message << std::endl;
}
void IntLog () 
{
    Log("world!");
}

这样就很明显了,我们将 Log 函数定义了三次,链接器当然会报错了。

那么我们要怎样避免这样的错误呢?

方法一:在 Log.h 文件中对 Log 函数的定义前添加 static 即可,也就是说,被复制过去的对于 Log 函数的定义仅在当前文件中生效。这样就可以解决链接方面的问题了。

方法二:在 Log.h 文件中对 Log 函数的定义前添加 inline 即可,也就是说,被复制过去的仅仅是 Log 函数的定义的内部的文件仅在当前文件中生效。

拿文件1举例:

方法1:

#include <iostream>
stastic Log(const char* message)
{
    std::cout << message << std::endl;
}
int main()
{
    Log("hello");
}

方法2:

#include <iostream>
int main()
{
    std::cout << "hello" << std::endl;
}

还有一种方法,就是将其写作一个单独的 Translation unit,然后在头文件中仅保留对此函数的声明(再次区分定义与声明的区别!)。

最后再重复一次:链接器(Linking)的作用就是要将编译过程中生成的所有对象文件(.obj)链接起来。它还会导入我们所需要的其他的一些库文件,例如 C运行时库、C++标准库、平台API以及其它的一些东西,这是非常常见的。同时也有不同类型的链接:静态链接和动态链接 。这些内容在后续的学习中将会被涉及。

文章二维码
CPP学习DAY2
共计 0 条评论,点此发表评论
博客主页 CHI's blog 今春不见桃花
闽ICP备2022003806号 闽公网安备35012102500456号 本站由又拍云提供CDN加速/云存储服务 本站已运行 1 年 298 天 21 小时 57 分 自豪地使用 Typecho 建站,并搭配 MyDiary 主题 Copyright © 2022 ~ 2024. CHI's blog All rights reserved.
打赏图
打赏博主
欢迎
欢迎
欢迎访问CHI's blog
欢迎您来评论,但首次评论需经过审核才能显示,之后就不用啦^_^
搜 索
足 迹
分 类
  • 默认分类
  • 相册
  • 随想录
  • 技术向
  • 读书笔记
  • 生活小记