【C++】How the C++ Linker Works

server/2024/11/26 21:27:58/
  • 如果是编译阶段的error,报错代码会以C开头(compiling),比如语句结束少写了分号
  • 如果是链接阶段的error,报错代码会以LNK开头(linking),比如缺少main函数

为什么缺少main函数就报错了呢?

Linker01

我们打开设置,可以看到配置类型是Application(.exe),而每个.exe文件必须有 entry point(入口点)

Linker02

我们还可以指定一个自定义的入口点,入口点不一定是main函数(通常是),只要有一个入口点就行了


让我们来看具体的例子

#include<iostream>void Log(const char* message) {std::cout<< message <<std::endl;
}int Multiply(int a, int b)
{Log("Multiply");return a * b;
}

此时程序缺少main函数,编译(Compile)不会报错,生成(Build)会报错

#include<iostream>void Log(const char* message) {std::cout<< message <<std::endl;
}int Multiply(int a, int b)
{Log("Multiply");return a * b;
}int main() {std::cout << Multiply(5, 8) << std::endl;std::cin.get();
}

我们将main函数加上,Link阶段就不会出问题了

我们可以把Log函数单独放到一个文件中(Log.cpp),这样可以降低程序的耦合度

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

此时直接编译会有compile error (error C3861: “Log”: 找不到标识符)

我们需要对Log函数进行声明

#include<iostream>void Log(const char* message);//函数声明int Multiply(int a, int b)
{Log("Multiply");return a * b;
}

我们再进行Compile,没有问题,那我们接着Build,又出现问题了

1>E:\SavingProject_C++\HelloWorld\HelloWorld\Log.cpp(2,7): error C2039: "cout": 不是 "std" 的成员
1>    E:\SavingProject_C++\HelloWorld\HelloWorld\predefined C++ types (compiler internal)(347,11):
1>    参见“std”的声明
1>E:\SavingProject_C++\HelloWorld\HelloWorld\Log.cpp(2,7): error C2065: “cout”: 未声明的标识符
1>E:\SavingProject_C++\HelloWorld\HelloWorld\Log.cpp(2,31): error C2039: "endl": 不是 "std" 的成员
1>    E:\SavingProject_C++\HelloWorld\HelloWorld\predefined C++ types (compiler internal)(347,11):
1>    参见“std”的声明
1>E:\SavingProject_C++\HelloWorld\HelloWorld\Log.cpp(2,31): error C2065: “endl”: 未声明的标识符

我们应该在Log.cpp中加上#include<iostream>

这样就没问题啦


我们再来看一个可能会出现的 linking error——unresolved external symbol(无法解析的外部符号)

It happens when the Linker can’t find something that it needs.

在Log.cpp中将Log函数名加一个r,变成Logr

Compile不会出现报错,他相信在某处有一个Log函数,而要找到它就是Link的事情了

因为我们将Log变成了Logr,自然就找不到了,Build自然就会报错:unresolved external symbol(无法解析的外部符号)

error LNK2019: 无法解析的外部符号 "void __cdecl Log(char const *)" (?Log@@YAXPEBD@Z),函数 "int __cdecl Multiply(int,int)" (?Multiply@@YAHHH@Z) 中引用了该符号

如果我们将Log("Multiply");这一行代码注释了,就不会报错了

这是因为我们没有调用过Log函数,链接器不需要去链接


如果我们不将那一行注释,而是将std::cout << Multiply(5, 8) << std::endl;这一行注释

即不调用Multiply函数

但是报错了,这是为什么呢?

虽然我们没有在这个文件里调用Multiply函数,但技术上讲,我们可能在另一个文件中会使用它

所以链接器确实需要链接它


如果我们能告诉编译器,这个Multiply函数我只会在这个文件中使用它。

当然,我们可以去掉这种链接的必要性,因为Multiply从来不会被调用,也就从不需要Log

int Multiply(int a, int b)前加上static

这意味着Multiply函数只被声明在这个翻译单元中(也就是math.cpp)

此时我们再进行Build,不会报错;而把注释删去,仍然会报错


在这个例子中,我们改变的是函数名,会出现报错;

同样的,如果参数列表和返回值不一样,也会出现报错。

函数名一样,参数列表和返回值不一样,这就是一个新的函数

为什么可以这样?还有我们调用的时候怎么知道自己调用的是哪一个呢?

这就涉及到函数重载了,我们以后会讲


那如果有两个一模一样的函数呢?

Log.cpp

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

compiling error

函数“void Log(const char *)”已有主体(already has a body)

我们将一份移到Math.cpp中

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

linking error

1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPEBD@Z) 已经在 Log.obj 中定义
1>E:\SavingProject_C++\HelloWorld\x64\Debug\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号

链接器不知道它该链接哪一个Log函数,是Log.cpp中的还是Math.cpp中的?


看完以上这些你可能会觉得自己并不会犯这样的错误,那我们接着往下看

创建头文件Log.h 将Log.cpp此部分内容剪切到Log.h中

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

Log.cpp变成

#include<iostream>void InitLog() {Log("Initialized Log");
}

此时编译:error C3861: “Log”: 找不到标识符

在Log.cpp中添加#include "Log.h"

在Math.cpp中删去函数声明,同样地添加#include "Log.h"

此时生成:

1>LINK : 没有找到 E:\SavingProject_C++\HelloWorld\x64\Debug\HelloWorld.exe 或上一个增量链接没有生成它;正在执行完全链接
1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPEBD@Z) 已经在 Log.obj 中定义
1>E:\SavingProject_C++\HelloWorld\x64\Debug\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号

实际上我们就定义了一个Log函数,它在Log.h中。那为什么会说我们重复定义了呢?

还记得#include的工作原理吗?

在How the C++ Compiler Works中我们是这样说的

You basically specify which file you want to include and then the pre-processor will open that file read all of its contents and just paste it into the file where you wrote your include statement.

你只需指定要包含的文件,然后预处理器将打开该文件,读取其所有内容,然后将其粘贴到你写include语句的文件中。

在Log.cpp和Math.cpp中都有#include "Log.h",也就都定义了Log函数

我们如何解决这个问题呢?

(1)标记Log函数为静态的(前面加static),这意味着在链接Log函数时,它只能是内部函数。这样Log.cpp和Math.cpp都会有自己版本的Log函数,它对任何其他的obj文件都不可见。

(2)前面加inline,它会获取我们实际的函数体并将函数调用替换为函数体

在这种情况下,InitLog函数实际上是这样的

void InitLog() {std::cout << "Initialized Log" << std::endl;
}

(3)将Log函数的定义放到Log.cpp中,而在Log.h中声明它

对于static和inline还不了解的同学可以先记这种,我们以后会对它们进行详细讲解

视频:https://www.youtube.com/watch?v=H4s55GgAg0I


http://www.ppmy.cn/server/137808.html

相关文章

SpringBoot环境下的学生请假管理平台开发

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

openpnp - 手工修改配置文件(元件高度,size,吸嘴)

文章目录 openpnp - 手工修改配置文件(元件高度,size,吸嘴)概述笔记parts.xmlpackages.xml 手工将已经存在的NT1,NT2拷贝出来改名备注END openpnp - 手工修改配置文件(元件高度,size,吸嘴) 概述 载入新板子贴片准备时&#xff0c;除了引入Named CSV文件&#xff0c;还要在ope…

Java的包、final关键字以及代码块

Java的包、final关键字以及代码块 一、包 包的作用 &#xff1a; ​ 包就是文件夹&#xff0c;用来管理各种不同功能的Java类 包名的书写规则&#xff1a; ​ 公司域名反写 包的作用&#xff0c;需要全部英文小写&#xff0c;见名知意 什么是全类名&#xff1a; ​ 包名…

录屏天花板,录课新玩法,人像+一切,PPT/PDF/视频/网页,也可即可录

上新啦 &#x1f4f1;&#x1f4bb; 录屏也能录课的万能神器——超级推荐&#xff01; 你是不是也在找一款能高效录屏、录课、轻松剪辑的小工具&#xff1f;作为一名需要频繁录制屏幕和课程内容的老师&#xff08;或内容创作者&#xff09;&#xff0c;我找到了这个宝藏App&…

青少年编程与数学 02-003 Go语言网络编程 02课题、网络分层模型

青少年编程与数学 02-003 Go语言网络编程 02课题、网络分层模型 课题摘要:一、网络分层模型&#xff08;一&#xff09;OSI七层模型&#xff08;Open Systems Interconnection Model&#xff09;&#xff08;二&#xff09;TCP/IP四层模型&#xff08;Transmission Control Pro…

Gorilla Mk1机器人:CubeMars电机加持,助力高空作业新突破

在澳大利亚输电网络的高空作业领域&#xff0c;一款由Crest Robotics研发的创新机器人正悄然改变着工作方式。这款名为Gorilla Mk1的机器人&#xff0c;凭借先进的技术和精密的动力系统&#xff0c;在高压输电线路的维护和检修作业中提供了前所未有的安全性和高效性。而这背后&…

Docker | 通过commit操作实例来认识镜像底层实现的原理以及学会打包镜像

镜像底层实现的原理 docker 镜像镜像是什么?分层的镜像UnionFS(联合文件系统)为什么Docker镜像要采用这种分层结构呢?打包镜像 docker commit ⭐⭐ubuntu安装vim docker 镜像 镜像是什么? 是一种轻量级、可执行的独立软件包&#xff0c;它包含运行某个软件所需的所有内容&…

Java/Springboot使用iText生成PDF

iText是一个用于创建和操作PDF文档的Java库。 常见使用步骤和示例如下&#xff1a; 1. 添加依赖 如果使用Maven项目&#xff0c;在pom.xml文件中添加以下依赖&#xff1a; <dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</ar…