C/C++ include 条件编译 extern及编译连接浅析
问题描述:
源于在尝试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++的定义和声明概念是非常重要的,也是最容易被新手忽视的,介绍一下区分方法。
对变量:
1. 声明:extern int a;//extern是外部的意思,就是说这个变量不在这定义,只是声明有这个变量,在外部定义,这样编译的时候会认为变量时外部变量
2. 定义:extern int a = 0;int a;int a =0;//只要分配了内存的,都是定义,就算声明了extern也一样。
所以对变量来说,只要分配了内存,就是定义。
对函数:
1. 声明:extern int function(); int funciton();//函数有没有extern都一样,不管你是不是extern了,只要没有定义函数体,就是声明
2. 定义: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代替),防止重定义