不妨先看一个例子:
//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()
{
}
这时候会在链接的时候报错
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);
};
我突然又想到个问题:为什么只有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()
{
}
错误输出:
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()编译可以通过(因为已经声明),链接不能通过埋下了*(找不到实现体)。
结论:
- 全局变量(对象)定义和全局函数定义一定不能出现在h文件中。类定义、枚举定义、结构体定义都可以。
- 类里面声明的函数方法,如果只声明,且没有被调用过,那么编译和链接均可以通过。但是,如果只声明,且被调用到,那么就会出现编译通过、链接不通过的错误。个人推荐,每个声明的函数方法都应该要实现。
- 编译的时候就是语法检测和声明检测(出现未声明的标识符会报错),链接的时候就是定义检测(出现重定义和函数调用时候没有该函数的定义会报错)。
- 无法解析的外部符号,大多数情况都是由于只声明了函数方法,没有函数方法实现造成的。
番外话:1、如果你的A.h的声明,在C.cpp中实现,那么生成的是C.obj文件。
2、如果你只有声明的h文件,没有实现的cpp文件,那么不会生成obj文件,且不会有任何报错。