基本解释

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。此外extern也可用来进行链接指定。

extern有两个作用:

  1. 当它与”C”一起连用时,如: extern “C” void fun(int a, int b);则告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字变得面目全非,可能是fun@aBc_int_int#%$也可能是别的,不同的编译器采用的方法不一样,因为C++支持函数的重载.

  2. 当extern不与”C”在一起修饰变量或函数时,如在头文件中: extern int g_Int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用,记住它是一个声明不是定义!

    也就是说B模块(编译单元)要是引用模块(编译单元)A中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但它不会报错,它会在连接时从模块A生成的目标代码中找到此函数。

extern 变量

现代编译器一般采用按文件编译的方式,因此在编译时,各个文件中定义的全局变量是互相透明的,也就是说,在编译时,全局变量的可见域限制在文件内部。

下面举一个简单的例子:创建一个工程,里面含有A.cpp和B.cpp两个简单的C++源文件:

1
2
3
4
5
6
7
8
9
//A.cpp
int i;
void main()
{

}

//B.cpp
int i;

这两个文件极为简单,在A.cpp中我们定义了一个全局变量i,在B中我们也定义了一个全局变量i。

我们对A和B分别编译,都可以正常通过编译,但是进行链接的时候,却出现了错误.

在编译阶段,各个文件中定义的全局变量相互是透明的,编译A时觉察不到B中也定义了i,同样,编译B时觉察不到A中也定义了i。

但是到了链接阶段,要将各个文件的内容“合为一体”,因此,如果某些文件中定义的全局变量名相同的话,在这个时候就会出现错误,也就是上面提示的重复定义的错误。

因此,各个文件中定义的全局变量名不可相同。

在链接阶段,各个文件的内容(实际是编译产生的obj文件)是被合并到一起的,因而,定义于某文件内的全局变量,在链接完成后,它的可见范围被扩大到了整个程序。

这样一来,按道理说,一个文件中定义的全局变量,可以在整个程序的任何地方被使用,举例说,如果A文件中定义了某全局变量,那么B文件中应可以该变量。修改我们的程序,加以验证:

1
2
3
4
5
6
7
8
//A.cpp
void main()
{
i = 100; //试图使用B中定义的全局变量
}

//B.cpp
int i;

结果必然是编译错误。因为文件中定义的全局变量的可见性扩展到整个程序是在链接完成之后,而在编译阶段,他们的可见性仍局限于各自的文件。

编译器的目光不够长远,编译器没有能够意识到,某个变量符号虽然不是本文件定义的,但是它可能是在其它的文件中定义的。

虽然编译器不够远见,但是我们可以给它提示,帮助它来解决上面出现的问题。这就是extern的作用了。

extern的原理很简单,就是告诉编译器:“你现在编译的文件中,有一个标识符虽然没有在本文件中定义,但是它是在别的文件中定义的全局变量,你要放行!”

我们为上面的错误程序加上extern关键字:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//A.cpp

extern int i;
void main()
{
i = 100; //试图使用B中定义的全局变量
}

//B.cpp
int i;

顺利通过编译,链接。

extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。

extern long a, *pA, &ra; 

上面就声明(不是定义)了三个变量a、pA和ra。因为extern表示外部的意思,因此上面就被认为是告诉编译器有三个外部的变量,为a、pA和ra,故被认为是声明语句,所以上面将不分配任何内存。

注意:

  在一个源文件里定义了一个数组:char a[6];

  在另外一个文件里用下列语句进行了声明:extern char *a;   请问,这样可以吗?

分析:

  不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。 在使用extern时候要严格对应声明时的格式.

extern 和 static修饰全局变量

  1. extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量.

  2. static 表示静态的变量,分配内存的时候, 存储在静态区,不存储在栈上面.

static 作用范围是内部连接的关系, 和extern有点相反.它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern 引用,而static 不可以,只允许对象本身用它.

具体差别:

  1. static与extern是一对“水火不容”的家伙,也就是说extern和static不能同时修饰一个变量

  2. 其次,static修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static声明了全局变量后,它也同时被定义了;

  3. static修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它

static修饰全局变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(1) test1.h:
#ifndef TEST1H
#define TEST1H
static char g_str[] = "123456"; 
void fun1();
#endif

(2) test1.cpp:
#include "test1.h"
void fun1()  {   cout << g_str << endl;  }
(3) test2.cpp
#include "test1.h"
void fun2()  {   cout << g_str << endl;  }

以上两个编译单元可以连接成功, 当你打开test1.obj时,你可以在它里面找到字符串”123456”,同时你也可以在test2.obj中找到它们,它们之所以可以连接成功而没有报重复定义的错误是因为虽然它们有相同的内容,但是存储的物理地址并不一样,就像是两个不同变量赋了相同的值一样,而这两个变量分别作用于它们各自的编译单元。

也许你比较较真,自己偷偷的跟踪调试上面的代码,结果你发现两个编译单元(test1,test2)的g_str的内存地址相同,于是你下结论static修饰的变量也可以作用于其他模块,但是我要告诉你,那是你的编译器在欺骗你,大多数编译器都对代码都有优化功能,以达到生成的目标程序更节省内存,执行效率更高,当编译器在连接各个编译单元的时候,它会把相同内容的内存只拷贝一份,比如上面的”123456”, 位于两个编译单元中的变量都是同样的内容,那么在连接的时候它在内存中就只会存在一份了,如果你把上面的代码改成下面的样子,你马上就可以拆穿编译器的谎言:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(1) test1.cpp:
#include "test1.h"
void fun1()
{
    g_str[0] = ''a'';
    cout << g_str << endl;
}

(2) test2.cpp
#include "test1.h"
void fun2()  {  cout << g_str << endl;  }
(3) void main()     {
    fun1(); // a23456
    fun2(); // 123456
}

这个时候你在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用。正是因为static有以上的特性,所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染,同样记住这个原则吧!

extern修饰全局变量

在test1.h中有下列声明:

1
2
3
4
5
#ifndef TEST1H
#define TEST1H
extern char g_str[]; // 声明全局变量g_str
void fun1();
#endif

在test1.cpp中

1
2
3
4
5
6
7
8
#include "test1.h"
char g_str[] = "123456"; // 定义全局变量g_str
void fun1() { cout << g_str << endl; }
以上是test1模块, 它的编译和连接都可以通过,如果我们还有test2模块也想使用g_str,只需要在原文件中引用就可以了

#include "test1.h"

 void fun2()    { cout << g_str << endl;    }

上test1和test2可以同时编译连接通过,如果你感兴趣的话可以用ultraEdit打开test1.obj,你可以在里面找到”123456”这个字符串,但是你却不能在test2.obj里面找到,这是因为g_str是整个工程的全局变量,在内存中只存在一份,test2.obj这个编译单元不需要再有一份了,不然会在连接时报告重复定义这个错误!

有些人喜欢把全局变量的声明和定义放在一起,这样可以防止忘记了定义,如把上面test1.h改为

extern char g_str[] = "123456"; // 这个时候相当于没有extern

然后把test1.cpp中的g_str的定义去掉,这个时候再编译连接test1和test2两个模块时,会报连接错误,这是因为你把全局变量g_str的定义放在了头文件之后,test1.cpp这个模块包含了test1.h所以定义了一次g_str,而test2.cpp也包含了test1.h所以再一次定义了g_str,这个时候连接器在连接test1和test2时发现两个g_str。

如果你非要把g_str的定义放在test1.h中的话,那么就把test2的代码中#include “test1.h”去掉 换成:

extern char g_str[];
void fun2()   {  cout << g_str << endl;   }

这个时候编译器就知道g_str是引自于外部的一个编译模块了,不会在本模块中再重复定义一个出来,但是我想说这样做非常糟糕,因为你由于无法在test2.cpp中使用#include “test1.h”,那么test1.h中声明的其他函数你也无法使用了,除非也用都用extern修饰,这样的话你光声明的函数就要一大串,而且头文件的作用就是要给外部提供接口使用的,所以 请记住, 只在头文件中做声明,真理总是这么简单。

extern 和const

C++中const修饰的全局常量具有跟static相同的特性,即它们只能作用于本编译模块中,但是const可以与extern连用来声明该常量可以作用于其他编译模块中, 如

extern const char g_str[];

然后在原文件中别忘了定义:

const char g_str[] = "123456"; 

所以当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!所以对const我没有什么可以过多的描述,我只是想提醒你,

const char* g_str = "123456" 

const char g_str[] ="123465"

是不同的, 前面那个const 修饰的是char* 而不是g_str,它的g_str并不是常量,它被看做是一个定义了的全局变量(可以被其他编译单元使用), 所以如果你像让char* g_str遵守const的全局常量的规则,最好这么定义

const char* const g_str="123456".

extern 函数声明

函数默认就是外部的,所以在语法上写不写extern都一样。 但从风格上说,显式地写上extern可以提醒阅读者函数的定义在其它文件内。

如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:

extern int f(); 

int f();

多文件程序中的函数调用

一般情况下,工程中的文件都是CPP文件(以及头文件)。如下面的程序仅包含两个文件:A.CPP和B.CPP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//A.CPP
void func();
void main()
{
     func();
}    

//B.CPP
void func()
{
}

程序的结构是这样的:在文件B.CPP中定义了一个函数void func(),main函数位于文件A.CPP,在main函数中调用了B中定义的函数func()。

要在A中调用B中定义的函数,必须要加上该函数的声明。如本例中的void func();就是对函数func()的声明。

只要加上该函数的声明即可,不需要extern关键字

如果没有声明的话,编译A.CPP时就会出错。因为编译器的目光只局限于被编译文件,必须通过加入函数声明来告诉编译器:“某个函数是定义在其它的文件中的,你要放行!”,这一点跟用extern来声明外部全局变量是一个道理。

需要注意的是,一般的程序都是通过包含头文件来完成函数的声明。拿本例来说,一般是创建一个头文件B.H,在头文件中加入声明语句void func(); 并且在A.CPP中加入包含语句:#include “B.H”。

在C++程序中,头文件的功能从函数声明被扩展为类的定义。

单文件程序中extern标识函数声明

extern void ABC( long );   或   extern long AB( short b );

上面的extern等同于不写,因为编译器根据最后的“;”就可以判断出来上面是函数声明,而且提供的“外部”这个信息对于函数来说没有意义,编译器将不予理会。extern实际还指定其后修饰的标识符的修饰方式,实际应为extern”C”或extern”C++”,分别表示按照C语言风格和C++语言风格来解析声明的标识符。

当书写extern void ABC( long );时,是extern”C”还是extern”C++”?在VC中,如果上句代码所在源文件的扩展名为.cpp以表示是C++源代码,则默认解释成后者。如果是.c,则默认解释成前者。不过在VC中还可以通过修改项目选项来改变上面的默认设置。而extern long a;也和上面是同样的。

因此如下:

extern"C++" void ABC(), *ABC( long ), ABC( long, short ); 
int main(){ ABC(); } 

上面第一句就告诉编译器后续代码可能要用到这个三个函数,叫编译器不要报错。假设上面程序放在一个VC项目下的a.cpp中,编译a.cpp将不会出现任何错误。但当连接时,编译器就会说符号“?ABC@@YAXXZ”没找到,因为这个项目只包含了一个文件,连接也就只连接相应的a.obj以及其他的一些必要库文件。连接器在它所能连接的所有对象文件(a.obj)以及库文件中查找符号“?ABC@@YAXXZ”对应的地址是什么,不过都没找到,故报错。换句话说就是main函数使用了在a.cpp以外定义的函数void ABC();,但没找到这个函数的定义。

应注意,如果写成int main() { void ( *pA ) = ABC; }依旧会报错,因为ABC就相当于一个地址,这里又要求计算此地址的值(即使并不使用pA),故同样报错。

为了消除上面的错误,就应该定义函数void ABC();,既可以在a.cpp中,如main函数的后面,也可以重新生成一个.cpp文件,加入到项目中,在那个.cpp文件中定义函数ABC。因此如下即可:

extern"C++" void ABC(), *ABC( long ), ABC( long, short ); 
int main(){ ABC(); } void ABC(){} 

extern “C”

不同编译方式下的函数调用

如果在工程中,不仅有CPP文件,还有以C方式编译的C文件,函数调用就会有一些微妙之处。我们将B.CPP改作B.C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//A.CPP
void func();
void main()
{
     func();
}

//B.C
void func()
{

}

对A.CPP和B.C分别编译,都没有问题,但是链接时出现错误。

原因就在于不同的编译方式产生的冲突。

对于文件A,是按照C++的方式进行编译的,其中的func()调用被编译成了

call    ?func1@@YAXXZ    

如果B文件也是按照C++方式编译的,那么B中的func函数名也会被编译器改成?func1@@YAXXZ,这样的话,就没有任何问题。

但是现在对B文件,是按照C方式编译的,B中的func函数名被改成了_func,这样一来,A中的call ?func1@@YAXXZ这个函数调用就没有了着落,因为在链接器看来,B文件中没有名为?func1@@YAXXZ的函数。

事实是,我们编程者知道,B文件中有A中调用的func函数的定义,只不过它是按照C方式编译的,故它的名字被改成了_func。因而,我们需要通过某种方式告诉编译器:“B中定义的函数func()经编译后命名成了_func,而不是?func1@@YAXXZ,你必须通过call _func来调用它,而不是call ?func1@@YAXXZ。”简单的说,就是告诉编译器,调用的func()函数是以C方式编译的,fun();语句必须被编译成call _func;而不是call ?func1@@YAXXZ。

我们可以通过extern关键字,来帮助编译器解决上面提到的问题。

对于本例,只需将A.CPP改成如下即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//A.CPP
extern "C"
{
     void func();
}

void main()
{

     func();
}

察看汇编代码,发现此时的func();语句被编译成了call _func。

不同编译方式下的变量调用

考虑下面的程序:

1
2
3
4
5
6
7
8
9
//A.CPP
extern int i;
void main()
{
     i = 100;
}

//B.C
int i;

程序很简单:在文件B.C中定义了一个全局变量i,在A.CPP中使用了这个全局变量。

编译没有问题,链接时却出现错误。这是因为,在C方式编译下,i被重命名为_i,而在C++方式下,i会被重命名为?i@@3HA。

因而,我们只用extern int i;来声明还不够,必须告诉编译器,全局变量i是以C方式编译的,

它会被重命名为_i,而不是?i@@3HA。

我们修改A.CPP,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//A.CPP
extern "C"
{
     int i;
}

void main()
{
     i = 100;
}

程序正常通过编译和链接。

我们察看一下汇编代码,发现语句

i = 100;

被编译成了

mov  DWORD PTR _i, 100。

C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。 下面是一个标准的写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
 #endif
 #endif /* __cplusplus */ 
 
 
//.h文件结束的地方
 #ifdef __cplusplus
 #if __cplusplus
}
#endif
#endif /* __cplusplus */