问题描述:
源于在尝试extern的使用范围时,遇到的bug,代码如下:
1 main.cpp
2
3 #include "stdafx.h"
4 #include "ExternExample.h"
5
6
7 int _tmain(int argc, _TCHAR* argv[])
8 {
9 using namespace std;
10 cout << temp << endl;
11
12 system("pause");
13 return 0;
14 }
15
16 ExternExample.h
17
18 int temp;
错误结果 error LNK2005: "int temp" (?temp@@3HA) already defined in ExternExample.obj
以为是ExternExample.h文件中没有加条件编译,把ExternExample.h变成
ifndef _EXTERN_EXAMPLE
#define _EXTERN_EXAMPLE
int temp;
endif
结果还是错,所以才有了以下的调查。
一切都要从c++的编译开始:
概念:
1、定义和声明
C++的定义和声明概念是非常重要的,也是最容易被新手忽视的,介绍一下区分方法。
对变量:
-
声明:extern int a;//extern是外部的意思,就是说这个变量不在这定义,只是声明有这个变量,在外部定义,这样编译的时候会认为变量时外部变量
-
定义:extern int a = 0;int a;int a =0;//只要分配了内存的,都是定义,就算声明了extern也一样。
所以对变量来说,只要分配了内存,就是定义。
对函数:
-
声明:extern int function(); int funciton();//函数有没有extern都一样,不管你是不是extern了,只要没有定义函数体,就是声明
-
定义:extern int function(){return 1} int function(){return 1} //只要定义了函数体,就是声明
所以对函数来说,只要定义了函数体,就是定义。
2、编译单元
首先要理解c++的一个编译单元是cpp文件,所以每一个cpp文件都会被编译成一个.obj文件,而cpp文件要联系它的头文件
3、.h文件和.cpp文件
在最开始写c的时候,是没有.h文件的,所有的内容都写在.c文件里面,后来人们发现,每一次引用其他文件内容的时候,都要进行声明,很麻烦,
尤其是当一个文件中的变量名改了之后,需要改所有文件中用到的地方,简直灾难。所以后来加入了.h文件,把所有的声明写在.h文件里面,
使用的人只要include就行,并且变量名改变之后,因为直接include进来后展开,所以也不需要使用者改变代码。
编译器编译过程
1、预编译
所谓预编译,其实就是#include展开和宏展开,上述代码被展开后,main.cpp里面就了int temp这个定义,注意是定义。
而之前的条件编译语句#ifndef _EXTERN_EXAMPLE,在这里起作用,如果XX.h包含了ExternExample.h,maip.cpp既包含了XX.h也包含了ExternExample.h
就会出问题,这就是重定义,如果ExternExample.h里的内容是声明extern int a,实际测试,包含两次是没有问题的,估计是忽略了吧。
2、编译
编译就是将刚刚展开之后的.h中的内容和.cpp中的内容作为一个编译单元来进行编译。
上述展开后的main.cpp代码可能被编译成
符号 地址
temp 0x00??
_main 0x00??
这些全局的定义,都会被记录到一张表中,称为符号导出表。
如果编译单元之中有extern外部变量怎么办么?这些变量也会被记录到一张表中,称为未知符号表。
然后每个.cpp文件都会按这种方式被编译成一个.obj文件
3、连接
编译好之后的obj是独立的,需要连接在一起,才能成为一个程序。
因为之前生成的.obj文件都是独立的,所以里面的地址都是相对的,所以需要地址重定向,比如将A.obj定向到0x00002000~0x00003000之间,B.obj定向到0x00005000~0x00006000之间
这样就会生成另一张表,地址重定向表。
而未知符号表里面的地址怎么办?就从其他.obj文件中的导出符号表中找,找到地址之后,填上ok了。
本文程序的错误就出在这里,前面我们知道main.cpp导出符号里有int temp,而ExternExample.h的导出符号里也有int temp。
所以错误为 error LNK2005: "int temp" (?temp@@3HA) already defined in ExternExample.obj。
结论:
1、.h头文件中一定不要有定义,只能有声明,定义全部卸载.cpp文件里面
2、在头文件中,需要使用条件编译(windows中可以用#pragma once代替),防止重定义
原文链接: https://www.cnblogs.com/chjtao/p/4673224.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/219558
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!