C/C++中extern关键字
一.基本解释
- extern关键字可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。所以一般extern关键字后面跟着的都是声明。
- 它有两个作用:
- 当extern与"C"一起连用时,如:extern "C" void fun(int a, int b); 这就告诉编译器在编译fun这个函数名时按着C的规则去翻译相应的函数名而不是C++的,C++的规则在翻译这个函数名时会把fun这个名字弄得面目全非,例如:fun@aBc_isdnfsdf#%&,不同编译器采取的翻译方法不一样。这么做的原因因为C++支持函数重载。
下面是一个标准的写法:
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif /*__cplusplus*/
}
#endif
#endif /*__cplusplus*/
- 当extern不与"C"一起修饰变量或函数,例如在头文件中:extern int f_int; 它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用,一定要切记是声明。
例:当B模块(编译单元)引用A模块(编译单元)中定义的全局变量或函数时,它只要包含A模块的头文件即可,在编译阶段,模块B虽然找不到该函数或变量,但不会报错,它会在链接时从模块A生成的目标代码中找到该函数。
二.当extern修饰变量时,定义式要严格对应声明时的格式
- 例如:在源文件定义了一个数组:char a[666]; 头文件中声明的格式却是:extern char *a; 会造成非法访问,原因在于指向类型T的指针并不等价于类型T的数组。应将声明改为extern char a[];
- 当你在.cpp文件中定义了一个全局的变量,这个全局变量如果要被引用,就放到.h中并用extern声明。
三.单方面修改extern函数原型
- 当函数提供方单方面修改函数原型时,若使用方不知情而继续沿用原来的extern声明,这样编译时编译器不会报错。但运行过程中,由于输入参数的不匹配,往往会造成系统错误。
- 处理这个问题目前通常的做法是函数提供方在自己的.h中提供对外部接口的声明,然后函数调用方include该头文件,从而省去extern这一步。
四.extern函数声明
- 如果函数声明中带有关键字extern,仅仅暗示这个函数可能在其他源文件里定义,无其他作用。extern int f();与int f();没有明显区别。
- 它的用处在于在程序中取代include "*.h"来声明函数,例如:
(1)在test1.h中有下列声明
#ifdef TEST1H
#define TEST1H
extern char g_str[];//声明全局变量
void fun1();
#endif
(2)在test1.cpp中
#include "test1.h"
char g_str[] = "123456";//定义全局变量
void fun1(){cout << g_str << endl;}
(3)以上的test1模块,编译和链接都可以通过,如果我们还有test2模块且也想使用g_str,只需要在.cpp中引用就可以:
#include "test1.h"
void fun2(){cout << g_str << endl;}
此时若打开test1.obj,你可以在里面找到"123456"这个字符串,但是你却不能在test2.obj中找到它,因为g_str为整个工程项目的全局变量,在内存中只存在一份,如果test2.obj也有一份,会在链接时报告重复定义错误。
(4)当全局变量声明与定义放在一起时,例如把test1.h改为:
extern char g_str[] = "123456";//此时相当于无extern
然后令test1.cpp中的定义去掉,此时再编译链接test1和test2两个模块时,会报链接错误,这是因为全局变量g_str的定义放到了头文件中,test1.cpp与test2.cpp同时包含test1.h,此时链接器在链接test1和test2时发现了两个g_str,造成重复定义。
解决这个问题的办法是:把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与static
- extern表示该变量在其他地方已经定义过了,在这里要使用那个变量。
- static表示静态变量,分配内存时储存在静态区,不存储在栈上。
static作用范围是内部连接的关系,和extern有点相反;它和对象本身是分开存储的,extern也是分开存储,但extern可以被其他对象用extern引用,但static只允许对象本身用它。它俩的区别:
1.extern和static不能同时修饰一个变量。
2.static修饰的全局变量声明与定义同时进行,也就是说,当你在头文件中使用static声明全局变量后,它也同时被定义了。
3.static修饰的全局变量的作用域只能是本身编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元看不到它,例如:
(1)test1.h:
#ifndef TEST1H
#define TEST1H
static cahr g_str[] = "123456";
void fun1();
#endif
(2)test1.cpp:
#include "test1.h"
void fun1(){cout << g_str << endl;}
(3)test2.cpp:
void fun2(){cout << g_str << endl; }
以上两个编译单元可以链接成功,当你打开test1.obj时,可以在其中找到字符串“123456”,同时,也能在test2.obj中找到他们,之所以能链接成功而没有报重复定义,是因为虽然他们有相同的内容,但是存储的物理地址不同,就像两个不同的变量赋给了相同的值,而这两个变量分别作用于它们各自的编译单元。
此时如果你想探查到底,跟踪调试上述代码,结果你可以会发现两个编译单元的g_str的内存地址相同。这可与我们总结的严重不符,原因在哪呢?其实这是编译器在欺骗你,大多数编译器都有代码优化功能,以达成生成的目标程序更节省内存,执行效率更高,当编译器在连接各个单元的时候,它会把相同内容的内存只拷贝一份,就例如上述的“123456”。
下列的代码能够马上拆穿这个谎言:
(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)main.cpp:
void main(){
fun1();//a23456
fun2();//123456
}
此时在跟踪代码时,就会发现两个编译单元中的g_str地址并不相同,因为你在一处修改了它,所以编译器强行恢复内存的原貌,在内存中存在了两份拷贝给两个模块。正因为static有这样的特性,所以一般定义static全局变量时,都把它放在源文件而不是头文件,这样就不会给其他模块造成不必要的信息污染。
六.extern和const
const可以与extern连用来声明该常量可以作用于其他编译模块中,头文件中extern const char g_str[];
,然后在源文件定义const char g_str[] = "123456"
所以当const单独使用时与static相同,而当与extern连用时,特性就跟extern的一样了。
内容总结于博主chao_yu