【C++】链接器如何工作

例子1:

#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();
}
#Log.cpp
#include <iostream>


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

生成项目时的报错信息:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
1>Math.obj : error LNK2019: 无法解析的外部符号 "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z),该符号在函数 "int __cdecl Multiply(int,int)" (?Multiply@@YAHHH@Z) 中被引用
1>****:  fatal error LNK1120: 1 个无法解析的外部命令
========== 生成:  成功 0 个,失败 1 个,最新 0 个,跳过 0==========

编译时无报错:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
========== 生成:  成功 1 个,失败 0 个,最新 0 个,跳过 0==========

以上代码可以发现,Log函数的名字写成了Logr,这是错误所在
但是,如果只对Math.cpp文件编译,没有报错,说明编译过程只负责检查语法是否有错,对于是否有调用错误的函数是检查不出来的;
生成项目包含编译和链接,既然编译没有问题,那么问题肯定出在链接这个过程。
由于文件中会调用另一个文件/库的函数或常量,所以链接过程需要把相关的文件都连接起来,而我们连接的函数找不到(即名字写错或根本没有这个函数),所以会在这个过程报错,报错的标识是LNK,即在链接过程报错。

另外,我们还可以发现,在main函数其实没有调用Multiply函数,也就是没有实际调用Log函数,按理说,就不会出现报错信息,但显然,链接器还是出现报错了。这又如何解释呢?
这是因为虽然在本文件(Math.cpp)下不会调用到Multiply函数,但是不保证在其他文件不调用Multiply函数,所以链接器还是会将Log函数进行连接,从而发现函数名写错。

例子2:

但是,如果给Multiply函数前面添加static, 则Multiply的调用只会被限定在Math.cpp文件中,这样,如果在Math.cpp文件没有被调用,则不会在其他文件被调用,链接器就不必把相关的函数进行链接,也就不会有报错。

Multiply函数添加static后生成没有报错:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
1>  HelloWorld.vcxproj -> ***\HelloWorld.exe
========== 生成:  成功 1 个,失败 0 个,最新 0 个,跳过 0==========

例子3:

进一步研究,如果把Log.cpp文件里的Logr函数名修正Log,然后将返回类型改为int, 并在函数后面添加return 0,生成项目依然会有报错。

#include <iostream>

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

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();
}

生成项目时的报错信息:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
1>Math.obj : error LNK2019: 无法解析的外部符号 "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z),该符号在函数 "int __cdecl Multiply(int,int)" (?Multiply@@YAHHH@Z) 中被引用
1>*** : fatal error LNK1120: 1 个无法解析的外部命令
========== 生成:  成功 0 个,失败 1 个,最新 0 个,跳过 0==========

例子4:

如果在Log.cpp写了两个相同的函数,会出现编译错误。

#Log.cpp
#include <iostream>


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

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

生成项目时的报错信息:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Log.cpp
1>***\log.cpp(10): error C2084: 函数“void Log(const char *)”已有主体
1>          ****\log.cpp(4) : 参见“Log”的前一个定义
========== 生成:  成功 0 个,失败 1 个,最新 0 个,跳过 0==========

错误发生的时候,链接还没开始,编译器可以识别这类错误,编译报错识别标识符是C。

例子5:

如果在重复写的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();
}

生成项目时的报错信息:

>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
1>  Log.cpp
1>  正在生成代码...
1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z) 已经在 Log.obj 中定义
1>****\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
========== 生成:  成功 0 个,失败 1 个,最新 0 个,跳过 0==========

可以观察到,链接错误识别标识符是LNK,编译可以通过,但是在链接过程中,出现了两个相同的函数,不知道该链接哪个,出现报错。

例子6:

再来看另一个例子。添加一个头文件Log.h,并且在Log.cpp和Math.cpp都申明该头文件,并调用它。

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


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

int main()
{
	std::cout << Multiply(5, 8) << std::endl;
	std::cin.get();
}
#Log.cpp
#include <iostream>
#include "Log.h"

void InitLog()
{
	Log("Initialized Log");
}
#pragma once

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

生成项目时的报错信息:

1>------ 已启动生成:  项目: HelloWorld, 配置: Debug Win32 ------
1>  Math.cpp
1>Math.obj : error LNK2005: "void __cdecl Log(char const *)" (?Log@@YAXPBD@Z) 已经在 Log.obj 中定义
1>******\HelloWorld.exe : fatal error LNK1169: 找到一个或多个多重定义的符号
========== 生成:  成功 0 个,失败 1 个,最新 0 个,跳过 0==========

明明只有一个Log函数的定义,为什么会报错有多重符号呢?
include语句的工作原理:当我们包含头文件时,取头文件的内容,粘贴到include语句的位置。
所以,实际情况是,Log函数被放在了Log.cpp文件和Math.cpp文件里,这就回到了例子5的情况,出现链接错误。

难道定义了一个头文件,就不能同时有两个文件同时调用吗?其实不然,可以将这个Log函数标记为静态的(static),意味着在链接这个函数时,log函数只能分别是Log.cpp和Math.cpp文件里的内部函数

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

当然,另一个相似的办法是,标记Log函数为内联函数(inline):

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

内联函数:表示我们获取实际的函数体并将函数调用替换为函数体。

其实,以上报错的主要原因是log函数的定义被写在了两个cpp文件里,所以在连接时会出现多重符号报错。那么有没有一种方法另Log函数只在一个cpp文件里被定义?
答案是有的,这种解决方案是:
将Log函数挪到Log.cpp中,Log.h头文件只保留一行关于这个函数的申明语句,这样的话,在Log.cpp和Math.cpp文件中都申明了该函数,相而函数Log只在Log.cpp文件中被定义,所以,链接时只有一个Log函数体出现,没有报错。

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

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

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

总结

  1. 编译不会检查函数体调用的错误。
  2. 链接过程会把显式调用的函数全部连接起来,如果发现函数名(或其他形式)写错,则会报错。
  3. static可以限定函数只允许在本文件下被调用,不允许被外部文件调用。
  4. 链接函数过程中,函数的名字、函数的返回类型以及函数的参量一定要一致才行。
  5. 如果在同一个文件中出现相同的函数名和函数体,编译器可以识别出这类错误,出现编译错误。
  6. 如果在主文件和被调用文件里出现相同的函数名和函数体,编译器识别不出这类错误,但链接器可以,因为链接器找到了两个相同的函数,不知道该连接哪一个。
  7. 如果两个文件同时申明一个头文件,需要将头文件里的函数标记为静态的(static)或者内联函数(inline)。
  8. 在将所有的头文件内容全部粘贴到各自的cpp文件里后,要确保每个函数都是唯一的,在同一文件里唯一且在不同文件之间也是唯一的(不同文件里相同函数可由static和inline来赋予其唯一性)
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值