关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究

时间:2022-12-31 09:44:21

不妨先看一个例子:

关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究
//A.h

#pragma once

#include
"B.h"

class A

{

public:

A(
void);

~A(void);

};



//B.h

#pragma once

int a;



class B

{

public:

B(
void);

~B(void);

};



//main.cpp

#include
<iostream>

#include
"A.h"

#include
"B.h"

using namespace std;



int main()

{



}
View Code

 

这时候会在链接的时候报错

1>B.obj : error LNK2005: "int a" (?a@@3HA) 已经在 A.obj 中定义

1>main.obj : error LNK2005: "int a" (?a@@3HA) 已经在 A.obj 中定义

错误基本原因:

因为A.h在#include”b.h”后会产生一个int a的定义语句。程序在编译的时候只会对单个文件进行语法等要素的编译生成obj文件,在链接的时候将这些obj文件整合起来,这时候由于a.obj和b.obj都有int a的定义,就出现了上述所谓的重定义。

 

深入探究原因:

因为:1.定义和声明(定义就是说程序一定知道会为其分配多少内存,比如int a就会知道分配4bytes的内存,声明是不会知道分配多少内存的。定义分为3种:int a 对象定义;void func(){…}函数定义;Class A{…}类型定义;声明分为2种:void func();函数声明, Class A;类型声明)。最重要的点:在一个文件中同一个声明可以多次声明,但是定义只能定义一次。2.在编译的时候,include”B.h”会将B.h中的所有东西拷贝到a.h中,这时候的a.h就应该是这样的:

关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究
//A.h

#pragma once

int a;



class B

{

public:

B(
void);

~B(void);

};

class A

{

public:

A(
void);

~A(void);

};
View Code

 

我突然又想到个问题:为什么只有int a类型重定义,不会有class B类型重定义呢?

不妨再来看个例子。

关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究关于链接过程中出现的“找到一个或多个多重定义的符号”和“无法解析的外部符号”错误的探究
//A.h

#pragma once

#include
"B.h"

//int tempA;

//void Func2();

//class Cattle

//{

//public:

// Cattle();

// ~Cattle();

//};

class Apple

{

public:

Apple(
void);

~Apple(void);

BadBoy bad;

Cattle catt;

void Func1();

};



//B.h

#pragma once

int tempB;

void func(){}

class Cattle

{

public:

Cattle();

~Cattle();

};

class BadBoy

{

public:

BadBoy(
void);

~BadBoy(void);

};

#include
<iostream>

#include
"A.h"

#include
"B.h"

using namespace std;



//main.cpp

int main()

{



}
View Code

 

错误输出:

1>B.obj : error LNK2005: "void __cdecl func(void)" (?func@@YAXXZ) 已经在 A.obj 中定义

1>B.obj : error LNK2005: "int tempB" (?tempB@@3HA) 已经在 A.obj 中定义

1>main.obj : error LNK2005: "void __cdecl func(void)" (?func@@YAXXZ) 已经在 A.obj 中定义

1>main.obj : error LNK2005: "int tempB" (?tempB@@3HA) 已经在 A.obj 中定义

 

我打开a.obj找到这样一段:

$T__________`_______________________________________?tempB@@3HA_?func@@YAXXZ___RTC_Shutdown.rtc$TMZ___RTC_Shutdown___RTC_InitBase.rtc$IMZ___RTC_InitBase_??0Apple@@QAE@XZ_??1BadBoy@@QAE@XZ___unwindfunclet$??0Apple@@QAE@XZ$0_??0Cattle@@QAE@XZ_??0BadBoy@@QAE@XZ____security_cookie___ehhandler$??0Apple@@QAE@XZ____CxxFrameHandler3_@__security_check_cookie@4___ehfuncinfo$??0Apple@@QAE@XZ___unwindtable$??0Apple@@QAE@XZ___RTC_CheckEsp_??1Apple@@QAE@XZ___unwindfunclet$??1Apple@@QAE@XZ$0_??1Cattle@@QAE@XZ___ehhandler$??1Apple@@QAE@XZ___ehfuncinfo$??1Apple@@QAE@XZ___unwindtable$??1Apple@@QAE@XZ_

 

在b.obj找到这样一段:

$T__________`___________________________________?___?tempB@@3HA_?func@@YAXXZ___RTC_Shutdown.rtc$TMZ___RTC_Shutdown___RTC_InitBase.rtc$IMZ___RTC_InitBase_??0Cattle@@QAE@XZ_??1Cattle@@QAE@XZ_??0BadBoy@@QAE@XZ_??1BadBoy@@QAE@XZ_

 

通过这样一段,我个人所认为的是对a.obj和b.obj进行链接时,其实就是字符串比较,遇到$T之后的如果a.obj和b.obj同时出现相同的?***@@***的字符串时候就会进行产生重定义的错误。在遇到$ IMZ___RTC_InitBase之后就不会进行字符串比较了。这只是我个人猜测,但是VS绝对有某种方式将类型定义隐藏起来,从而不会出现重定义的错误。

 

这2段信息都是表示这个obj中的出现的定义和函数引用信息。实际就是h文件向下转换的更底层的代码语言。

 

我在后面测试的BUG中发现了红色标注的内容。

>A.obj : error LNK2019: 无法解析的外部符号 "public: __thiscall BadBoy::~BadBoy(void)" (??1BadBoy@@QAE@XZ),该符号在函数 __unwindfunclet$??0Apple@@QAE@XZ$0 中被引用

1>A.obj : error LNK2019: 无法解析的外部符号 "public: __thiscall Cattle::Cattle(void)" (??0Cattle@@QAE@XZ),该符号在函数 "public: __thiscall Apple::Apple(void)" (??0Apple@@QAE@XZ) 中被引用

1>A.obj : error LNK2019: 无法解析的外部符号 "public: __thiscall BadBoy::BadBoy(void)" (??0BadBoy@@QAE@XZ),该符号在函数 "public: __thiscall Apple::Apple(void)" (??0Apple@@QAE@XZ) 中被引用

1>A.obj : error LNK2019: 无法解析的外部符号 "public: __thiscall Cattle::~Cattle(void)" (??1Cattle@@QAE@XZ),该符号在函数 "public: __thiscall Apple::~Apple(void)" (??1Apple@@QAE@XZ) 中被引用

 

可以推断:@表示冒号,??0Apple@@QAE@XZ == XZ:QAE::Apple() == public:thiscall::apple()

 

这几个错误的原因都是A.obj文件没有在其他obj文件中找到这些函数的实现体。

 

另外,我在debug的过程中有个重要发现:在A.obj找不到Apple类中的属性bad和catt。这说明了obj文件不保存属性的名称,只保存了该属性的序列长度(也就是多少比特数),同时如果只声明Func1(),在CPP文件中不实现,也找不到方法Func();这为后期的Func1()编译可以通过(因为已经声明),链接不能通过埋下了*(找不到实现体)。

结论:

  1. 全局变量(对象)定义和全局函数定义一定不能出现在h文件中。类定义、枚举定义、结构体定义都可以。
  2. 类里面声明的函数方法,如果只声明,且没有被调用过,那么编译和链接均可以通过。但是,如果只声明,且被调用到,那么就会出现编译通过、链接不通过的错误。个人推荐,每个声明的函数方法都应该要实现。
  3. 编译的时候就是语法检测和声明检测(出现未声明的标识符会报错),链接的时候就是定义检测(出现重定义和函数调用时候没有该函数的定义会报错)。
  4. 无法解析的外部符号,大多数情况都是由于只声明了函数方法,没有函数方法实现造成的。

 

番外话:1、如果你的A.h的声明,在C.cpp中实现,那么生成的是C.obj文件。

2、如果你只有声明的h文件,没有实现的cpp文件,那么不会生成obj文件,且不会有任何报错。